Selaa lähdekoodia

Tests: Added tests for the new ListItemView architecture.

Aleksander Nowodzinski 7 vuotta sitten
vanhempi
commit
4ce6af546f

+ 2 - 2
packages/ckeditor5-ui/src/button/button.jsdoc

@@ -121,14 +121,14 @@
  */
 
 /**
- * (Optional) The additional class set on the button.
+ * (Optional) The additional CSS class set on the button.
  *
  * @observable
  * @member {String} #class
  */
 
 /**
- * (Optional) The value of the style attribute.
+ * (Optional) The value of the `style` attribute of the label.
  *
  * @observable
  * @member {String} #style

+ 2 - 1
packages/ckeditor5-ui/src/button/buttonview.js

@@ -128,7 +128,6 @@ export default class ButtonView extends View {
 					bind.to( 'isOn', value => value ? 'ck-on' : 'ck-off' ),
 					bind.if( 'withText', 'ck-button_with-text' )
 				],
-				style: bind.to( 'style' ),
 				type: bind.to( 'type', value => value ? value : 'button' ),
 				tabindex: bind.to( 'tabindex' ),
 				'aria-labelledby': `ck-editor__aria-label_${ ariaLabelUid }`,
@@ -205,6 +204,7 @@ export default class ButtonView extends View {
 	 */
 	_createLabelView( ariaLabelUid ) {
 		const labelView = new View();
+		const bind = this.bindTemplate;
 
 		labelView.setTemplate( {
 			tag: 'span',
@@ -214,6 +214,7 @@ export default class ButtonView extends View {
 					'ck',
 					'ck-button__label'
 				],
+				style: bind.to( 'style' ),
 				id: `ck-editor__aria-label_${ ariaLabelUid }`,
 			},
 

+ 24 - 16
packages/ckeditor5-ui/src/dropdown/utils.js

@@ -165,31 +165,28 @@ export function addToolbarToDropdown( dropdownView, buttons ) {
  * See {@link module:ui/dropdown/utils~createDropdown} and {@link module:list/list~List}.
  *
  * @param {module:ui/dropdown/dropdownview~DropdownView} dropdownView A dropdown instance to which `ListVIew` will be added.
- * @param {module:utils/collection~Collection} items
- * that the inner dropdown {@link module:ui/list/listview~ListView} children are created from.
- *
- * Usually, it is a collection of {@link module:ui/model~Model models}.
+ * @param {Iterable.<module:ui/dropdown/utils~ListDropdownItemDefinition>} items
+ * A collection of the list item definitions to populate the list.
  */
 export function addListToDropdown( dropdownView, items ) {
 	const locale = dropdownView.locale;
 	const listView = dropdownView.listView = new ListView( locale );
 
-	listView.items.bindTo( items ).using( itemModel => {
-		let item;
-
-		if ( itemModel.isSeparator ) {
-			item = new ListSeparatorView( locale );
-		} else {
+	listView.items.bindTo( items ).using( ( { type, model } ) => {
+		if ( type === 'separator' ) {
+			return new ListSeparatorView( locale );
+		} else if ( type === 'button' ) {
+			const listItemView = new ListItemView( locale );
 			const buttonView = new ButtonView( locale );
 
-			item = new ListItemView( locale, buttonView );
+			// Bind all model properties to the button view.
+			buttonView.bind( ...Object.keys( model ) ).to( model );
+			buttonView.delegate( 'execute' ).to( listItemView );
 
-			// Bind all attributes of the model to the item view.
-			buttonView.bind( ...Object.keys( itemModel ) ).to( itemModel );
-			buttonView.delegate( 'execute' ).to( item );
-		}
+			listItemView.children.add( buttonView );
 
-		return item;
+			return listItemView;
+		}
 	} );
 
 	dropdownView.panelView.children.add( listView );
@@ -252,3 +249,14 @@ function focusDropdownContentsOnArrows( dropdownView ) {
 		}
 	} );
 }
+
+/**
+ * A definition of the list item used by the {@link module:ui/dropdown/utils#addListToDropdown}
+ * utility.
+ *
+ * @typedef {Object} module:ui/dropdown/utils~ListDropdownItemDefinition
+ *
+ * @property {String} type Either `'separator'` or `'button'`.
+ * @property {module:ui/model~Model} [model] Model of the item (when **not** `'separator'`).
+ * Its properties fuel the newly created list item (or its children, depending on the `type`).
+ */

+ 8 - 9
packages/ckeditor5-ui/src/list/listitemview.js

@@ -18,15 +18,16 @@ export default class ListItemView extends View {
 	/**
 	 * @inheritDoc
 	 */
-	constructor( locale, childView ) {
-		super();
+	constructor( locale ) {
+		super( locale );
 
 		/**
-		 * The child view of the list item.
+		 * Collection of the child views inside of the list item {@link #element}.
 		 *
-		 * @member {module:ui/view~View} #childView
+		 * @readonly
+		 * @member {module:ui/viewcollection~ViewCollection}
 		 */
-		this.childView = childView;
+		this.children = this.createCollection();
 
 		this.setTemplate( {
 			tag: 'li',
@@ -38,9 +39,7 @@ export default class ListItemView extends View {
 				]
 			},
 
-			children: [
-				childView
-			]
+			children: this.children
 		} );
 	}
 
@@ -48,6 +47,6 @@ export default class ListItemView extends View {
 	 * Focuses the list item.
 	 */
 	focus() {
-		this.childView.focus();
+		this.children.get( 0 ).focus();
 	}
 }

+ 16 - 0
packages/ckeditor5-ui/tests/button/buttonview.js

@@ -97,6 +97,22 @@ describe( 'ButtonView', () => {
 				view.type = null;
 				expect( view.element.getAttribute( 'type' ) ).to.equal( 'button' );
 			} );
+
+			it( 'reacts on view#class', () => {
+				view.set( 'class', 'foo' );
+
+				expect( view.element.classList.contains( 'foo' ) ).to.be.true;
+			} );
+		} );
+
+		describe( 'label', () => {
+			it( 'reacts on view#style', () => {
+				expect( view.labelView.element.attributes.getNamedItem( 'style' ) ).to.be.null;
+
+				view.style = 'color: red';
+
+				expect( view.labelView.element.attributes.getNamedItem( 'style' ).value ).to.equal( 'color: red' );
+			} );
 		} );
 
 		describe( 'tooltip', () => {

+ 64 - 37
packages/ckeditor5-ui/tests/dropdown/utils.js

@@ -278,10 +278,10 @@ describe( 'utils', () => {
 	} );
 
 	describe( 'addListToDropdown()', () => {
-		let items;
+		let definitions, listItems;
 
 		beforeEach( () => {
-			items = new Collection();
+			definitions = new Collection();
 
 			dropdownView = createDropdown( locale );
 			dropdownView.buttonView.set( {
@@ -290,8 +290,9 @@ describe( 'utils', () => {
 				label: 'foo'
 			} );
 
-			addListToDropdown( dropdownView, items );
+			addListToDropdown( dropdownView, definitions );
 
+			listItems = dropdownView.listView.items;
 			dropdownView.render();
 			document.body.appendChild( dropdownView.element );
 		} );
@@ -309,54 +310,80 @@ describe( 'utils', () => {
 				expect( dropdownView.listView ).to.be.instanceof( ListView );
 			} );
 
-			it( 'is bound to model#items', () => {
-				items.add( new Model( { label: 'a', style: 'b' } ) );
-				items.add( new Model( { label: 'c', style: 'd' } ) );
+			it( 'ignores unknown definition types', () => {
+				definitions.add( { type: 'foo' } );
 
-				expect( dropdownView.listView.items ).to.have.length( 2 );
-				expect( dropdownView.listView.items.get( 0 ) ).to.be.instanceOf( ListItemView );
-				expect( dropdownView.listView.items.get( 1 ).label ).to.equal( 'c' );
-				expect( dropdownView.listView.items.get( 1 ).style ).to.equal( 'd' );
-
-				items.remove( 1 );
-				expect( dropdownView.listView.items ).to.have.length( 1 );
-				expect( dropdownView.listView.items.get( 0 ).label ).to.equal( 'a' );
-				expect( dropdownView.listView.items.get( 0 ).style ).to.equal( 'b' );
+				expect( listItems.length ).to.equal( 0 );
 			} );
 
-			it( 'binds all attributes in model#items', () => {
-				const itemModel = new Model( { label: 'a', style: 'b', foo: 'bar', baz: 'qux' } );
+			describe( 'with ButtonView', () => {
+				it( 'is populated using item definitions', () => {
+					definitions.add( {
+						type: 'button',
+						model: new Model( { label: 'a', style: 'b' } )
+					} );
+
+					definitions.add( {
+						type: 'button',
+						model: new Model( { label: 'c', style: 'd' } )
+					} );
+
+					expect( listItems ).to.have.length( 2 );
+					expect( listItems.get( 0 ) ).to.be.instanceOf( ListItemView );
+					expect( listItems.get( 0 ).children.get( 0 ) ).to.be.instanceOf( ButtonView );
+
+					expect( listItems.get( 1 ).children.get( 0 ).label ).to.equal( 'c' );
+					expect( listItems.get( 1 ).children.get( 0 ).style ).to.equal( 'd' );
+
+					definitions.remove( 1 );
+					expect( listItems ).to.have.length( 1 );
+					expect( listItems.get( 0 ).children.get( 0 ).label ).to.equal( 'a' );
+					expect( listItems.get( 0 ).children.get( 0 ).style ).to.equal( 'b' );
+				} );
 
-				items.add( itemModel );
+				it( 'binds all button properties', () => {
+					const def = {
+						type: 'button',
+						model: new Model( { label: 'a', style: 'b', foo: 'bar', baz: 'qux' } )
+					};
 
-				const item = dropdownView.listView.items.get( 0 );
+					definitions.add( def );
 
-				expect( item.foo ).to.equal( 'bar' );
-				expect( item.baz ).to.equal( 'qux' );
+					const button = listItems.get( 0 ).children.get( 0 );
 
-				itemModel.baz = 'foo?';
-				expect( item.baz ).to.equal( 'foo?' );
-			} );
+					expect( button.foo ).to.equal( 'bar' );
+					expect( button.baz ).to.equal( 'qux' );
 
-			it( 'delegates view.listView#execute to the view', done => {
-				items.add( new Model( { label: 'a', style: 'b' } ) );
+					def.model.baz = 'foo?';
+					expect( button.baz ).to.equal( 'foo?' );
+				} );
 
-				dropdownView.on( 'execute', evt => {
-					expect( evt.source ).to.equal( dropdownView.listView.items.get( 0 ) );
-					expect( evt.path ).to.deep.equal( [ dropdownView.listView.items.get( 0 ), dropdownView ] );
+				it( 'delegates ButtonView#execute to the ListItemView', done => {
+					definitions.add( {
+						type: 'button',
+						model: new Model( { label: 'a', style: 'b' } )
+					} );
 
-					done();
-				} );
+					const listItem = listItems.get( 0 );
+					const button = listItem.children.get( 0 );
 
-				dropdownView.listView.items.get( 0 ).fire( 'execute' );
-			} );
+					dropdownView.on( 'execute', evt => {
+						expect( evt.source ).to.equal( button );
+						expect( evt.path ).to.deep.equal( [ button, listItem, dropdownView ] );
+
+						done();
+					} );
 
-			it( 'is filled with separators', () => {
-				const itemModel = new Model( { isSeparator: true } );
+					button.fire( 'execute' );
+				} );
+			} );
 
-				items.add( itemModel );
+			describe( 'with ListSeparatorView', () => {
+				it( 'creates a separator from the definition', () => {
+					definitions.add( { type: 'separator' } );
 
-				expect( dropdownView.listView.items.get( 0 ) ).to.be.instanceOf( ListSeparatorView );
+					expect( listItems.get( 0 ) ).to.be.instanceOf( ListSeparatorView );
+				} );
 			} );
 		} );
 	} );

+ 9 - 140
packages/ckeditor5-ui/tests/list/listitemview.js

@@ -3,20 +3,15 @@
  * For licensing, see LICENSE.md.
  */
 
-/* global Event */
-
+import ButtonView from '../../src/button/buttonview';
 import ListItemView from '../../src/list/listitemview';
-import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler';
+import ViewCollection from '../../src/viewcollection';
 
 describe( 'ListItemView', () => {
 	let view;
 
 	beforeEach( () => {
 		view = new ListItemView();
-		view.set( {
-			style: 'foo',
-			label: 'bar'
-		} );
 
 		return view.render();
 	} );
@@ -27,145 +22,19 @@ describe( 'ListItemView', () => {
 			expect( view.element.classList.contains( 'ck-list__item' ) ).to.be.true;
 		} );
 
-		it( 'should create #keystrokes instance', () => {
-			expect( view.keystrokes ).to.be.instanceOf( KeystrokeHandler );
-		} );
-	} );
-
-	describe( 'render()', () => {
-		it( 'starts listening for #keystrokes coming from #element', () => {
-			view = new ListItemView();
-
-			const spy = sinon.spy( view.keystrokes, 'listenTo' );
-
-			view.render();
-			sinon.assert.calledOnce( spy );
-			sinon.assert.calledWithExactly( spy, view.element );
-		} );
-
-		// https://github.com/ckeditor/ckeditor5-ui/issues/153
-		it( 'triggers view#execute event when Enter or Space key is pressed', () => {
-			const spy = sinon.spy();
-			const evtData = {
-				keyCode: 10,
-				preventDefault: sinon.spy(),
-				stopPropagation: sinon.spy()
-			};
-
-			view.on( 'execute', spy );
-			view.keystrokes.press( evtData );
-
-			sinon.assert.notCalled( spy );
-			sinon.assert.notCalled( evtData.preventDefault );
-
-			evtData.keyCode = 13;
-			view.keystrokes.press( evtData );
-
-			sinon.assert.calledOnce( spy );
-			sinon.assert.calledOnce( evtData.preventDefault );
-
-			evtData.keyCode = 32;
-			view.keystrokes.press( evtData );
-
-			sinon.assert.calledTwice( spy );
-			sinon.assert.calledTwice( evtData.preventDefault );
-		} );
-	} );
-
-	describe( 'DOM bindings', () => {
-		describe( '"class" attribute', () => {
-			it( 'reacts on view#class', () => {
-				expect( view.element.classList ).to.have.length( 2 );
-
-				view.set( 'class', 'foo' );
-
-				expect( view.element.classList.contains( 'foo' ) ).to.be.true;
-			} );
-
-			it( 'reacts on view#isActive', () => {
-				expect( view.element.classList ).to.have.length( 2 );
-
-				view.set( 'isActive', true );
-
-				expect( view.element.classList.contains( 'ck-list__item_active' ) ).to.be.true;
-			} );
-
-			it( 'reacts on view#isEnabled', () => {
-				expect( view.element.classList ).to.have.length( 2 );
-
-				expect( view.element.classList.contains( 'ck-disabled' ) ).to.be.false;
-
-				view.set( 'isEnabled', false );
-
-				expect( view.element.classList.contains( 'ck-disabled' ) ).to.be.true;
-
-				view.set( 'isEnabled', true );
-
-				expect( view.element.classList.contains( 'ck-disabled' ) ).to.be.false;
-			} );
-		} );
-
-		describe( '"style" attribute', () => {
-			it( 'reacts on view#style', () => {
-				expect( view.element.attributes.getNamedItem( 'style' ).value ).to.equal( 'foo' );
-
-				view.style = 'color: red';
-
-				expect( view.element.attributes.getNamedItem( 'style' ).value ).to.equal( 'color: red' );
-			} );
-		} );
-
-		describe( 'text content', () => {
-			it( 'reacts on view#label', () => {
-				expect( view.element.innerHTML ).to.equal( 'bar' );
-
-				view.label = 'baz';
-
-				expect( view.element.innerHTML ).to.equal( 'baz' );
-			} );
-		} );
-
-		describe( 'tabindex', () => {
-			it( 'is initially set ', () => {
-				expect( view.element.attributes.tabindex.value ).to.equal( '-1' );
-			} );
-
-			it( 'reacts on view#tabindex', () => {
-				view.tabindex = 3;
-
-				expect( view.element.attributes.tabindex.value ).to.equal( '3' );
-			} );
-		} );
-
-		describe( 'view#execute event', () => {
-			it( 'triggers view#execute event when "click" is fired in DOM', () => {
-				const spy = sinon.spy();
-
-				view.on( 'execute', spy );
-
-				view.element.dispatchEvent( new Event( 'click' ) );
-				expect( spy.calledOnce ).to.be.true;
-			} );
-
-			it( 'does not triggers view#execute event when "click" is fired in DOM and isEnables=false', () => {
-				const spy = sinon.spy();
-
-				view.on( 'execute', spy );
-
-				view.isEnabled = false;
-
-				view.element.dispatchEvent( new Event( 'click' ) );
-				sinon.assert.notCalled( spy );
-			} );
+		it( 'creates view#children collection', () => {
+			expect( view.children ).to.be.instanceOf( ViewCollection );
 		} );
 	} );
 
 	describe( 'focus()', () => {
-		it( 'focuses the item in DOM', () => {
-			const spy = sinon.spy( view.element, 'focus' );
+		it( 'focuses the first child item', () => {
+			const button = new ButtonView();
+			view.children.add( button );
 
-			view.focus();
+			const spy = sinon.spy( button.element, 'focus' );
 
+			view.focus();
 			sinon.assert.calledOnce( spy );
 		} );
 	} );