|
|
@@ -3,7 +3,7 @@
|
|
|
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
|
|
*/
|
|
|
|
|
|
-/* global document, Event, console */
|
|
|
+/* global document, Event, console, setTimeout */
|
|
|
|
|
|
import ToolbarView from '../../src/toolbar/toolbarview';
|
|
|
import ToolbarSeparatorView from '../../src/toolbar/toolbarseparatorview';
|
|
|
@@ -13,6 +13,7 @@ import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker';
|
|
|
import FocusCycler from '../../src/focuscycler';
|
|
|
import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
|
|
|
import ViewCollection from '../../src/viewcollection';
|
|
|
+import global from '@ckeditor/ckeditor5-utils/src/dom/global';
|
|
|
import View from '../../src/view';
|
|
|
import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
|
|
|
import { add as addTranslations, _clear as clearTranslations } from '@ckeditor/ckeditor5-utils/src/translation-service';
|
|
|
@@ -52,11 +53,25 @@ describe( 'ToolbarView', () => {
|
|
|
expect( view.locale ).to.equal( locale );
|
|
|
} );
|
|
|
|
|
|
- it( 'should set view#isVertical', () => {
|
|
|
- expect( view.isVertical ).to.be.false;
|
|
|
+ describe( '#options', () => {
|
|
|
+ it( 'should be an empty object if none were passed', () => {
|
|
|
+ expect( view.options ).to.deep.equal( {} );
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'should be an empty object if none were passed', () => {
|
|
|
+ const options = {
|
|
|
+ foo: 'bar'
|
|
|
+ };
|
|
|
+
|
|
|
+ const toolbar = new ToolbarView( locale, options );
|
|
|
+
|
|
|
+ expect( toolbar.options ).to.equal( options );
|
|
|
+
|
|
|
+ toolbar.destroy();
|
|
|
+ } );
|
|
|
} );
|
|
|
|
|
|
- it( 'should create view#children collection', () => {
|
|
|
+ it( 'should create view#items collection', () => {
|
|
|
expect( view.items ).to.be.instanceOf( ViewCollection );
|
|
|
} );
|
|
|
|
|
|
@@ -68,9 +83,21 @@ describe( 'ToolbarView', () => {
|
|
|
expect( view.keystrokes ).to.be.instanceOf( KeystrokeHandler );
|
|
|
} );
|
|
|
|
|
|
+ it( 'should create view#itemsView', () => {
|
|
|
+ expect( view.itemsView ).to.be.instanceOf( View );
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'should create view#children collection', () => {
|
|
|
+ expect( view.children ).to.be.instanceOf( ViewCollection );
|
|
|
+ } );
|
|
|
+
|
|
|
it( 'creates #_focusCycler instance', () => {
|
|
|
expect( view._focusCycler ).to.be.instanceOf( FocusCycler );
|
|
|
} );
|
|
|
+
|
|
|
+ it( 'creates #_behavior', () => {
|
|
|
+ expect( view._behavior ).to.be.an( 'object' );
|
|
|
+ } );
|
|
|
} );
|
|
|
|
|
|
describe( 'template', () => {
|
|
|
@@ -79,6 +106,12 @@ describe( 'ToolbarView', () => {
|
|
|
expect( view.element.classList.contains( 'ck-toolbar' ) ).to.true;
|
|
|
} );
|
|
|
|
|
|
+ it( 'should create #itemsView from template', () => {
|
|
|
+ expect( view.element.firstChild ).to.equal( view.itemsView.element );
|
|
|
+ expect( view.itemsView.element.classList.contains( 'ck' ) ).to.true;
|
|
|
+ expect( view.itemsView.element.classList.contains( 'ck-toolbar__items' ) ).to.true;
|
|
|
+ } );
|
|
|
+
|
|
|
describe( 'attributes', () => {
|
|
|
it( 'should be defined', () => {
|
|
|
expect( view.element.getAttribute( 'role' ) ).to.equal( 'toolbar' );
|
|
|
@@ -93,6 +126,8 @@ describe( 'ToolbarView', () => {
|
|
|
view.render();
|
|
|
|
|
|
expect( view.element.getAttribute( 'aria-label' ) ).to.equal( 'Custom label' );
|
|
|
+
|
|
|
+ view.destroy();
|
|
|
} );
|
|
|
|
|
|
it( 'should allow the aria-label to be translated', () => {
|
|
|
@@ -101,6 +136,8 @@ describe( 'ToolbarView', () => {
|
|
|
view.render();
|
|
|
|
|
|
expect( view.element.getAttribute( 'aria-label' ) ).to.equal( 'Pasek narzędzi edytora' );
|
|
|
+
|
|
|
+ view.destroy();
|
|
|
} );
|
|
|
} );
|
|
|
|
|
|
@@ -117,14 +154,6 @@ describe( 'ToolbarView', () => {
|
|
|
|
|
|
describe( 'element bindings', () => {
|
|
|
describe( 'class', () => {
|
|
|
- it( 'reacts on view#isVertical', () => {
|
|
|
- view.isVertical = false;
|
|
|
- expect( view.element.classList.contains( 'ck-toolbar_vertical' ) ).to.be.false;
|
|
|
-
|
|
|
- view.isVertical = true;
|
|
|
- expect( view.element.classList.contains( 'ck-toolbar_vertical' ) ).to.be.true;
|
|
|
- } );
|
|
|
-
|
|
|
it( 'reacts on view#class', () => {
|
|
|
view.class = 'foo';
|
|
|
expect( view.element.classList.contains( 'foo' ) ).to.be.true;
|
|
|
@@ -150,6 +179,7 @@ describe( 'ToolbarView', () => {
|
|
|
sinon.assert.notCalled( spyAdd );
|
|
|
|
|
|
view.render();
|
|
|
+
|
|
|
sinon.assert.calledTwice( spyAdd );
|
|
|
|
|
|
view.items.remove( 1 );
|
|
|
@@ -171,11 +201,7 @@ describe( 'ToolbarView', () => {
|
|
|
|
|
|
describe( 'activates keyboard navigation for the toolbar', () => {
|
|
|
it( 'so "arrowup" focuses previous focusable item', () => {
|
|
|
- const keyEvtData = {
|
|
|
- keyCode: keyCodes.arrowup,
|
|
|
- preventDefault: sinon.spy(),
|
|
|
- stopPropagation: sinon.spy()
|
|
|
- };
|
|
|
+ const keyEvtData = getArrowKeyData( 'arrowup' );
|
|
|
|
|
|
// No children to focus.
|
|
|
view.keystrokes.press( keyEvtData );
|
|
|
@@ -198,20 +224,15 @@ describe( 'ToolbarView', () => {
|
|
|
view.focusTracker.isFocused = true;
|
|
|
view.focusTracker.focusedElement = view.items.get( 4 ).element;
|
|
|
|
|
|
- const spy = sinon.spy( view.items.get( 2 ), 'focus' );
|
|
|
view.keystrokes.press( keyEvtData );
|
|
|
|
|
|
sinon.assert.calledThrice( keyEvtData.preventDefault );
|
|
|
sinon.assert.calledThrice( keyEvtData.stopPropagation );
|
|
|
- sinon.assert.calledOnce( spy );
|
|
|
+ sinon.assert.calledOnce( view.items.get( 2 ).focus );
|
|
|
} );
|
|
|
|
|
|
it( 'so "arrowleft" focuses previous focusable item', () => {
|
|
|
- const keyEvtData = {
|
|
|
- keyCode: keyCodes.arrowleft,
|
|
|
- preventDefault: sinon.spy(),
|
|
|
- stopPropagation: sinon.spy()
|
|
|
- };
|
|
|
+ const keyEvtData = getArrowKeyData( 'arrowleft' );
|
|
|
|
|
|
view.items.add( focusable() );
|
|
|
view.items.add( nonFocusable() );
|
|
|
@@ -221,18 +242,12 @@ describe( 'ToolbarView', () => {
|
|
|
view.focusTracker.isFocused = true;
|
|
|
view.focusTracker.focusedElement = view.items.get( 2 ).element;
|
|
|
|
|
|
- const spy = sinon.spy( view.items.get( 0 ), 'focus' );
|
|
|
-
|
|
|
view.keystrokes.press( keyEvtData );
|
|
|
- sinon.assert.calledOnce( spy );
|
|
|
+ sinon.assert.calledOnce( view.items.get( 0 ).focus );
|
|
|
} );
|
|
|
|
|
|
it( 'so "arrowdown" focuses next focusable item', () => {
|
|
|
- const keyEvtData = {
|
|
|
- keyCode: keyCodes.arrowdown,
|
|
|
- preventDefault: sinon.spy(),
|
|
|
- stopPropagation: sinon.spy()
|
|
|
- };
|
|
|
+ const keyEvtData = getArrowKeyData( 'arrowdown' );
|
|
|
|
|
|
// No children to focus.
|
|
|
view.keystrokes.press( keyEvtData );
|
|
|
@@ -255,20 +270,15 @@ describe( 'ToolbarView', () => {
|
|
|
view.focusTracker.isFocused = true;
|
|
|
view.focusTracker.focusedElement = view.items.get( 4 ).element;
|
|
|
|
|
|
- const spy = sinon.spy( view.items.get( 2 ), 'focus' );
|
|
|
view.keystrokes.press( keyEvtData );
|
|
|
|
|
|
sinon.assert.calledThrice( keyEvtData.preventDefault );
|
|
|
sinon.assert.calledThrice( keyEvtData.stopPropagation );
|
|
|
- sinon.assert.calledOnce( spy );
|
|
|
+ sinon.assert.calledOnce( view.items.get( 2 ).focus );
|
|
|
} );
|
|
|
|
|
|
it( 'so "arrowright" focuses next focusable item', () => {
|
|
|
- const keyEvtData = {
|
|
|
- keyCode: keyCodes.arrowright,
|
|
|
- preventDefault: sinon.spy(),
|
|
|
- stopPropagation: sinon.spy()
|
|
|
- };
|
|
|
+ const keyEvtData = getArrowKeyData( 'arrowright' );
|
|
|
|
|
|
view.items.add( focusable() );
|
|
|
view.items.add( nonFocusable() );
|
|
|
@@ -278,16 +288,42 @@ describe( 'ToolbarView', () => {
|
|
|
view.focusTracker.isFocused = true;
|
|
|
view.focusTracker.focusedElement = view.items.get( 0 ).element;
|
|
|
|
|
|
- const spy = sinon.spy( view.items.get( 2 ), 'focus' );
|
|
|
-
|
|
|
view.keystrokes.press( keyEvtData );
|
|
|
- sinon.assert.calledOnce( spy );
|
|
|
+ sinon.assert.calledOnce( view.items.get( 2 ).focus );
|
|
|
} );
|
|
|
} );
|
|
|
+
|
|
|
+ it( 'calls _behavior#render()', () => {
|
|
|
+ const view = new ToolbarView( locale );
|
|
|
+ sinon.spy( view._behavior, 'render' );
|
|
|
+
|
|
|
+ view.render();
|
|
|
+ sinon.assert.calledOnce( view._behavior.render );
|
|
|
+ sinon.assert.calledWithExactly( view._behavior.render, view );
|
|
|
+
|
|
|
+ view.destroy();
|
|
|
+ } );
|
|
|
+ } );
|
|
|
+
|
|
|
+ describe( 'destroy()', () => {
|
|
|
+ it( 'destroys the feature', () => {
|
|
|
+ sinon.spy( view._behavior, 'destroy' );
|
|
|
+
|
|
|
+ view.destroy();
|
|
|
+
|
|
|
+ sinon.assert.calledOnce( view._behavior.destroy );
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'calls _behavior#destroy()', () => {
|
|
|
+ sinon.spy( view._behavior, 'destroy' );
|
|
|
+
|
|
|
+ view.destroy();
|
|
|
+ sinon.assert.calledOnce( view._behavior.destroy );
|
|
|
+ } );
|
|
|
} );
|
|
|
|
|
|
describe( 'focus()', () => {
|
|
|
- it( 'focuses the first focusable item in DOM', () => {
|
|
|
+ it( 'focuses the first focusable of #items in DOM', () => {
|
|
|
// No children to focus.
|
|
|
view.focus();
|
|
|
|
|
|
@@ -296,15 +332,14 @@ describe( 'ToolbarView', () => {
|
|
|
view.items.add( focusable() );
|
|
|
view.items.add( nonFocusable() );
|
|
|
|
|
|
- const spy = sinon.spy( view.items.get( 1 ), 'focus' );
|
|
|
view.focus();
|
|
|
|
|
|
- sinon.assert.calledOnce( spy );
|
|
|
+ sinon.assert.calledOnce( view.items.get( 1 ).focus );
|
|
|
} );
|
|
|
} );
|
|
|
|
|
|
describe( 'focusLast()', () => {
|
|
|
- it( 'focuses the last focusable item in DOM', () => {
|
|
|
+ it( 'focuses the last focusable of #items in DOM', () => {
|
|
|
// No children to focus.
|
|
|
view.focusLast();
|
|
|
|
|
|
@@ -315,10 +350,9 @@ describe( 'ToolbarView', () => {
|
|
|
view.items.add( focusable() );
|
|
|
view.items.add( nonFocusable() );
|
|
|
|
|
|
- const spy = sinon.spy( view.items.get( 3 ), 'focus' );
|
|
|
view.focusLast();
|
|
|
|
|
|
- sinon.assert.calledOnce( spy );
|
|
|
+ sinon.assert.calledOnce( view.items.get( 3 ).focus );
|
|
|
} );
|
|
|
} );
|
|
|
|
|
|
@@ -361,19 +395,516 @@ describe( 'ToolbarView', () => {
|
|
|
);
|
|
|
} );
|
|
|
} );
|
|
|
+
|
|
|
+ describe( 'toolbar with static items', () => {
|
|
|
+ describe( 'constructor()', () => {
|
|
|
+ it( 'should set view#isVertical', () => {
|
|
|
+ expect( view.isVertical ).to.be.false;
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'binds itemsView#children to #items', () => {
|
|
|
+ const itemA = focusable();
|
|
|
+ const itemB = focusable();
|
|
|
+ const itemC = focusable();
|
|
|
+
|
|
|
+ view.items.add( itemA );
|
|
|
+ view.items.add( itemB );
|
|
|
+ view.items.add( itemC );
|
|
|
+
|
|
|
+ expect( view.itemsView.children.map( i => i ) ).to.have.ordered.members( [ itemA, itemB, itemC ] );
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'binds #focusables to #items', () => {
|
|
|
+ const itemA = focusable();
|
|
|
+ const itemB = focusable();
|
|
|
+ const itemC = focusable();
|
|
|
+
|
|
|
+ view.items.add( itemA );
|
|
|
+ view.items.add( itemB );
|
|
|
+ view.items.add( itemC );
|
|
|
+
|
|
|
+ expect( view.focusables.map( i => i ) ).to.have.ordered.members( [ itemA, itemB, itemC ] );
|
|
|
+ } );
|
|
|
+ } );
|
|
|
+
|
|
|
+ describe( 'element bindings', () => {
|
|
|
+ describe( 'class', () => {
|
|
|
+ it( 'reacts on view#isVertical', () => {
|
|
|
+ view.isVertical = false;
|
|
|
+ expect( view.element.classList.contains( 'ck-toolbar_vertical' ) ).to.be.false;
|
|
|
+
|
|
|
+ view.isVertical = true;
|
|
|
+ expect( view.element.classList.contains( 'ck-toolbar_vertical' ) ).to.be.true;
|
|
|
+ } );
|
|
|
+ } );
|
|
|
+ } );
|
|
|
+ } );
|
|
|
+
|
|
|
+ describe( 'toolbar with a dynamic item grouping', () => {
|
|
|
+ let locale, view, groupedItems, ungroupedItems, groupedItemsDropdown;
|
|
|
+
|
|
|
+ beforeEach( () => {
|
|
|
+ locale = new Locale();
|
|
|
+ view = new ToolbarView( locale, {
|
|
|
+ shouldGroupWhenFull: true
|
|
|
+ } );
|
|
|
+ view.render();
|
|
|
+ view.element.style.width = '200px';
|
|
|
+ document.body.appendChild( view.element );
|
|
|
+
|
|
|
+ groupedItems = view._behavior.groupedItems;
|
|
|
+ ungroupedItems = view._behavior.ungroupedItems;
|
|
|
+ groupedItemsDropdown = view._behavior.groupedItemsDropdown;
|
|
|
+ } );
|
|
|
+
|
|
|
+ afterEach( () => {
|
|
|
+ sinon.restore();
|
|
|
+ view.element.remove();
|
|
|
+ view.destroy();
|
|
|
+ } );
|
|
|
+
|
|
|
+ describe( 'constructor()', () => {
|
|
|
+ it( 'extends the template with the CSS class', () => {
|
|
|
+ expect( view.element.classList.contains( 'ck-toolbar_grouping' ) ).to.be.true;
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'updates the UI as new #items are added', () => {
|
|
|
+ sinon.spy( view._behavior, '_updateGrouping' );
|
|
|
+
|
|
|
+ const itemA = focusable();
|
|
|
+ const itemB = focusable();
|
|
|
+ const itemC = focusable();
|
|
|
+ const itemD = focusable();
|
|
|
+
|
|
|
+ view.element.style.width = '200px';
|
|
|
+
|
|
|
+ view.items.add( itemA );
|
|
|
+ view.items.add( itemB );
|
|
|
+
|
|
|
+ sinon.assert.calledTwice( view._behavior._updateGrouping );
|
|
|
+
|
|
|
+ expect( ungroupedItems ).to.have.length( 2 );
|
|
|
+ expect( groupedItems ).to.have.length( 0 );
|
|
|
+
|
|
|
+ view.items.add( itemC );
|
|
|
+
|
|
|
+ // The dropdown took some extra space.
|
|
|
+ expect( ungroupedItems ).to.have.length( 1 );
|
|
|
+ expect( groupedItems ).to.have.length( 2 );
|
|
|
+
|
|
|
+ view.items.add( itemD, 2 );
|
|
|
+
|
|
|
+ expect( ungroupedItems ).to.have.length( 1 );
|
|
|
+ expect( groupedItems ).to.have.length( 3 );
|
|
|
+
|
|
|
+ expect( ungroupedItems.map( i => i ) ).to.have.ordered.members( [ itemA ] );
|
|
|
+ expect( groupedItems.map( i => i ) ).to.have.ordered.members( [ itemB, itemD, itemC ] );
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'updates the UI as #items are removed', () => {
|
|
|
+ const itemA = focusable();
|
|
|
+ const itemB = focusable();
|
|
|
+ const itemC = focusable();
|
|
|
+ const itemD = focusable();
|
|
|
+
|
|
|
+ view.element.style.width = '200px';
|
|
|
+
|
|
|
+ view.items.add( itemA );
|
|
|
+ view.items.add( itemB );
|
|
|
+ view.items.add( itemC );
|
|
|
+ view.items.add( itemD );
|
|
|
+
|
|
|
+ sinon.spy( view._behavior, '_updateGrouping' );
|
|
|
+ view.items.remove( 2 );
|
|
|
+
|
|
|
+ expect( ungroupedItems.map( i => i ) ).to.have.ordered.members( [ itemA ] );
|
|
|
+ expect( groupedItems.map( i => i ) ).to.have.ordered.members( [ itemB, itemD ] );
|
|
|
+
|
|
|
+ sinon.assert.calledOnce( view._behavior._updateGrouping );
|
|
|
+
|
|
|
+ view.items.remove( 0 );
|
|
|
+ sinon.assert.calledTwice( view._behavior._updateGrouping );
|
|
|
+
|
|
|
+ expect( ungroupedItems.map( i => i ) ).to.have.ordered.members( [ itemB, itemD ] );
|
|
|
+ } );
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'groups items that overflow into the dropdown', () => {
|
|
|
+ const itemA = focusable();
|
|
|
+ const itemB = focusable();
|
|
|
+ const itemC = focusable();
|
|
|
+ const itemD = focusable();
|
|
|
+
|
|
|
+ view.items.add( itemA );
|
|
|
+ view.items.add( itemB );
|
|
|
+ view.items.add( itemC );
|
|
|
+ view.items.add( itemD );
|
|
|
+
|
|
|
+ expect( ungroupedItems.map( i => i ) ).to.have.ordered.members( [ itemA ] );
|
|
|
+ expect( groupedItems.map( i => i ) ).to.have.ordered.members( [ itemB, itemC, itemD ] );
|
|
|
+ expect( view.children ).to.have.length( 3 );
|
|
|
+ expect( view.children.get( 0 ) ).to.equal( view.itemsView );
|
|
|
+ expect( view.children.get( 1 ) ).to.be.instanceOf( ToolbarSeparatorView );
|
|
|
+ expect( view.children.get( 2 ) ).to.equal( groupedItemsDropdown );
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'ungroups items if there is enough space to display them (all)', () => {
|
|
|
+ const itemA = focusable();
|
|
|
+ const itemB = focusable();
|
|
|
+ const itemC = focusable();
|
|
|
+ const itemD = focusable();
|
|
|
+
|
|
|
+ view.items.add( itemA );
|
|
|
+ view.items.add( itemB );
|
|
|
+ view.items.add( itemC );
|
|
|
+ view.items.add( itemD );
|
|
|
+
|
|
|
+ expect( ungroupedItems.map( i => i ) ).to.have.ordered.members( [ itemA ] );
|
|
|
+ expect( groupedItems.map( i => i ) ).to.have.ordered.members( [ itemB, itemC, itemD ] );
|
|
|
+
|
|
|
+ view.element.style.width = '350px';
|
|
|
+
|
|
|
+ // Some grouped items cannot be ungrouped because there is not enough space and they will
|
|
|
+ // land back in #_behavior.groupedItems after an attempt was made.
|
|
|
+ view._behavior._updateGrouping();
|
|
|
+ expect( ungroupedItems.map( i => i ) ).to.have.ordered.members( [ itemA, itemB, itemC ] );
|
|
|
+ expect( groupedItems.map( i => i ) ).to.have.ordered.members( [ itemD ] );
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'ungroups items if there is enough space to display them (some)', () => {
|
|
|
+ const itemA = focusable();
|
|
|
+ const itemB = focusable();
|
|
|
+ const itemC = focusable();
|
|
|
+
|
|
|
+ view.items.add( itemA );
|
|
|
+ view.items.add( itemB );
|
|
|
+ view.items.add( itemC );
|
|
|
+
|
|
|
+ expect( ungroupedItems.map( i => i ) ).to.have.ordered.members( [ itemA ] );
|
|
|
+ expect( groupedItems.map( i => i ) ).to.have.ordered.members( [ itemB, itemC ] );
|
|
|
+
|
|
|
+ view.element.style.width = '350px';
|
|
|
+
|
|
|
+ // All grouped items will be ungrouped because they fit just alright in the main space.
|
|
|
+ view._behavior._updateGrouping();
|
|
|
+ expect( ungroupedItems.map( i => i ) ).to.have.ordered.members( [ itemA, itemB, itemC ] );
|
|
|
+ expect( groupedItems ).to.have.length( 0 );
|
|
|
+ expect( view.children ).to.have.length( 1 );
|
|
|
+ expect( view.children.get( 0 ) ).to.equal( view.itemsView );
|
|
|
+ } );
|
|
|
+
|
|
|
+ describe( 'render()', () => {
|
|
|
+ it( 'starts observing toolbar resize immediatelly after render', () => {
|
|
|
+ function FakeResizeObserver( callback ) {
|
|
|
+ this.callback = callback;
|
|
|
+ }
|
|
|
+
|
|
|
+ FakeResizeObserver.prototype.observe = sinon.spy();
|
|
|
+ FakeResizeObserver.prototype.disconnect = sinon.spy();
|
|
|
+
|
|
|
+ testUtils.sinon.stub( global.window, 'ResizeObserver' ).value( FakeResizeObserver );
|
|
|
+
|
|
|
+ const view = new ToolbarView( locale, {
|
|
|
+ shouldGroupWhenFull: true
|
|
|
+ } );
|
|
|
+
|
|
|
+ view.render();
|
|
|
+
|
|
|
+ sinon.assert.calledOnce( view._behavior.resizeObserver.observe );
|
|
|
+ sinon.assert.calledWithExactly( view._behavior.resizeObserver.observe, view.element );
|
|
|
+
|
|
|
+ view.destroy();
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'updates the UI when the toolbar is being resized (expanding)', done => {
|
|
|
+ view.element.style.width = '200px';
|
|
|
+
|
|
|
+ view.items.add( focusable() );
|
|
|
+ view.items.add( focusable() );
|
|
|
+ view.items.add( focusable() );
|
|
|
+ view.items.add( focusable() );
|
|
|
+ view.items.add( focusable() );
|
|
|
+
|
|
|
+ expect( ungroupedItems ).to.have.length( 1 );
|
|
|
+ expect( groupedItems ).to.have.length( 4 );
|
|
|
+
|
|
|
+ view.element.style.width = '500px';
|
|
|
+
|
|
|
+ setTimeout( () => {
|
|
|
+ expect( ungroupedItems ).to.have.length( 5 );
|
|
|
+ expect( groupedItems ).to.have.length( 0 );
|
|
|
+
|
|
|
+ done();
|
|
|
+ }, 100 );
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'updates the UI when the toolbar is being resized (narrowing)', done => {
|
|
|
+ view.element.style.width = '500px';
|
|
|
+
|
|
|
+ view.items.add( focusable() );
|
|
|
+ view.items.add( focusable() );
|
|
|
+ view.items.add( focusable() );
|
|
|
+ view.items.add( focusable() );
|
|
|
+ view.items.add( focusable() );
|
|
|
+
|
|
|
+ expect( ungroupedItems ).to.have.length( 5 );
|
|
|
+ expect( groupedItems ).to.have.length( 0 );
|
|
|
+
|
|
|
+ view.element.style.width = '200px';
|
|
|
+
|
|
|
+ setTimeout( () => {
|
|
|
+ expect( ungroupedItems ).to.have.length( 1 );
|
|
|
+ expect( groupedItems ).to.have.length( 4 );
|
|
|
+
|
|
|
+ done();
|
|
|
+ }, 100 );
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'does not react to changes in height', done => {
|
|
|
+ view.element.style.width = '500px';
|
|
|
+ view.element.style.height = '200px';
|
|
|
+
|
|
|
+ view.items.add( focusable() );
|
|
|
+ view.items.add( focusable() );
|
|
|
+ view.items.add( focusable() );
|
|
|
+ view.items.add( focusable() );
|
|
|
+ view.items.add( focusable() );
|
|
|
+
|
|
|
+ sinon.spy( view._behavior, '_updateGrouping' );
|
|
|
+ view.element.style.width = '500px';
|
|
|
+
|
|
|
+ setTimeout( () => {
|
|
|
+ sinon.assert.calledOnce( view._behavior._updateGrouping );
|
|
|
+ view.element.style.height = '500px';
|
|
|
+
|
|
|
+ setTimeout( () => {
|
|
|
+ sinon.assert.calledOnce( view._behavior._updateGrouping );
|
|
|
+ done();
|
|
|
+ }, 100 );
|
|
|
+ }, 100 );
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'updates the state of grouped items upon resize', () => {
|
|
|
+ function FakeResizeObserver( callback ) {
|
|
|
+ this.callback = callback;
|
|
|
+ }
|
|
|
+
|
|
|
+ FakeResizeObserver.prototype.observe = sinon.spy();
|
|
|
+ FakeResizeObserver.prototype.disconnect = sinon.spy();
|
|
|
+
|
|
|
+ testUtils.sinon.stub( global.window, 'ResizeObserver' ).value( FakeResizeObserver );
|
|
|
+
|
|
|
+ const view = new ToolbarView( locale, {
|
|
|
+ shouldGroupWhenFull: true
|
|
|
+ } );
|
|
|
+
|
|
|
+ testUtils.sinon.spy( view._behavior, '_updateGrouping' );
|
|
|
+
|
|
|
+ view.render();
|
|
|
+
|
|
|
+ view._behavior.resizeObserver.callback( [
|
|
|
+ { contentRect: { width: 42 } }
|
|
|
+ ] );
|
|
|
+
|
|
|
+ sinon.assert.calledTwice( view._behavior._updateGrouping );
|
|
|
+
|
|
|
+ view.destroy();
|
|
|
+ } );
|
|
|
+ } );
|
|
|
+
|
|
|
+ describe( 'destroy()', () => {
|
|
|
+ it( 'destroys the #groupedItemsDropdown', () => {
|
|
|
+ view.element.style.width = '200px';
|
|
|
+
|
|
|
+ const itemA = focusable();
|
|
|
+ const itemB = focusable();
|
|
|
+ const itemC = focusable();
|
|
|
+ const itemD = focusable();
|
|
|
+
|
|
|
+ view.items.add( itemA );
|
|
|
+ view.items.add( itemB );
|
|
|
+ view.items.add( itemC );
|
|
|
+ view.items.add( itemD );
|
|
|
+
|
|
|
+ sinon.spy( groupedItemsDropdown, 'destroy' );
|
|
|
+
|
|
|
+ view.element.style.width = '500px';
|
|
|
+
|
|
|
+ // The dropdown hides; it does not belong to any collection but it still exist.
|
|
|
+ view._behavior._updateGrouping();
|
|
|
+
|
|
|
+ view.destroy();
|
|
|
+ sinon.assert.calledOnce( groupedItemsDropdown.destroy );
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'disconnects the #resizeObserver', () => {
|
|
|
+ view.element.style.width = '200px';
|
|
|
+
|
|
|
+ const itemA = focusable();
|
|
|
+ const itemB = focusable();
|
|
|
+ const itemC = focusable();
|
|
|
+ const itemD = focusable();
|
|
|
+
|
|
|
+ view.items.add( itemA );
|
|
|
+ view.items.add( itemB );
|
|
|
+ view.items.add( itemC );
|
|
|
+ view.items.add( itemD );
|
|
|
+
|
|
|
+ sinon.spy( view._behavior.resizeObserver, 'disconnect' );
|
|
|
+
|
|
|
+ view.destroy();
|
|
|
+ sinon.assert.calledOnce( view._behavior.resizeObserver.disconnect );
|
|
|
+ } );
|
|
|
+ } );
|
|
|
+
|
|
|
+ describe( 'dropdown with grouped items', () => {
|
|
|
+ it( 'has proper DOM structure', () => {
|
|
|
+ view.items.add( focusable() );
|
|
|
+ view.items.add( focusable() );
|
|
|
+ view.items.add( focusable() );
|
|
|
+ view.items.add( focusable() );
|
|
|
+
|
|
|
+ expect( view.children.has( groupedItemsDropdown ) ).to.be.true;
|
|
|
+ expect( groupedItemsDropdown.element.classList.contains( 'ck-toolbar__grouped-dropdown' ) );
|
|
|
+ expect( groupedItemsDropdown.buttonView.label ).to.equal( 'Show more items' );
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'shares its toolbarView#items with grouped items', () => {
|
|
|
+ view.items.add( focusable() );
|
|
|
+ view.items.add( focusable() );
|
|
|
+ view.items.add( focusable() );
|
|
|
+ view.items.add( focusable() );
|
|
|
+
|
|
|
+ expect( groupedItemsDropdown.toolbarView.items.map( i => i ) )
|
|
|
+ .to.have.ordered.members( groupedItems.map( i => i ) );
|
|
|
+ } );
|
|
|
+ } );
|
|
|
+
|
|
|
+ describe( 'item overflow checking logic', () => {
|
|
|
+ it( 'considers the right padding of the toolbar (LTR UI)', () => {
|
|
|
+ view.class = 'ck-reset_all';
|
|
|
+ view.element.style.width = '210px';
|
|
|
+ view.element.style.paddingLeft = '0px';
|
|
|
+ view.element.style.paddingRight = '20px';
|
|
|
+
|
|
|
+ view.items.add( focusable() );
|
|
|
+ view.items.add( focusable() );
|
|
|
+
|
|
|
+ expect( view._behavior.groupedItems ).to.have.length( 1 );
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'considers the left padding of the toolbar (RTL UI)', () => {
|
|
|
+ const locale = new Locale( { uiLanguage: 'ar' } );
|
|
|
+ const view = new ToolbarView( locale, {
|
|
|
+ shouldGroupWhenFull: true
|
|
|
+ } );
|
|
|
+
|
|
|
+ view.extendTemplate( {
|
|
|
+ attributes: {
|
|
|
+ dir: locale.uiLanguageDirection
|
|
|
+ }
|
|
|
+ } );
|
|
|
+
|
|
|
+ view.render();
|
|
|
+ document.body.appendChild( view.element );
|
|
|
+
|
|
|
+ view.class = 'ck-reset_all';
|
|
|
+ view.element.style.width = '210px';
|
|
|
+ view.element.style.paddingLeft = '20px';
|
|
|
+ view.element.style.paddingRight = '0px';
|
|
|
+
|
|
|
+ view.items.add( focusable() );
|
|
|
+ view.items.add( focusable() );
|
|
|
+
|
|
|
+ expect( view._behavior.groupedItems ).to.have.length( 1 );
|
|
|
+
|
|
|
+ view.destroy();
|
|
|
+ view.element.remove();
|
|
|
+ } );
|
|
|
+ } );
|
|
|
+
|
|
|
+ describe( 'focus management', () => {
|
|
|
+ it( '#focus() focuses the dropdown when it is the only focusable', () => {
|
|
|
+ sinon.spy( groupedItemsDropdown, 'focus' );
|
|
|
+ view.element.style.width = '10px';
|
|
|
+
|
|
|
+ const itemA = focusable();
|
|
|
+ const itemB = focusable();
|
|
|
+
|
|
|
+ view.items.add( itemA );
|
|
|
+ view.items.add( itemB );
|
|
|
+
|
|
|
+ expect( view.focusables.map( i => i ) ).to.have.ordered.members( [ groupedItemsDropdown ] );
|
|
|
+
|
|
|
+ view.focus();
|
|
|
+ sinon.assert.calledOnce( groupedItemsDropdown.focus );
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( '#focusLast() focuses the dropdown when present', () => {
|
|
|
+ sinon.spy( groupedItemsDropdown, 'focus' );
|
|
|
+ view.element.style.width = '200px';
|
|
|
+
|
|
|
+ const itemA = focusable();
|
|
|
+ const itemB = focusable();
|
|
|
+ const itemC = focusable();
|
|
|
+
|
|
|
+ view.items.add( itemA );
|
|
|
+ view.items.add( itemB );
|
|
|
+ view.items.add( itemC );
|
|
|
+
|
|
|
+ expect( view.focusables.map( i => i ) ).to.have.ordered.members( [ itemA, groupedItemsDropdown ] );
|
|
|
+
|
|
|
+ view.focusLast();
|
|
|
+
|
|
|
+ sinon.assert.calledOnce( groupedItemsDropdown.focus );
|
|
|
+
|
|
|
+ view.element.remove();
|
|
|
+ } );
|
|
|
+ } );
|
|
|
+ } );
|
|
|
} );
|
|
|
|
|
|
function focusable() {
|
|
|
const view = nonFocusable();
|
|
|
|
|
|
- view.focus = () => {};
|
|
|
+ view.label = 'focusable';
|
|
|
+ view.focus = sinon.stub().callsFake( () => {
|
|
|
+ view.element.focus();
|
|
|
+ } );
|
|
|
+
|
|
|
+ view.extendTemplate( {
|
|
|
+ attributes: {
|
|
|
+ tabindex: -1
|
|
|
+ }
|
|
|
+ } );
|
|
|
|
|
|
return view;
|
|
|
}
|
|
|
|
|
|
function nonFocusable() {
|
|
|
const view = new View();
|
|
|
- view.element = document.createElement( 'li' );
|
|
|
+
|
|
|
+ view.set( 'label', 'non-focusable' );
|
|
|
+
|
|
|
+ const bind = view.bindTemplate;
|
|
|
+
|
|
|
+ view.setTemplate( {
|
|
|
+ tag: 'div',
|
|
|
+ attributes: {
|
|
|
+ style: {
|
|
|
+ padding: '0',
|
|
|
+ margin: '0',
|
|
|
+ width: '100px',
|
|
|
+ height: '100px',
|
|
|
+ background: 'rgba(255,0,0,.3)'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ children: [
|
|
|
+ {
|
|
|
+ text: bind.to( 'label' )
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ } );
|
|
|
|
|
|
return view;
|
|
|
}
|
|
|
@@ -388,3 +919,11 @@ function namedFactory( name ) {
|
|
|
return view;
|
|
|
};
|
|
|
}
|
|
|
+
|
|
|
+function getArrowKeyData( arrow ) {
|
|
|
+ return {
|
|
|
+ keyCode: keyCodes[ arrow ],
|
|
|
+ preventDefault: sinon.spy(),
|
|
|
+ stopPropagation: sinon.spy()
|
|
|
+ };
|
|
|
+}
|