toolbarview.js 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976
  1. /**
  2. * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  4. */
  5. /* global document, Event, console, setTimeout */
  6. import ToolbarView from '../../src/toolbar/toolbarview';
  7. import ToolbarSeparatorView from '../../src/toolbar/toolbarseparatorview';
  8. import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler';
  9. import ComponentFactory from '../../src/componentfactory';
  10. import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker';
  11. import FocusCycler from '../../src/focuscycler';
  12. import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
  13. import ViewCollection from '../../src/viewcollection';
  14. import global from '@ckeditor/ckeditor5-utils/src/dom/global';
  15. import View from '../../src/view';
  16. import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
  17. import { add as addTranslations, _clear as clearTranslations } from '@ckeditor/ckeditor5-utils/src/translation-service';
  18. import Locale from '@ckeditor/ckeditor5-utils/src/locale';
  19. describe( 'ToolbarView', () => {
  20. let locale, view;
  21. testUtils.createSinonSandbox();
  22. before( () => {
  23. addTranslations( 'pl', {
  24. 'Editor toolbar': 'Pasek narzędzi edytora'
  25. } );
  26. addTranslations( 'en', {
  27. 'Editor toolbar': 'Editor toolbar'
  28. } );
  29. } );
  30. after( () => {
  31. clearTranslations();
  32. } );
  33. beforeEach( () => {
  34. locale = new Locale();
  35. view = new ToolbarView( locale );
  36. view.render();
  37. } );
  38. afterEach( () => {
  39. sinon.restore();
  40. view.destroy();
  41. } );
  42. describe( 'constructor()', () => {
  43. it( 'should set view#locale', () => {
  44. expect( view.locale ).to.equal( locale );
  45. } );
  46. it( 'should set view#isVertical', () => {
  47. expect( view.isVertical ).to.be.false;
  48. } );
  49. it( 'should create view#items collection', () => {
  50. expect( view.items ).to.be.instanceOf( ViewCollection );
  51. } );
  52. it( 'should not create view#groupedItems collection', () => {
  53. expect( view.groupedItems ).to.be.null;
  54. } );
  55. it( 'creates #focusTracker instance', () => {
  56. expect( view.focusTracker ).to.be.instanceOf( FocusTracker );
  57. } );
  58. it( 'creates #keystrokes instance', () => {
  59. expect( view.keystrokes ).to.be.instanceOf( KeystrokeHandler );
  60. } );
  61. it( 'should create view#itemsView', () => {
  62. expect( view.itemsView ).to.be.instanceOf( View );
  63. } );
  64. it( 'should not create view#groupedItemsDropdown', () => {
  65. expect( view.groupedItemsDropdown ).to.be.null;
  66. } );
  67. it( 'should set view#shouldGroupWhenFull', () => {
  68. expect( view.shouldGroupWhenFull ).to.be.false;
  69. } );
  70. it( 'should create view#_components collection', () => {
  71. expect( view._components ).to.be.instanceOf( ViewCollection );
  72. } );
  73. it( 'creates #_itemsFocusCycler instance', () => {
  74. expect( view._itemsFocusCycler ).to.be.instanceOf( FocusCycler );
  75. } );
  76. it( 'creates #_componentsFocusCycler instance', () => {
  77. expect( view._componentsFocusCycler ).to.be.instanceOf( FocusCycler );
  78. } );
  79. describe( '#shouldGroupWhenFull', () => {
  80. it( 'updates the state of grouped items immediatelly when set true', () => {
  81. sinon.spy( view, 'updateGroupedItems' );
  82. view.shouldGroupWhenFull = true;
  83. sinon.assert.calledOnce( view.updateGroupedItems );
  84. } );
  85. // Possibly in the future a possibility to turn the automatic grouping off could be required.
  86. // As for now, there is no such need, so there is no such functionality.
  87. it( 'does nothing if toggled false', () => {
  88. view.shouldGroupWhenFull = true;
  89. expect( () => {
  90. view.shouldGroupWhenFull = false;
  91. } ).to.not.throw();
  92. } );
  93. it( 'starts observing toolbar resize immediatelly when set true', () => {
  94. function FakeResizeObserver( callback ) {
  95. this.callback = callback;
  96. }
  97. FakeResizeObserver.prototype.observe = sinon.spy();
  98. FakeResizeObserver.prototype.disconnect = sinon.spy();
  99. testUtils.sinon.stub( global.window, 'ResizeObserver' ).value( FakeResizeObserver );
  100. expect( view._groupWhenFullResizeObserver ).to.be.null;
  101. view.shouldGroupWhenFull = true;
  102. sinon.assert.calledOnce( view._groupWhenFullResizeObserver.observe );
  103. sinon.assert.calledWithExactly( view._groupWhenFullResizeObserver.observe, view.element );
  104. } );
  105. it( 'updates the state of grouped items upon resize', () => {
  106. sinon.spy( view, 'updateGroupedItems' );
  107. function FakeResizeObserver( callback ) {
  108. this.callback = callback;
  109. }
  110. FakeResizeObserver.prototype.observe = sinon.spy();
  111. FakeResizeObserver.prototype.disconnect = sinon.spy();
  112. testUtils.sinon.stub( global.window, 'ResizeObserver' ).value( FakeResizeObserver );
  113. expect( view._groupWhenFullResizeObserver ).to.be.null;
  114. view.shouldGroupWhenFull = true;
  115. view._groupWhenFullResizeObserver.callback( [
  116. { contentRect: { width: 42 } }
  117. ] );
  118. sinon.assert.calledTwice( view.updateGroupedItems );
  119. } );
  120. } );
  121. } );
  122. describe( 'template', () => {
  123. it( 'should create element from template', () => {
  124. expect( view.element.classList.contains( 'ck' ) ).to.true;
  125. expect( view.element.classList.contains( 'ck-toolbar' ) ).to.true;
  126. } );
  127. it( 'should create #itemsView from template', () => {
  128. expect( view.element.firstChild ).to.equal( view.itemsView.element );
  129. expect( view.itemsView.element.classList.contains( 'ck' ) ).to.true;
  130. expect( view.itemsView.element.classList.contains( 'ck-toolbar__items' ) ).to.true;
  131. } );
  132. describe( 'attributes', () => {
  133. it( 'should be defined', () => {
  134. expect( view.element.getAttribute( 'role' ) ).to.equal( 'toolbar' );
  135. expect( view.element.getAttribute( 'aria-label' ) ).to.equal( 'Editor toolbar' );
  136. } );
  137. it( 'should allow a custom aria-label', () => {
  138. const view = new ToolbarView( locale );
  139. view.ariaLabel = 'Custom label';
  140. view.render();
  141. expect( view.element.getAttribute( 'aria-label' ) ).to.equal( 'Custom label' );
  142. } );
  143. it( 'should allow the aria-label to be translated', () => {
  144. const view = new ToolbarView( new Locale( { uiLanguage: 'pl' } ) );
  145. view.render();
  146. expect( view.element.getAttribute( 'aria-label' ) ).to.equal( 'Pasek narzędzi edytora' );
  147. } );
  148. } );
  149. describe( 'event listeners', () => {
  150. it( 'prevent default on #mousedown', () => {
  151. const evt = new Event( 'mousedown', { bubbles: true } );
  152. const spy = sinon.spy( evt, 'preventDefault' );
  153. view.element.dispatchEvent( evt );
  154. sinon.assert.calledOnce( spy );
  155. } );
  156. } );
  157. } );
  158. describe( 'element bindings', () => {
  159. describe( 'class', () => {
  160. it( 'reacts on view#isVertical', () => {
  161. view.isVertical = false;
  162. expect( view.element.classList.contains( 'ck-toolbar_vertical' ) ).to.be.false;
  163. view.isVertical = true;
  164. expect( view.element.classList.contains( 'ck-toolbar_vertical' ) ).to.be.true;
  165. } );
  166. it( 'reacts on view#class', () => {
  167. view.class = 'foo';
  168. expect( view.element.classList.contains( 'foo' ) ).to.be.true;
  169. view.class = 'bar';
  170. expect( view.element.classList.contains( 'bar' ) ).to.be.true;
  171. view.class = false;
  172. expect( view.element.classList.contains( 'foo' ) ).to.be.false;
  173. expect( view.element.classList.contains( 'bar' ) ).to.be.false;
  174. } );
  175. it( 'reacts on view#shouldGroupWhenFull', () => {
  176. view.shouldGroupWhenFull = false;
  177. expect( view.element.classList.contains( 'ck-toolbar_grouping' ) ).to.be.false;
  178. view.shouldGroupWhenFull = true;
  179. expect( view.element.classList.contains( 'ck-toolbar_grouping' ) ).to.be.true;
  180. } );
  181. } );
  182. } );
  183. describe( 'render()', () => {
  184. it( 'registers #_components in #focusTracker', () => {
  185. const view = new ToolbarView( locale );
  186. const spyAdd = sinon.spy( view.focusTracker, 'add' );
  187. const spyRemove = sinon.spy( view.focusTracker, 'remove' );
  188. view._components.add( focusable() );
  189. view._components.add( focusable() );
  190. sinon.assert.notCalled( spyAdd );
  191. view.render();
  192. // First call is for the #itemsView.
  193. sinon.assert.calledThrice( spyAdd );
  194. view._components.remove( 1 );
  195. sinon.assert.calledOnce( spyRemove );
  196. view.destroy();
  197. } );
  198. it( 'starts listening for #keystrokes coming from #element', () => {
  199. const view = new ToolbarView( new Locale() );
  200. const spy = sinon.spy( view.keystrokes, 'listenTo' );
  201. view.render();
  202. sinon.assert.calledOnce( spy );
  203. sinon.assert.calledWithExactly( spy, view.element );
  204. view.destroy();
  205. } );
  206. describe( 'activates keyboard navigation for the toolbar', () => {
  207. it( 'so "arrowup" focuses previous focusable item', () => {
  208. const keyEvtData = getArrowKeyData( 'arrowup' );
  209. // No children to focus.
  210. view.keystrokes.press( keyEvtData );
  211. sinon.assert.calledOnce( keyEvtData.preventDefault );
  212. sinon.assert.calledOnce( keyEvtData.stopPropagation );
  213. view.items.add( nonFocusable() );
  214. view.items.add( nonFocusable() );
  215. // No focusable children.
  216. view.keystrokes.press( keyEvtData );
  217. sinon.assert.calledTwice( keyEvtData.preventDefault );
  218. sinon.assert.calledTwice( keyEvtData.stopPropagation );
  219. view.items.add( focusable() );
  220. view.items.add( nonFocusable() );
  221. view.items.add( focusable() );
  222. // Mock the last item is focused.
  223. view.itemsView.focusTracker.isFocused = true;
  224. view.itemsView.focusTracker.focusedElement = view.items.get( 4 ).element;
  225. view.keystrokes.press( keyEvtData );
  226. sinon.assert.calledThrice( keyEvtData.preventDefault );
  227. sinon.assert.calledThrice( keyEvtData.stopPropagation );
  228. sinon.assert.calledOnce( view.items.get( 2 ).focus );
  229. } );
  230. it( 'so "arrowleft" focuses previous focusable item', () => {
  231. const keyEvtData = getArrowKeyData( 'arrowleft' );
  232. view.items.add( focusable() );
  233. view.items.add( nonFocusable() );
  234. view.items.add( focusable() );
  235. // Mock the last item is focused.
  236. view.itemsView.focusTracker.isFocused = true;
  237. view.itemsView.focusTracker.focusedElement = view.items.get( 2 ).element;
  238. view.keystrokes.press( keyEvtData );
  239. sinon.assert.calledOnce( view.items.get( 0 ).focus );
  240. } );
  241. it( 'so "arrowdown" focuses next focusable item', () => {
  242. const keyEvtData = getArrowKeyData( 'arrowdown' );
  243. // No children to focus.
  244. view.keystrokes.press( keyEvtData );
  245. sinon.assert.calledOnce( keyEvtData.preventDefault );
  246. sinon.assert.calledOnce( keyEvtData.stopPropagation );
  247. view.items.add( nonFocusable() );
  248. view.items.add( nonFocusable() );
  249. // No focusable children.
  250. view.keystrokes.press( keyEvtData );
  251. sinon.assert.calledTwice( keyEvtData.preventDefault );
  252. sinon.assert.calledTwice( keyEvtData.stopPropagation );
  253. view.items.add( focusable() );
  254. view.items.add( nonFocusable() );
  255. view.items.add( focusable() );
  256. // Mock the last item is focused.
  257. view.itemsView.focusTracker.isFocused = true;
  258. view.itemsView.focusTracker.focusedElement = view.items.get( 4 ).element;
  259. view.keystrokes.press( keyEvtData );
  260. sinon.assert.calledThrice( keyEvtData.preventDefault );
  261. sinon.assert.calledThrice( keyEvtData.stopPropagation );
  262. sinon.assert.calledOnce( view.items.get( 2 ).focus );
  263. } );
  264. it( 'so "arrowright" focuses next focusable item', () => {
  265. const keyEvtData = getArrowKeyData( 'arrowright' );
  266. view.items.add( focusable() );
  267. view.items.add( nonFocusable() );
  268. view.items.add( focusable() );
  269. // Mock the last item is focused.
  270. view.itemsView.focusTracker.isFocused = true;
  271. view.itemsView.focusTracker.focusedElement = view.items.get( 0 ).element;
  272. view.keystrokes.press( keyEvtData );
  273. sinon.assert.calledOnce( view.items.get( 2 ).focus );
  274. } );
  275. describe( 'when #shouldGroupWhenFull is true', () => {
  276. beforeEach( () => {
  277. document.body.appendChild( view.element );
  278. view.element.style.width = '200px';
  279. view.shouldGroupWhenFull = true;
  280. } );
  281. afterEach( () => {
  282. view.element.remove();
  283. } );
  284. it( 'navigates from #items to the #groupedItemsDropdown (forwards)', () => {
  285. const keyEvtData = getArrowKeyData( 'arrowright' );
  286. view.items.add( focusable() );
  287. view.items.add( nonFocusable() );
  288. view.items.add( focusable() );
  289. view.updateGroupedItems();
  290. sinon.spy( view.groupedItemsDropdown, 'focus' );
  291. view.focusTracker.isFocused = true;
  292. view.focusTracker.focusedElement = view.itemsView.element;
  293. view.itemsView.focusTracker.isFocused = true;
  294. view.itemsView.focusTracker.focusedElement = view.items.get( 0 ).element;
  295. view.keystrokes.press( keyEvtData );
  296. sinon.assert.calledOnce( view.groupedItemsDropdown.focus );
  297. } );
  298. it( 'navigates from the #groupedItemsDropdown to #items (forwards)', () => {
  299. const keyEvtData = getArrowKeyData( 'arrowright' );
  300. view.items.add( focusable() );
  301. view.items.add( nonFocusable() );
  302. view.items.add( focusable() );
  303. view.updateGroupedItems();
  304. view.focusTracker.isFocused = true;
  305. view.focusTracker.focusedElement = view.groupedItemsDropdown.element;
  306. view.itemsView.focusTracker.isFocused = false;
  307. view.itemsView.focusTracker.focusedElement = null;
  308. view.keystrokes.press( keyEvtData );
  309. sinon.assert.calledOnce( view.items.get( 0 ).focus );
  310. } );
  311. it( 'navigates from #items to the #groupedItemsDropdown (backwards)', () => {
  312. const keyEvtData = getArrowKeyData( 'arrowleft' );
  313. view.items.add( focusable() );
  314. view.items.add( nonFocusable() );
  315. view.items.add( focusable() );
  316. view.updateGroupedItems();
  317. sinon.spy( view.groupedItemsDropdown, 'focus' );
  318. view.focusTracker.isFocused = true;
  319. view.focusTracker.focusedElement = view.itemsView.element;
  320. view.itemsView.focusTracker.isFocused = true;
  321. view.itemsView.focusTracker.focusedElement = view.items.get( 0 ).element;
  322. view.keystrokes.press( keyEvtData );
  323. sinon.assert.calledOnce( view.groupedItemsDropdown.focus );
  324. } );
  325. it( 'navigates from the #groupedItemsDropdown to #items (backwards)', () => {
  326. const keyEvtData = getArrowKeyData( 'arrowleft' );
  327. view.items.add( focusable() );
  328. view.items.add( nonFocusable() );
  329. view.items.add( focusable() );
  330. view.updateGroupedItems();
  331. view.focusTracker.isFocused = true;
  332. view.focusTracker.focusedElement = view.groupedItemsDropdown.element;
  333. view.itemsView.focusTracker.isFocused = false;
  334. view.itemsView.focusTracker.focusedElement = null;
  335. view.keystrokes.press( keyEvtData );
  336. sinon.assert.calledOnce( view.items.get( 0 ).focus );
  337. } );
  338. } );
  339. } );
  340. } );
  341. describe( 'destroy()', () => {
  342. it( 'destroys the #groupedItemsDropdown', () => {
  343. document.body.appendChild( view.element );
  344. view.element.style.width = '200px';
  345. const itemA = focusable();
  346. const itemB = focusable();
  347. const itemC = focusable();
  348. const itemD = focusable();
  349. view.items.add( itemA );
  350. view.items.add( itemB );
  351. view.items.add( itemC );
  352. view.items.add( itemD );
  353. // The dropdown shows up.
  354. view.shouldGroupWhenFull = true;
  355. sinon.spy( view.groupedItemsDropdown, 'destroy' );
  356. view.element.style.width = '500px';
  357. // The dropdown hides; it does not belong to any collection but it still exist.
  358. view.updateGroupedItems();
  359. view.destroy();
  360. sinon.assert.calledOnce( view.groupedItemsDropdown.destroy );
  361. view.element.remove();
  362. } );
  363. it( 'disconnects the #_groupWhenFullResizeObserver', () => {
  364. document.body.appendChild( view.element );
  365. view.element.style.width = '200px';
  366. const itemA = focusable();
  367. const itemB = focusable();
  368. const itemC = focusable();
  369. const itemD = focusable();
  370. view.items.add( itemA );
  371. view.items.add( itemB );
  372. view.items.add( itemC );
  373. view.items.add( itemD );
  374. view.shouldGroupWhenFull = true;
  375. sinon.spy( view._groupWhenFullResizeObserver, 'disconnect' );
  376. view.destroy();
  377. sinon.assert.calledOnce( view._groupWhenFullResizeObserver.disconnect );
  378. view.element.remove();
  379. } );
  380. } );
  381. describe( 'focus()', () => {
  382. it( 'focuses the first focusable of #items in DOM', () => {
  383. // No children to focus.
  384. view.focus();
  385. // The second child is focusable.
  386. view.items.add( nonFocusable() );
  387. view.items.add( focusable() );
  388. view.items.add( nonFocusable() );
  389. view.focus();
  390. sinon.assert.calledOnce( view.items.get( 1 ).focus );
  391. } );
  392. it( 'if no items the first focusable of #items in DOM', () => {
  393. document.body.appendChild( view.element );
  394. view.element.style.width = '10px';
  395. view.items.add( focusable() );
  396. view.items.add( focusable() );
  397. view.shouldGroupWhenFull = true;
  398. sinon.spy( view.groupedItemsDropdown, 'focus' );
  399. view.focus();
  400. sinon.assert.calledOnce( view.groupedItemsDropdown.focus );
  401. } );
  402. } );
  403. describe( 'focusLast()', () => {
  404. it( 'focuses the last focusable of #items in DOM', () => {
  405. // No children to focus.
  406. view.focusLast();
  407. // The second child is focusable.
  408. view.items.add( nonFocusable() );
  409. view.items.add( focusable() );
  410. view.items.add( focusable() );
  411. view.items.add( focusable() );
  412. view.items.add( nonFocusable() );
  413. view.focusLast();
  414. sinon.assert.calledOnce( view.items.get( 3 ).focus );
  415. } );
  416. it( 'focuses the #groupedItemsDropdown when view#shouldGroupWhenFull is true', () => {
  417. document.body.appendChild( view.element );
  418. view.element.style.width = '200px';
  419. view.shouldGroupWhenFull = true;
  420. view.items.add( focusable() );
  421. view.items.add( focusable() );
  422. view.items.add( focusable() );
  423. sinon.spy( view.groupedItemsDropdown, 'focus' );
  424. view.focusLast();
  425. sinon.assert.calledOnce( view.groupedItemsDropdown.focus );
  426. view.element.remove();
  427. } );
  428. } );
  429. describe( 'fillFromConfig()', () => {
  430. let factory;
  431. beforeEach( () => {
  432. factory = new ComponentFactory( {} );
  433. factory.add( 'foo', namedFactory( 'foo' ) );
  434. factory.add( 'bar', namedFactory( 'bar' ) );
  435. } );
  436. it( 'expands the config into collection', () => {
  437. view.fillFromConfig( [ 'foo', 'bar', '|', 'foo' ], factory );
  438. const items = view.items;
  439. expect( items ).to.have.length( 4 );
  440. expect( items.get( 0 ).name ).to.equal( 'foo' );
  441. expect( items.get( 1 ).name ).to.equal( 'bar' );
  442. expect( items.get( 2 ) ).to.be.instanceOf( ToolbarSeparatorView );
  443. expect( items.get( 3 ).name ).to.equal( 'foo' );
  444. } );
  445. it( 'warns if there is no such component in the factory', () => {
  446. const items = view.items;
  447. const consoleWarnStub = sinon.stub( console, 'warn' );
  448. view.fillFromConfig( [ 'foo', 'bar', 'baz' ], factory );
  449. expect( items ).to.have.length( 2 );
  450. expect( items.get( 0 ).name ).to.equal( 'foo' );
  451. expect( items.get( 1 ).name ).to.equal( 'bar' );
  452. sinon.assert.calledOnce( consoleWarnStub );
  453. sinon.assert.calledWithExactly( consoleWarnStub,
  454. sinon.match( /^toolbarview-item-unavailable/ ),
  455. { name: 'baz' }
  456. );
  457. } );
  458. } );
  459. describe( 'updateGroupedItems()', () => {
  460. beforeEach( () => {
  461. document.body.appendChild( view.element );
  462. view.element.style.width = '200px';
  463. } );
  464. afterEach( () => {
  465. view.element.remove();
  466. } );
  467. it( 'only works when #shouldGroupWhenFull', () => {
  468. view.items.add( focusable() );
  469. view.items.add( focusable() );
  470. view.items.add( focusable() );
  471. view.items.add( focusable() );
  472. view.updateGroupedItems();
  473. expect( view.items ).to.have.length( 4 );
  474. expect( view.groupedItems ).to.be.null;
  475. } );
  476. it( 'stays silent if the toolbar is detached from visible DOM', () => {
  477. testUtils.sinon.spy( console, 'warn' );
  478. view.element.remove();
  479. view.items.add( focusable() );
  480. view.items.add( focusable() );
  481. view.items.add( focusable() );
  482. view.items.add( focusable() );
  483. view.shouldGroupWhenFull = true;
  484. sinon.assert.notCalled( console.warn );
  485. } );
  486. it( 'does not group when items fit', () => {
  487. const itemA = focusable();
  488. const itemB = focusable();
  489. view.items.add( itemA );
  490. view.items.add( itemB );
  491. view.shouldGroupWhenFull = true;
  492. expect( view.groupedItems ).to.be.null;
  493. expect( view.groupedItemsDropdown ).to.be.null;
  494. } );
  495. it( 'groups items that overflow into #groupedItemsDropdown', () => {
  496. const itemA = focusable();
  497. const itemB = focusable();
  498. const itemC = focusable();
  499. const itemD = focusable();
  500. view.items.add( itemA );
  501. view.items.add( itemB );
  502. view.items.add( itemC );
  503. view.items.add( itemD );
  504. view.shouldGroupWhenFull = true;
  505. expect( view.items.map( i => i ) ).to.have.members( [ itemA ] );
  506. expect( view.groupedItems.map( i => i ) ).to.have.members( [ itemB, itemC, itemD ] );
  507. expect( view._components ).to.have.length( 3 );
  508. expect( view._components.get( 0 ) ).to.equal( view.itemsView );
  509. expect( view._components.get( 1 ) ).to.be.instanceOf( ToolbarSeparatorView );
  510. expect( view._components.get( 2 ) ).to.equal( view.groupedItemsDropdown );
  511. } );
  512. it( 'ungroups items from #groupedItemsDropdown if there is enough space to display them (all)', () => {
  513. const itemA = focusable();
  514. const itemB = focusable();
  515. const itemC = focusable();
  516. const itemD = focusable();
  517. view.items.add( itemA );
  518. view.items.add( itemB );
  519. view.items.add( itemC );
  520. view.items.add( itemD );
  521. view.shouldGroupWhenFull = true;
  522. expect( view.items.map( i => i ) ).to.have.members( [ itemA ] );
  523. expect( view.groupedItems.map( i => i ) ).to.have.members( [ itemB, itemC, itemD ] );
  524. view.element.style.width = '350px';
  525. // Some grouped items cannot be ungrouped because there is not enough space and they will
  526. // land back in #groupedItems after an attempt was made.
  527. view.updateGroupedItems();
  528. expect( view.items.map( i => i ) ).to.have.members( [ itemA, itemB, itemC ] );
  529. expect( view.groupedItems.map( i => i ) ).to.have.members( [ itemD ] );
  530. } );
  531. it( 'ungroups items from #groupedItemsDropdown if there is enough space to display them (some)', () => {
  532. const itemA = focusable();
  533. const itemB = focusable();
  534. const itemC = focusable();
  535. view.items.add( itemA );
  536. view.items.add( itemB );
  537. view.items.add( itemC );
  538. view.shouldGroupWhenFull = true;
  539. expect( view.items.map( i => i ) ).to.have.members( [ itemA ] );
  540. expect( view.groupedItems.map( i => i ) ).to.have.members( [ itemB, itemC ] );
  541. view.element.style.width = '350px';
  542. // All grouped items will be ungrouped because they fit just alright in the main space.
  543. view.updateGroupedItems();
  544. expect( view.items.map( i => i ) ).to.have.members( [ itemA, itemB, itemC ] );
  545. expect( view.groupedItems ).to.have.length( 0 );
  546. expect( view._components ).to.have.length( 1 );
  547. expect( view._components.get( 0 ) ).to.equal( view.itemsView );
  548. } );
  549. describe( '#groupedItemsDropdown', () => {
  550. it( 'has proper DOM structure', () => {
  551. view.items.add( focusable() );
  552. view.items.add( focusable() );
  553. view.items.add( focusable() );
  554. view.items.add( focusable() );
  555. view.shouldGroupWhenFull = true;
  556. const dropdown = view.groupedItemsDropdown;
  557. expect( view._components.has( view.groupedItemsDropdown ) ).to.be.true;
  558. expect( dropdown.element.classList.contains( 'ck-toolbar__grouped-dropdown' ) );
  559. expect( dropdown.buttonView.label ).to.equal( 'Show more items' );
  560. } );
  561. it( 'shares its toolbarView#items with ToolbarView#groupedItems', () => {
  562. view.items.add( focusable() );
  563. view.items.add( focusable() );
  564. view.items.add( focusable() );
  565. view.items.add( focusable() );
  566. view.shouldGroupWhenFull = true;
  567. expect( view.groupedItemsDropdown.toolbarView.items ).to.equal( view.groupedItems );
  568. } );
  569. } );
  570. describe( '#items overflow checking logic', () => {
  571. it( 'considers the right padding of the toolbar (LTR UI)', () => {
  572. view.class = 'ck-reset_all';
  573. view.element.style.width = '210px';
  574. view.element.style.paddingLeft = '0px';
  575. view.element.style.paddingRight = '20px';
  576. view.items.add( focusable() );
  577. view.items.add( focusable() );
  578. view.shouldGroupWhenFull = true;
  579. expect( view.groupedItems ).to.have.length( 1 );
  580. } );
  581. it( 'considers the left padding of the toolbar (RTL UI)', () => {
  582. const locale = new Locale( { uiLanguage: 'ar' } );
  583. const view = new ToolbarView( locale );
  584. view.extendTemplate( {
  585. attributes: {
  586. dir: locale.uiLanguageDirection
  587. }
  588. } );
  589. view.render();
  590. document.body.appendChild( view.element );
  591. view.class = 'ck-reset_all';
  592. view.element.style.width = '210px';
  593. view.element.style.paddingLeft = '20px';
  594. view.element.style.paddingRight = '0px';
  595. view.items.add( focusable() );
  596. view.items.add( focusable() );
  597. view.shouldGroupWhenFull = true;
  598. expect( view.groupedItems ).to.have.length( 1 );
  599. view.destroy();
  600. view.element.remove();
  601. } );
  602. } );
  603. } );
  604. describe( 'automatic toolbar grouping (#shouldGroupWhenFull = true)', () => {
  605. beforeEach( () => {
  606. document.body.appendChild( view.element );
  607. view.element.style.width = '200px';
  608. } );
  609. afterEach( () => {
  610. view.element.remove();
  611. } );
  612. it( 'updates the UI as new #items are added', () => {
  613. sinon.spy( view, 'updateGroupedItems' );
  614. sinon.assert.notCalled( view.updateGroupedItems );
  615. view.items.add( focusable() );
  616. view.items.add( focusable() );
  617. sinon.assert.calledTwice( view.updateGroupedItems );
  618. } );
  619. it( 'updates the UI as #items are removed', () => {
  620. sinon.spy( view, 'updateGroupedItems' );
  621. sinon.assert.notCalled( view.updateGroupedItems );
  622. view.items.add( focusable() );
  623. sinon.assert.calledOnce( view.updateGroupedItems );
  624. view.items.remove( 0 );
  625. sinon.assert.calledTwice( view.updateGroupedItems );
  626. } );
  627. it( 'updates the UI when the toolbar is being resized (expanding)', done => {
  628. view.items.add( focusable() );
  629. view.items.add( focusable() );
  630. view.items.add( focusable() );
  631. view.items.add( focusable() );
  632. view.items.add( focusable() );
  633. view.element.style.width = '200px';
  634. view.shouldGroupWhenFull = true;
  635. expect( view.items ).to.have.length( 1 );
  636. expect( view.groupedItems ).to.have.length( 4 );
  637. view.element.style.width = '500px';
  638. setTimeout( () => {
  639. expect( view.items ).to.have.length( 5 );
  640. expect( view.groupedItems ).to.have.length( 0 );
  641. done();
  642. }, 100 );
  643. } );
  644. it( 'updates the UI when the toolbar is being resized (narrowing)', done => {
  645. view.items.add( focusable() );
  646. view.items.add( focusable() );
  647. view.items.add( focusable() );
  648. view.items.add( focusable() );
  649. view.items.add( focusable() );
  650. view.element.style.width = '500px';
  651. view.shouldGroupWhenFull = true;
  652. expect( view.items ).to.have.length( 5 );
  653. expect( view.groupedItems ).to.be.null;
  654. view.element.style.width = '200px';
  655. setTimeout( () => {
  656. expect( view.items ).to.have.length( 1 );
  657. expect( view.groupedItems ).to.have.length( 4 );
  658. done();
  659. }, 100 );
  660. } );
  661. it( 'does not react to changes in height', done => {
  662. view.element.style.width = '500px';
  663. view.element.style.height = '200px';
  664. view.items.add( focusable() );
  665. view.items.add( focusable() );
  666. view.items.add( focusable() );
  667. view.items.add( focusable() );
  668. view.items.add( focusable() );
  669. view.shouldGroupWhenFull = true;
  670. sinon.spy( view, 'updateGroupedItems' );
  671. expect( view.items ).to.have.length( 5 );
  672. expect( view.groupedItems ).to.be.null;
  673. setTimeout( () => {
  674. view.element.style.height = '500px';
  675. setTimeout( () => {
  676. sinon.assert.calledOnce( view.updateGroupedItems );
  677. done();
  678. }, 100 );
  679. }, 100 );
  680. } );
  681. } );
  682. } );
  683. function focusable() {
  684. const view = nonFocusable();
  685. view.label = 'focusable';
  686. view.focus = sinon.stub().callsFake( () => {
  687. view.element.focus();
  688. } );
  689. view.extendTemplate( {
  690. attributes: {
  691. tabindex: -1
  692. }
  693. } );
  694. return view;
  695. }
  696. function nonFocusable() {
  697. const view = new View();
  698. view.set( 'label', 'non-focusable' );
  699. const bind = view.bindTemplate;
  700. view.setTemplate( {
  701. tag: 'div',
  702. attributes: {
  703. style: {
  704. padding: '0',
  705. margin: '0',
  706. width: '100px',
  707. height: '100px',
  708. background: 'rgba(255,0,0,.3)'
  709. }
  710. },
  711. children: [
  712. {
  713. text: bind.to( 'label' )
  714. }
  715. ]
  716. } );
  717. return view;
  718. }
  719. function namedFactory( name ) {
  720. return locale => {
  721. const view = new View( locale );
  722. view.name = name;
  723. view.element = document.createElement( 'a' );
  724. return view;
  725. };
  726. }
  727. function getArrowKeyData( arrow ) {
  728. return {
  729. keyCode: keyCodes[ arrow ],
  730. preventDefault: sinon.spy(),
  731. stopPropagation: sinon.spy()
  732. };
  733. }