Browse Source

Merge branch 'master' into i/6667

Kuba Niegowski 5 years ago
parent
commit
eb160ba661

+ 1 - 0
docs/framework/guides/contributing/testing-environment.md

@@ -57,6 +57,7 @@ The task accepts the following options:
 * `--files` – Specifies test files to run. Accepts a package name or a glob. For example `--files=ckeditor5` will only run tests from the CKEditor 5 main package. Read more about the [rules for converting the `--files` option to a glob pattern](https://github.com/ckeditor/ckeditor5-dev/tree/master/packages/ckeditor5-dev-tests#rules-for-converting---files-option-to-glob-pattern).
 * `--additionalLanguages="ar,pl,..."` – Specifies extra languages to the [CKEditor 5 webpack plugin](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-webpack-plugin). Check out the {@link features/ui-language UI language guide} to learn more.
 * `--debug` (alias `-d`) – Allows specifying custom debug flags. For example, the `--debug engine` option uncomments the `// @if CK_DEBUG_ENGINE //` lines in the code. Note that by default `--debug` is set to `true` even if you did not specify it. This enables the base set of debug logs (`// @if CK_DEBUG //`) which should always be enabled in the testing environment. You can completely turn off the debug mode by setting the `--debug false` option.
+* `--port` – Specifies the port for the server to use. Defaults to `8125`.
 
 It starts the server available at http://localhost:8125.
 

+ 1 - 1
package.json

@@ -130,7 +130,7 @@
     "manual": "node --max_old_space_size=4096 node_modules/@ckeditor/ckeditor5-dev-tests/bin/test-manual.js",
     "bootstrap": "yarn install",
     "clean": "rm -rf node_modules",
-    "reset": "rm -rf ./packages ./node_modules && yarn run bootstrap",
+    "reset": "rm -rf ./node_modules && yarn run bootstrap",
     "reinstall": "yarn run clean && yarn run bootstrap",
     "docs": "node --max-old-space-size=4096 ./scripts/docs/build-docs.js",
     "docs:api": "node ./scripts/docs/build-api-docs.js",

+ 31 - 14
packages/ckeditor5-engine/tests/view/view/view.js

@@ -16,6 +16,7 @@ import FocusObserver from '../../../src/view/observer/focusobserver';
 import CompositionObserver from '../../../src/view/observer/compositionobserver';
 import ViewRange from '../../../src/view/range';
 import ViewElement from '../../../src/view/element';
+import ViewContainerElement from '../../../src/view/containerelement';
 import ViewPosition from '../../../src/view/position';
 import ViewSelection from '../../../src/view/selection';
 import { StylesProcessor } from '../../../src/view/stylesmap';
@@ -527,7 +528,10 @@ describe( 'view', () => {
 
 			view.attachDomRoot( domRoot );
 
-			viewRoot._appendChild( new ViewElement( viewDocument, 'p' ) );
+			// It must be a container element to be rendered with the bogus <br> inside which ensures
+			// that the browser sees a selection position inside (empty <p> may not be selectable).
+			// May help resolving https://github.com/ckeditor/ckeditor5/issues/6655.
+			viewRoot._appendChild( new ViewContainerElement( viewDocument, 'p' ) );
 			view.forceRender();
 
 			domElement = createElement( document, 'div', { contenteditable: 'true' } );
@@ -542,27 +546,40 @@ describe( 'view', () => {
 		} );
 
 		it( 'should be true if selection is inside a DOM root element', done => {
-			domSelection.collapse( domP, 0 );
+			domRoot.focus();
 
-			// Wait for async selectionchange event on DOM document.
 			setTimeout( () => {
-				expect( view.hasDomSelection ).to.be.true;
+				domSelection.collapse( domP, 0 );
 
-				done();
-			}, 100 );
+				// Wait for async selectionchange event on DOM document.
+				setTimeout( () => {
+					expect( view.hasDomSelection ).to.be.true;
+
+					done();
+				}, 10 );
+			}, 10 );
 		} );
 
 		it( 'should be true if selection is inside a DOM root element - no focus', done => {
-			domSelection.collapse( domP, 0 );
-			domRoot.blur();
+			domRoot.focus();
 
-			// Wait for async selectionchange event on DOM document.
 			setTimeout( () => {
-				expect( view.hasDomSelection ).to.be.true;
-				expect( view.document.isFocused ).to.be.false;
-
-				done();
-			}, 100 );
+				domSelection.collapse( domP, 0 );
+
+				setTimeout( () => {
+					// We could also do domRoot.blur() here but it's always better to know where the focus went.
+					// E.g. if it went to some <input>, the selection would disappear from the editor and the test would fail.
+					domRoot.blur();
+
+					// Wait for async selectionchange event on DOM document.
+					setTimeout( () => {
+						expect( view.hasDomSelection ).to.be.true;
+						expect( view.document.isFocused ).to.be.false;
+
+						done();
+					}, 10 );
+				}, 10 );
+			}, 10 );
 		} );
 
 		it( 'should be false if selection is outside DOM root element', done => {

+ 17 - 3
packages/ckeditor5-table/src/converters/table-cell-refresh-post-fixer.js

@@ -28,10 +28,21 @@ function tableCellRefreshPostFixer( model ) {
 	// Stores cells to be refreshed so the table cell will be refreshed once for multiple changes.
 	const cellsToRefresh = new Set();
 
+	// Counting the paragraph inserts to verify if it increased to more than one paragraph in the current differ.
+	let insertCount = 0;
+
 	for ( const change of differ.getChanges() ) {
 		const parent = change.type == 'insert' || change.type == 'remove' ? change.position.parent : change.range.start.parent;
 
-		if ( parent.is( 'tableCell' ) && checkRefresh( parent, change.type ) ) {
+		if ( !parent.is( 'tableCell' ) ) {
+			continue;
+		}
+
+		if ( change.type == 'insert' ) {
+			insertCount++;
+		}
+
+		if ( checkRefresh( parent, change.type, insertCount ) ) {
 			cellsToRefresh.add( parent );
 		}
 	}
@@ -60,7 +71,8 @@ function tableCellRefreshPostFixer( model ) {
 //
 // @param {module:engine/model/element~Element} tableCell The table cell to check.
 // @param {String} type Type of change.
-function checkRefresh( tableCell, type ) {
+// @param {Number} insertCount The number of inserts in differ.
+function checkRefresh( tableCell, type, insertCount ) {
 	const hasInnerParagraph = Array.from( tableCell.getChildren() ).some( child => child.is( 'paragraph' ) );
 
 	// If there is no paragraph in table cell then the view doesn't require refreshing.
@@ -83,5 +95,7 @@ function checkRefresh( tableCell, type ) {
 	//
 	// - another element is added to a single paragraph (childCount becomes >= 2)
 	// - another element is removed and a single paragraph is left (childCount == 1)
-	return tableCell.childCount <= ( type == 'insert' ? 2 : 1 );
+	//
+	// Change is not needed if there were multiple blocks before change.
+	return tableCell.childCount <= ( type == 'insert' ? insertCount + 1 : 1 );
 }

+ 1 - 1
packages/ckeditor5-table/src/tableselection.js

@@ -231,7 +231,7 @@ export default class TableSelection extends Plugin {
 				return;
 			}
 
-			const anchorCell = getTableCellsContainingSelection( editor.model.document.selection )[ 0 ];
+			const anchorCell = this.getAnchorCell() || getTableCellsContainingSelection( editor.model.document.selection )[ 0 ];
 
 			if ( !anchorCell ) {
 				return;

+ 64 - 11
packages/ckeditor5-table/src/ui/colorinputview.js

@@ -101,10 +101,20 @@ export default class ColorInputView extends View {
 		 * An instance of the input allowing the user to type a color value.
 		 *
 		 * @protected
-		 * @member {module:ui/dropdown/dropdown~DropdownView}
+		 * @member {module:ui/inputtext/inputtextview~InputTextView}
 		 */
 		this._inputView = this._createInputTextView( locale );
 
+		/**
+		 * The flag that indicates whether the user is still typing.
+		 * If set to true, it means that the text input field ({@link #_inputView}) still has the focus.
+		 * So, we should interrupt the user by replacing the input's value.
+		 *
+		 * @protected
+		 * @member {Boolean}
+		 */
+		this._stillTyping = false;
+
 		this.setTemplate( {
 			tag: 'div',
 			attributes: {
@@ -122,6 +132,8 @@ export default class ColorInputView extends View {
 				this._dropdownView
 			]
 		} );
+
+		this.on( 'change:value', ( evt, name, inputValue ) => this._setInputValue( inputValue ) );
 	}
 
 	/**
@@ -186,25 +198,42 @@ export default class ColorInputView extends View {
 	}
 
 	/**
-	 * Creates and configures the {@link #_inputView}.
+	 * Creates and configures an instance of {@link module:ui/inputtext/inputtextview~InputTextView}.
 	 *
 	 * @private
+	 * @returns {module:ui/inputtext/inputtextview~InputTextView} A configured instance to be set as {@link #_inputView}.
 	 */
 	_createInputTextView() {
 		const locale = this.locale;
-		const input = new InputTextView( locale );
+		const inputView = new InputTextView( locale );
+
+		inputView.extendTemplate( {
+			on: {
+				blur: inputView.bindTemplate.to( 'blur' )
+			}
+		} );
+
+		inputView.value = this.value;
+		inputView.bind( 'isReadOnly' ).to( this );
+		inputView.bind( 'hasError' ).to( this );
 
-		input.bind( 'value' ).to( this );
-		input.bind( 'isReadOnly' ).to( this );
-		input.bind( 'hasError' ).to( this );
+		inputView.on( 'input', () => {
+			const inputValue = inputView.element.value;
+			// Check if the value matches one of our defined colors' label.
+			const mappedColor = this.options.colorDefinitions.find( def => inputValue === def.label );
 
-		input.on( 'input', () => {
-			this.value = input.element.value;
+			this._stillTyping = true;
+			this.value = mappedColor && mappedColor.color || inputValue;
 		} );
 
-		input.delegate( 'input' ).to( this );
+		inputView.on( 'blur', () => {
+			this._stillTyping = false;
+			this._setInputValue( inputView.element.value );
+		} );
 
-		return input;
+		inputView.delegate( 'input' ).to( this );
+
+		return inputView;
 	}
 
 	/**
@@ -246,9 +275,33 @@ export default class ColorInputView extends View {
 			this._dropdownView.isOpen = false;
 			this.fire( 'input' );
 		} );
-
 		colorGrid.bind( 'selectedColor' ).to( this, 'value' );
 
 		return colorGrid;
 	}
+
+	/**
+	 * Sets {@link #_inputView}'s value property to the color value or color label,
+	 * if there is one and the user is not typing.
+	 *
+	 * Handles cases like:
+	 *
+	 * * Someone picks the color in the grid.
+	 * * The color is set from the plugin level.
+	 *
+	 * @private
+	 * @param {String} inputValue Color value to be set.
+	 */
+	_setInputValue( inputValue ) {
+		if ( !this._stillTyping ) {
+			// Check if the value matches one of our defined colors.
+			const mappedColor = this.options.colorDefinitions.find( def => inputValue === def.color );
+
+			if ( mappedColor ) {
+				this._inputView.value = mappedColor.label;
+			} else {
+				this._inputView.value = inputValue || '';
+			}
+		}
+	}
 }

+ 76 - 7
packages/ckeditor5-table/tests/converters/table-cell-refresh-post-fixer.js

@@ -55,18 +55,16 @@ describe( 'Table cell refresh post-fixer', () => {
 		return editor.destroy();
 	} );
 
-	it( 'should rename <span> to <p> when adding more <paragraph> elements to the same table cell', () => {
+	it( 'should rename <span> to <p> when adding <paragraph> element to the same table cell', () => {
 		editor.setData( viewTable( [ [ '<p>00</p>' ] ] ) );
 
 		const table = root.getChild( 0 );
 
 		model.change( writer => {
 			const nodeByPath = table.getNodeByPath( [ 0, 0, 0 ] );
-
 			const paragraph = writer.createElement( 'paragraph' );
 
 			writer.insert( paragraph, nodeByPath, 'after' );
-
 			writer.setSelection( nodeByPath.nextSibling, 0 );
 		} );
 
@@ -76,18 +74,37 @@ describe( 'Table cell refresh post-fixer', () => {
 		sinon.assert.calledOnce( refreshItemSpy );
 	} );
 
-	it( 'should rename <span> to <p> on adding other block element to the same table cell', () => {
+	it( 'should rename <span> to <p> when adding more <paragraph> elements to the same table cell', () => {
 		editor.setData( viewTable( [ [ '<p>00</p>' ] ] ) );
 
 		const table = root.getChild( 0 );
 
 		model.change( writer => {
 			const nodeByPath = table.getNodeByPath( [ 0, 0, 0 ] );
+			const paragraph1 = writer.createElement( 'paragraph' );
+			const paragraph2 = writer.createElement( 'paragraph' );
 
-			const paragraph = writer.createElement( 'block' );
+			writer.insert( paragraph1, nodeByPath, 'after' );
+			writer.insert( paragraph2, nodeByPath, 'after' );
+			writer.setSelection( nodeByPath.nextSibling, 0 );
+		} );
 
-			writer.insert( paragraph, nodeByPath, 'after' );
+		assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [
+			[ '<p>00</p><p></p><p></p>' ]
+		], { asWidget: true } ) );
+		sinon.assert.calledOnce( refreshItemSpy );
+	} );
 
+	it( 'should rename <span> to <p> on adding other block element to the same table cell', () => {
+		editor.setData( viewTable( [ [ '<p>00</p>' ] ] ) );
+
+		const table = root.getChild( 0 );
+
+		model.change( writer => {
+			const nodeByPath = table.getNodeByPath( [ 0, 0, 0 ] );
+			const block = writer.createElement( 'block' );
+
+			writer.insert( block, nodeByPath, 'after' );
 			writer.setSelection( nodeByPath.nextSibling, 0 );
 		} );
 
@@ -97,6 +114,27 @@ describe( 'Table cell refresh post-fixer', () => {
 		sinon.assert.calledOnce( refreshItemSpy );
 	} );
 
+	it( 'should rename <span> to <p> on adding multiple other block elements to the same table cell', () => {
+		editor.setData( viewTable( [ [ '<p>00</p>' ] ] ) );
+
+		const table = root.getChild( 0 );
+
+		model.change( writer => {
+			const nodeByPath = table.getNodeByPath( [ 0, 0, 0 ] );
+			const block1 = writer.createElement( 'block' );
+			const block2 = writer.createElement( 'block' );
+
+			writer.insert( block1, nodeByPath, 'after' );
+			writer.insert( block2, nodeByPath, 'after' );
+			writer.setSelection( nodeByPath.nextSibling, 0 );
+		} );
+
+		assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [
+			[ '<p>00</p><div></div><div></div>' ]
+		], { asWidget: true } ) );
+		sinon.assert.calledOnce( refreshItemSpy );
+	} );
+
 	it( 'should properly rename the same element on consecutive changes', () => {
 		editor.setData( viewTable( [ [ '<p>00</p>' ] ] ) );
 
@@ -140,7 +178,7 @@ describe( 'Table cell refresh post-fixer', () => {
 		sinon.assert.calledOnce( refreshItemSpy );
 	} );
 
-	it( 'should rename <p> to <span> when removing all but one paragraph inside table cell', () => {
+	it( 'should rename <p> to <span> when removing one of two paragraphs inside table cell', () => {
 		editor.setData( viewTable( [ [ '<p>00</p><p>foo</p>' ] ] ) );
 
 		const table = root.getChild( 0 );
@@ -155,6 +193,22 @@ describe( 'Table cell refresh post-fixer', () => {
 		sinon.assert.calledOnce( refreshItemSpy );
 	} );
 
+	it( 'should rename <p> to <span> when removing all but one paragraph inside table cell', () => {
+		editor.setData( viewTable( [ [ '<p>00</p><p>foo</p><p>bar</p>' ] ] ) );
+
+		const table = root.getChild( 0 );
+
+		model.change( writer => {
+			writer.remove( table.getNodeByPath( [ 0, 0, 1 ] ) );
+			writer.remove( table.getNodeByPath( [ 0, 0, 1 ] ) );
+		} );
+
+		assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [
+			[ '00' ]
+		], { asWidget: true } ) );
+		sinon.assert.calledOnce( refreshItemSpy );
+	} );
+
 	it( 'should rename <p> to <span> when removing attribute from <paragraph>', () => {
 		editor.setData( '<table><tr><td><p foo="bar">00</p></td></tr></table>' );
 
@@ -250,6 +304,21 @@ describe( 'Table cell refresh post-fixer', () => {
 		sinon.assert.notCalled( refreshItemSpy );
 	} );
 
+	it( 'should do nothing on adding <paragraph> to existing paragraphs', () => {
+		editor.setData( viewTable( [ [ '<p>a</p><p>b</p>' ] ] ) );
+
+		const table = root.getChild( 0 );
+
+		model.change( writer => {
+			writer.insertElement( 'paragraph', table.getNodeByPath( [ 0, 0, 1 ] ), 'after' );
+		} );
+
+		assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [
+			[ '<p>a</p><p>b</p><p></p>' ]
+		], { asWidget: true } ) );
+		sinon.assert.notCalled( refreshItemSpy );
+	} );
+
 	it( 'should do nothing when setting attribute on block item other then <paragraph>', () => {
 		editor.setData( viewTable( [ [ '<div>foo</div>' ] ] ) );
 

+ 33 - 0
packages/ckeditor5-table/tests/tableselection.js

@@ -227,6 +227,39 @@ describe( 'table selection', () => {
 			expect( preventDefault.called ).to.equal( true );
 		} );
 
+		it( 'should use the anchor cell from the selection if possible', () => {
+			const preventDefault = sinon.spy();
+
+			const domEventDataMock = new DomEventData( view, {
+				shiftKey: true,
+				target: view.domConverter.mapViewToDom(
+					// figure > table > tbody > tr > td
+					viewDocument.getRoot().getChild( 0 ).getChild( 1 ).getChild( 0 ).getChild( 0 ).getChild( 2 )
+				),
+				preventDefault
+			} );
+
+			tableSelection.setCellSelection(
+				modelRoot.getNodeByPath( [ 0, 1, 0 ] ),
+				modelRoot.getNodeByPath( [ 0, 2, 1 ] )
+			);
+			assertSelectedCells( model, [
+				[ 0, 0, 0 ],
+				[ 1, 1, 0 ],
+				[ 1, 1, 0 ]
+			] );
+
+			viewDocument.fire( 'mousedown', domEventDataMock );
+
+			assertSelectedCells( model, [
+				[ 1, 1, 1 ],
+				[ 1, 1, 1 ],
+				[ 0, 0, 0 ]
+			] );
+
+			expect( preventDefault.called ).to.equal( true );
+		} );
+
 		it( 'should ignore `selectionChange` event when selecting cells', () => {
 			const consoleLog = sinon.stub( console, 'log' );
 			const preventDefault = sinon.spy();

+ 87 - 3
packages/ckeditor5-table/tests/ui/colorinputview.js

@@ -3,6 +3,8 @@
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
+/* global Event */
+
 import ColorInputView from '../../src/ui/colorinputview';
 import InputTextView from '@ckeditor/ckeditor5-ui/src/inputtext/inputtextview';
 import ColorGridView from '@ckeditor/ckeditor5-ui/src/colorgrid/colorgridview';
@@ -138,7 +140,7 @@ describe( 'ColorInputView', () => {
 				expect( colorGridView ).to.be.instanceOf( ColorGridView );
 			} );
 
-			it( 'should set #value upon #execute', () => {
+			it( 'should set ColorInputView#value upon ColorTileView#execute', () => {
 				expect( view.value ).to.equal( '' );
 
 				colorGridView.items.last.fire( 'execute' );
@@ -146,7 +148,15 @@ describe( 'ColorInputView', () => {
 				expect( view.value ).to.equal( 'rgb(0,0,255)' );
 			} );
 
-			it( 'should close the dropdown upon #execute', () => {
+			it( 'should set InputTextView#value to the selected color\'s label upon ColorTileView#execute', () => {
+				expect( inputView.value ).to.equal( '' );
+
+				colorGridView.items.last.fire( 'execute' );
+
+				expect( inputView.value ).to.equal( 'Blue' );
+			} );
+
+			it( 'should close the dropdown upon ColorTileView#execute', () => {
 				view._dropdownView.isOpen = true;
 
 				colorGridView.items.last.fire( 'execute' );
@@ -154,7 +164,7 @@ describe( 'ColorInputView', () => {
 				expect( view._dropdownView.isOpen ).to.be.false;
 			} );
 
-			it( 'should fire #input upon #execute', () => {
+			it( 'should fire the ColorInputView#input event upon ColorTileView#execute', () => {
 				const spy = sinon.spy( view, 'fire' );
 
 				colorGridView.items.last.fire( 'execute' );
@@ -208,6 +218,15 @@ describe( 'ColorInputView', () => {
 				expect( inputView.value ).to.equal( 'bar' );
 			} );
 
+			it( `when the color input value is set to one of defined colors,
+			should use its label as the text input value`, () => {
+				view.value = 'rgb(0,255,0)';
+				expect( inputView.value ).to.equal( 'Green' );
+
+				view.value = 'rgb(255,0,0)';
+				expect( inputView.value ).to.equal( 'Red' );
+			} );
+
 			it( 'should have #isReadOnly bound to the color input', () => {
 				view.isReadOnly = true;
 				expect( inputView.isReadOnly ).to.equal( true );
@@ -236,6 +255,71 @@ describe( 'ColorInputView', () => {
 				expect( view.value ).to.equal( 'bar' );
 			} );
 
+			it(
+				`when any defined color label is given as the text input #value (case-sensitive),
+				should set the color as #value on #input event`,
+				() => {
+					inputView.element.value = 'Red';
+					inputView.fire( 'input' );
+
+					expect( view.value ).to.equal( 'rgb(255,0,0)' );
+
+					inputView.element.value = 'Green';
+					inputView.fire( 'input' );
+
+					expect( view.value ).to.equal( 'rgb(0,255,0)' );
+
+					inputView.element.value = 'blue';
+					inputView.fire( 'input' );
+
+					expect( view.value ).to.equal( 'blue' );
+				}
+			);
+
+			it(
+				`when any defined color label is given as the text input #value (case-sensitive),
+				then a non-defined value is set to the color input,
+				the latter value should be set to text input`,
+				() => {
+					inputView.element.value = 'Red';
+					inputView.fire( 'input' );
+
+					expect( view.value ).to.equal( 'rgb(255,0,0)' );
+
+					view.value = 'rgb(0,0,255)';
+
+					expect( view.value ).to.equal( 'rgb(0,0,255)' );
+				}
+			);
+
+			it(
+				`when any defined color value is given as the text input #value (case-sensitive),
+				its value should be set to color and text inputs after input event`,
+				() => {
+					inputView.element.value = 'rgb(255,0,0)';
+					inputView.fire( 'input' );
+
+					expect( view.value ).to.equal( 'rgb(255,0,0)' );
+					expect( inputView.element.value ).to.equal( 'rgb(255,0,0)' );
+				}
+			);
+
+			it(
+				`when any defined color value is given as the text input #value (case-sensitive),
+				its label should be set to text inputs after blur event on input view input element`,
+				() => {
+					inputView.element.value = 'rgb(255,0,0)';
+
+					inputView.fire( 'input' );
+
+					expect( inputView.element.value ).to.equal( 'rgb(255,0,0)' );
+
+					inputView.element.dispatchEvent( new Event( 'blur' ) );
+
+					expect( inputView.element.value ).to.equal( 'Red' );
+				}
+			);
+
 			it( 'should have #input event delegated to the color input', () => {
 				const spy = sinon.spy();
 				view.on( 'input', spy );

+ 21 - 8
packages/ckeditor5-ui/src/colorgrid/colorgridview.js

@@ -95,21 +95,21 @@ export default class ColorGridView extends View {
 			colorTile.isOn = colorTile.color === this.selectedColor;
 		} );
 
-		colorDefinitions.forEach( item => {
+		colorDefinitions.forEach( color => {
 			const colorTile = new ColorTileView();
 
 			colorTile.set( {
-				color: item.color,
-				label: item.label,
+				color: color.color,
+				label: color.label,
 				tooltip: true,
-				hasBorder: item.options.hasBorder
+				hasBorder: color.options.hasBorder
 			} );
 
 			colorTile.on( 'execute', () => {
 				this.fire( 'execute', {
-					value: item.color,
-					hasBorder: item.options.hasBorder,
-					label: item.label
+					value: color.color,
+					hasBorder: color.options.hasBorder,
+					label: color.label
 				} );
 			} );
 
@@ -175,13 +175,26 @@ export default class ColorGridView extends View {
 		// Start listening for the keystrokes coming from #element.
 		this.keystrokes.listenTo( this.element );
 	}
+
+	/**
+	 * Fired when the `ColorTileView` for the picked item is executed.
+	 *
+	 * @event execute
+	 * @param {Object} data Additional information about the event.
+	 * @param {String} data.value The value of the selected color
+	 * ({@link module:ui/colorgrid/colorgrid~ColorDefinition#color `color.color`}).
+	 * @param {Boolean} data.hasBorder The `hasBorder` property of the selected color
+	 * ({@link module:ui/colorgrid/colorgrid~ColorDefinition#options `color.options.hasBorder`}).
+	 * @param {String} data.Label The label of the selected color
+	 * ({@link module:ui/colorgrid/colorgrid~ColorDefinition#label `color.label`})
+	 */
 }
 
 /**
  * A color definition used to create a {@link module:ui/colorgrid/colortile~ColorTileView}.
  *
  *		{
- *			color: hsl(0, 0%, 75%),
+ *			color: 'hsl(0, 0%, 75%)',
  *			label: 'Light Grey',
  *			options: {
  *				hasBorder: true