浏览代码

Code refactoring and tests in the ToolbarView class.

Aleksander Nowodzinski 6 年之前
父节点
当前提交
5ec858f207
共有 2 个文件被更改,包括 229 次插入59 次删除
  1. 11 5
      packages/ckeditor5-ui/src/toolbar/toolbarview.js
  2. 218 54
      packages/ckeditor5-ui/tests/toolbar/toolbarview.js

+ 11 - 5
packages/ckeditor5-ui/src/toolbar/toolbarview.js

@@ -173,7 +173,7 @@ export default class ToolbarView extends View {
 		 * **Note:** Used only when {@link #shouldGroupWhenFull} is `true`.
 		 *
 		 * @readonly
-		 * @protected
+		 * @private
 		 * @member {Boolean}
 		 */
 		this._updateGroupedItemsLock = false;
@@ -199,7 +199,7 @@ export default class ToolbarView extends View {
 		 * **Note:** Created dynamically only when {@link #shouldGroupWhenFull} is `true`.
 		 *
 		 * @readonly
-		 * @protected
+		 * @private
 		 * @member {module:utils/dom/getresizeobserver~ResizeObserver}
 		 */
 		this._resizeObserver = null;
@@ -335,7 +335,13 @@ export default class ToolbarView extends View {
 	 * Focuses the last focusable in {@link #items}.
 	 */
 	focusLast() {
-		this._componentsFocusCycler.focusLast();
+		const last = this._componentsFocusCycler.last;
+
+		if ( last === this.itemsView ) {
+			this.itemsView._focusCycler.focusLast();
+		} else {
+			this._componentsFocusCycler.focusLast();
+		}
 	}
 
 	/**
@@ -541,7 +547,7 @@ export default class ToolbarView extends View {
 	 */
 	_focusNext( keyEvtData, cancel ) {
 		if ( this.itemsView.focusTracker.isFocused ) {
-			if ( this._itemsFocusCycler.next === this._itemsFocusCycler.first ) {
+			if ( !this._itemsFocusCycler.next || this._itemsFocusCycler.next === this._itemsFocusCycler.first ) {
 				this._componentsFocusCycler.focusNext();
 			} else {
 				this._itemsFocusCycler.focusNext();
@@ -575,7 +581,7 @@ export default class ToolbarView extends View {
 	 */
 	_focusPrevious( keyEvtData, cancel ) {
 		if ( this.itemsView.focusTracker.isFocused ) {
-			if ( this._itemsFocusCycler.previous === this._itemsFocusCycler.last ) {
+			if ( !this._itemsFocusCycler.next || this._itemsFocusCycler.previous === this._itemsFocusCycler.last ) {
 				const hasGroupedItemsDropdown = this.groupedItemsDropdown && this._components.has( this.groupedItemsDropdown );
 
 				if ( hasGroupedItemsDropdown ) {

+ 218 - 54
packages/ckeditor5-ui/tests/toolbar/toolbarview.js

@@ -56,10 +56,14 @@ describe( 'ToolbarView', () => {
 			expect( view.isVertical ).to.be.false;
 		} );
 
-		it( 'should create view#children collection', () => {
+		it( 'should create view#items collection', () => {
 			expect( view.items ).to.be.instanceOf( ViewCollection );
 		} );
 
+		it( 'should not create view#groupedItems collection', () => {
+			expect( view.groupedItems ).to.be.null;
+		} );
+
 		it( 'creates #focusTracker instance', () => {
 			expect( view.focusTracker ).to.be.instanceOf( FocusTracker );
 		} );
@@ -68,8 +72,28 @@ describe( 'ToolbarView', () => {
 			expect( view.keystrokes ).to.be.instanceOf( KeystrokeHandler );
 		} );
 
-		it( 'creates #_focusCycler instance', () => {
-			expect( view._focusCycler ).to.be.instanceOf( FocusCycler );
+		it( 'should create view#itemsView', () => {
+			expect( view.itemsView ).to.be.instanceOf( View );
+		} );
+
+		it( 'should not create view#groupedItemsDropdown', () => {
+			expect( view.groupedItemsDropdown ).to.be.null;
+		} );
+
+		it( 'should set view#shouldGroupWhenFull', () => {
+			expect( view.shouldGroupWhenFull ).to.be.false;
+		} );
+
+		it( 'should create view#_components collection', () => {
+			expect( view._components ).to.be.instanceOf( ViewCollection );
+		} );
+
+		it( 'creates #_itemsFocusCycler instance', () => {
+			expect( view._itemsFocusCycler ).to.be.instanceOf( FocusCycler );
+		} );
+
+		it( 'creates #_componentsFocusCycler instance', () => {
+			expect( view._componentsFocusCycler ).to.be.instanceOf( FocusCycler );
 		} );
 	} );
 
@@ -79,6 +103,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' );
@@ -136,23 +166,33 @@ describe( 'ToolbarView', () => {
 				expect( view.element.classList.contains( 'foo' ) ).to.be.false;
 				expect( view.element.classList.contains( 'bar' ) ).to.be.false;
 			} );
+
+			it( 'reacts on view#shouldGroupWhenFull', () => {
+				view.shouldGroupWhenFull = false;
+				expect( view.element.classList.contains( 'ck-toolbar_grouping' ) ).to.be.false;
+
+				view.shouldGroupWhenFull = true;
+				expect( view.element.classList.contains( 'ck-toolbar_grouping' ) ).to.be.true;
+			} );
 		} );
 	} );
 
 	describe( 'render()', () => {
-		it( 'registers #items in #focusTracker', () => {
+		it( 'registers #_components in #focusTracker', () => {
 			const view = new ToolbarView( locale );
 			const spyAdd = sinon.spy( view.focusTracker, 'add' );
 			const spyRemove = sinon.spy( view.focusTracker, 'remove' );
 
-			view.items.add( focusable() );
-			view.items.add( focusable() );
+			view._components.add( focusable() );
+			view._components.add( focusable() );
 			sinon.assert.notCalled( spyAdd );
 
 			view.render();
-			sinon.assert.calledTwice( spyAdd );
 
-			view.items.remove( 1 );
+			// First call is for the #itemsView.
+			sinon.assert.calledThrice( spyAdd );
+
+			view._components.remove( 1 );
 			sinon.assert.calledOnce( spyRemove );
 
 			view.destroy();
@@ -171,11 +211,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 );
@@ -195,44 +231,33 @@ describe( 'ToolbarView', () => {
 				view.items.add( focusable() );
 
 				// Mock the last item is focused.
-				view.focusTracker.isFocused = true;
-				view.focusTracker.focusedElement = view.items.get( 4 ).element;
+				view.itemsView.focusTracker.isFocused = true;
+				view.itemsView.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() );
 				view.items.add( focusable() );
 
 				// Mock the last item is focused.
-				view.focusTracker.isFocused = true;
-				view.focusTracker.focusedElement = view.items.get( 2 ).element;
-
-				const spy = sinon.spy( view.items.get( 0 ), 'focus' );
+				view.itemsView.focusTracker.isFocused = true;
+				view.itemsView.focusTracker.focusedElement = view.items.get( 2 ).element;
 
 				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 );
@@ -252,42 +277,125 @@ describe( 'ToolbarView', () => {
 				view.items.add( focusable() );
 
 				// Mock the last item is focused.
-				view.focusTracker.isFocused = true;
-				view.focusTracker.focusedElement = view.items.get( 4 ).element;
+				view.itemsView.focusTracker.isFocused = true;
+				view.itemsView.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() );
 				view.items.add( focusable() );
 
 				// Mock the last item is focused.
-				view.focusTracker.isFocused = true;
-				view.focusTracker.focusedElement = view.items.get( 0 ).element;
-
-				const spy = sinon.spy( view.items.get( 2 ), 'focus' );
+				view.itemsView.focusTracker.isFocused = true;
+				view.itemsView.focusTracker.focusedElement = view.items.get( 0 ).element;
 
 				view.keystrokes.press( keyEvtData );
-				sinon.assert.calledOnce( spy );
+				sinon.assert.calledOnce( view.items.get( 2 ).focus );
+			} );
+
+			describe( 'when #shouldGroupWhenFull is true', () => {
+				beforeEach( () => {
+					document.body.appendChild( view.element );
+					view.element.style.width = '200px';
+					view.shouldGroupWhenFull = true;
+				} );
+
+				afterEach( () => {
+					view.element.remove();
+				} );
+
+				it( 'navigates from #items to the #groupedItemsDropdown (forwards)', () => {
+					const keyEvtData = getArrowKeyData( 'arrowright' );
+
+					view.items.add( focusable() );
+					view.items.add( nonFocusable() );
+					view.items.add( focusable() );
+
+					view.updateGroupedItems();
+					sinon.spy( view.groupedItemsDropdown, 'focus' );
+
+					view.focusTracker.isFocused = true;
+					view.focusTracker.focusedElement = view.itemsView.element;
+					view.itemsView.focusTracker.isFocused = true;
+					view.itemsView.focusTracker.focusedElement = view.items.get( 0 ).element;
+
+					view.keystrokes.press( keyEvtData );
+
+					sinon.assert.calledOnce( view.groupedItemsDropdown.focus );
+				} );
+
+				it( 'navigates from the #groupedItemsDropdown to #items (forwards)', () => {
+					const keyEvtData = getArrowKeyData( 'arrowright' );
+
+					view.items.add( focusable() );
+					view.items.add( nonFocusable() );
+					view.items.add( focusable() );
+
+					view.updateGroupedItems();
+
+					view.focusTracker.isFocused = true;
+					view.focusTracker.focusedElement = view.groupedItemsDropdown.element;
+					view.itemsView.focusTracker.isFocused = false;
+					view.itemsView.focusTracker.focusedElement = null;
+
+					view.keystrokes.press( keyEvtData );
+
+					sinon.assert.calledOnce( view.items.get( 0 ).focus );
+				} );
+
+				it( 'navigates from #items to the #groupedItemsDropdown (backwards)', () => {
+					const keyEvtData = getArrowKeyData( 'arrowleft' );
+
+					view.items.add( focusable() );
+					view.items.add( nonFocusable() );
+					view.items.add( focusable() );
+
+					view.updateGroupedItems();
+					sinon.spy( view.groupedItemsDropdown, 'focus' );
+
+					view.focusTracker.isFocused = true;
+					view.focusTracker.focusedElement = view.itemsView.element;
+					view.itemsView.focusTracker.isFocused = true;
+					view.itemsView.focusTracker.focusedElement = view.items.get( 0 ).element;
+
+					view.keystrokes.press( keyEvtData );
+
+					sinon.assert.calledOnce( view.groupedItemsDropdown.focus );
+				} );
+
+				it( 'navigates from the #groupedItemsDropdown to #items (backwards)', () => {
+					const keyEvtData = getArrowKeyData( 'arrowleft' );
+
+					view.items.add( focusable() );
+					view.items.add( nonFocusable() );
+					view.items.add( focusable() );
+
+					view.updateGroupedItems();
+
+					view.focusTracker.isFocused = true;
+					view.focusTracker.focusedElement = view.groupedItemsDropdown.element;
+					view.itemsView.focusTracker.isFocused = false;
+					view.itemsView.focusTracker.focusedElement = null;
+
+					view.keystrokes.press( keyEvtData );
+
+					sinon.assert.calledOnce( view.items.get( 0 ).focus );
+				} );
 			} );
 		} );
 	} );
 
 	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 +404,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 +422,29 @@ 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 );
+		} );
+
+		it( 'focuses the #groupedItemsDropdown when view#shouldGroupWhenFull is true', () => {
+			document.body.appendChild( view.element );
+			view.element.style.width = '200px';
+			view.shouldGroupWhenFull = true;
+
+			view.items.add( focusable() );
+			view.items.add( focusable() );
+			view.items.add( focusable() );
+
+			view.updateGroupedItems();
+
+			sinon.spy( view.groupedItemsDropdown, 'focus' );
+
+			view.focusLast();
+
+			sinon.assert.calledOnce( view.groupedItemsDropdown.focus );
+
+			view.element.remove();
 		} );
 	} );
 
@@ -366,14 +492,44 @@ describe( 'ToolbarView', () => {
 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',
+				outline: '1px solid green'
+			}
+		},
+		children: [
+			{
+				text: bind.to( 'label' )
+			}
+		]
+	} );
 
 	return view;
 }
@@ -388,3 +544,11 @@ function namedFactory( name ) {
 		return view;
 	};
 }
+
+function getArrowKeyData( arrow ) {
+	return {
+		keyCode: keyCodes[ arrow ],
+		preventDefault: sinon.spy(),
+		stopPropagation: sinon.spy()
+	};
+}