toolbarview.js 26 KB

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