Explorar el Código

Merge branch 'table-selection' into i/6113

Maciej Gołaszewski hace 6 años
padre
commit
2df65766be
Se han modificado 59 ficheros con 4296 adiciones y 515 borrados
  1. 6 0
      packages/ckeditor5-table/docs/_snippets/features/build-table-source.js
  2. 54 0
      packages/ckeditor5-table/docs/_snippets/features/table-styling.html
  3. 32 0
      packages/ckeditor5-table/docs/_snippets/features/table-styling.js
  4. 4 0
      packages/ckeditor5-table/docs/features/table.md
  5. 8 5
      packages/ckeditor5-table/lang/contexts.json
  6. 28 0
      packages/ckeditor5-table/src/commands/utils.js
  7. 3 1
      packages/ckeditor5-table/src/converters/downcast.js
  8. 5 1
      packages/ckeditor5-table/src/converters/tableproperties.js
  9. 28 0
      packages/ckeditor5-table/src/table.js
  10. 40 0
      packages/ckeditor5-table/src/tablecellproperties.js
  11. 16 1
      packages/ckeditor5-table/src/tablecellproperties/commands/tablecellborderwidthcommand.js
  12. 16 0
      packages/ckeditor5-table/src/tablecellproperties/commands/tablecellheightcommand.js
  13. 15 1
      packages/ckeditor5-table/src/tablecellproperties/commands/tablecellpaddingcommand.js
  14. 14 2
      packages/ckeditor5-table/src/tablecellproperties/commands/tablecellpropertycommand.js
  15. 2 1
      packages/ckeditor5-table/src/tablecellproperties/commands/tablecellverticalalignmentcommand.js
  16. 16 0
      packages/ckeditor5-table/src/tablecellproperties/commands/tablecellwidthcommand.js
  17. 51 9
      packages/ckeditor5-table/src/tablecellproperties/tablecellpropertiesediting.js
  18. 131 15
      packages/ckeditor5-table/src/tablecellproperties/tablecellpropertiesui.js
  19. 248 59
      packages/ckeditor5-table/src/tablecellproperties/ui/tablecellpropertiesview.js
  20. 40 0
      packages/ckeditor5-table/src/tableproperties.js
  21. 16 1
      packages/ckeditor5-table/src/tableproperties/commands/tableborderwidthcommand.js
  22. 16 0
      packages/ckeditor5-table/src/tableproperties/commands/tableheightcommand.js
  23. 14 2
      packages/ckeditor5-table/src/tableproperties/commands/tablepropertycommand.js
  24. 16 0
      packages/ckeditor5-table/src/tableproperties/commands/tablewidthcommand.js
  25. 43 53
      packages/ckeditor5-table/src/tableproperties/tablepropertiesediting.js
  26. 117 13
      packages/ckeditor5-table/src/tableproperties/tablepropertiesui.js
  27. 117 57
      packages/ckeditor5-table/src/tableproperties/ui/tablepropertiesview.js
  28. 20 12
      packages/ckeditor5-table/src/tableui.js
  29. 252 0
      packages/ckeditor5-table/src/ui/colorinputview.js
  30. 302 10
      packages/ckeditor5-table/src/ui/utils.js
  31. 15 6
      packages/ckeditor5-table/tests/_utils/utils.js
  32. 10 5
      packages/ckeditor5-table/tests/converters/downcast.js
  33. 2 2
      packages/ckeditor5-table/tests/converters/table-cell-refresh-post-fixer.js
  34. 0 1
      packages/ckeditor5-table/tests/manual/tableproperties.js
  35. 20 0
      packages/ckeditor5-table/tests/manual/tickets/6062.html
  36. 27 0
      packages/ckeditor5-table/tests/manual/tickets/6062.js
  37. 9 0
      packages/ckeditor5-table/tests/manual/tickets/6062.md
  38. 56 0
      packages/ckeditor5-table/tests/tablecellproperties/commands/tablecellborderwidthcommand.js
  39. 56 0
      packages/ckeditor5-table/tests/tablecellproperties/commands/tablecellheightcommand.js
  40. 56 0
      packages/ckeditor5-table/tests/tablecellproperties/commands/tablecellpaddingcommand.js
  41. 56 0
      packages/ckeditor5-table/tests/tablecellproperties/commands/tablecellwidthcommand.js
  42. 308 7
      packages/ckeditor5-table/tests/tablecellproperties/tablecellpropertiesediting.js
  43. 330 55
      packages/ckeditor5-table/tests/tablecellproperties/tablecellpropertiesui.js
  44. 216 22
      packages/ckeditor5-table/tests/tablecellproperties/ui/tablecellpropertiesview.js
  45. 4 4
      packages/ckeditor5-table/tests/tableproperties/commands/tablealignmentcommand.js
  46. 56 0
      packages/ckeditor5-table/tests/tableproperties/commands/tableborderwidthcommand.js
  47. 60 4
      packages/ckeditor5-table/tests/tableproperties/commands/tableheightcommand.js
  48. 60 4
      packages/ckeditor5-table/tests/tableproperties/commands/tablewidthcommand.js
  49. 63 0
      packages/ckeditor5-table/tests/tableproperties/tablepropertiesediting-integration.js
  50. 254 47
      packages/ckeditor5-table/tests/tableproperties/tablepropertiesediting.js
  51. 271 46
      packages/ckeditor5-table/tests/tableproperties/tablepropertiesui.js
  52. 145 18
      packages/ckeditor5-table/tests/tableproperties/ui/tablepropertiesview.js
  53. 13 1
      packages/ckeditor5-table/tests/tableui.js
  54. 301 0
      packages/ckeditor5-table/tests/ui/colorinputview.js
  55. 140 10
      packages/ckeditor5-table/tests/ui/utils.js
  56. 40 0
      packages/ckeditor5-table/theme/colorinput.css
  57. 5 0
      packages/ckeditor5-table/theme/table.css
  58. 53 17
      packages/ckeditor5-table/theme/tableform.css
  59. 0 23
      packages/ckeditor5-table/theme/tableproperties.css

+ 6 - 0
packages/ckeditor5-table/docs/_snippets/features/build-table-source.js

@@ -9,8 +9,14 @@ import ClassicEditor from '@ckeditor/ckeditor5-build-classic/src/ckeditor';
 
 import Table from '@ckeditor/ckeditor5-table/src/table';
 import TableToolbar from '@ckeditor/ckeditor5-table/src/tabletoolbar';
+import TableProperties from '@ckeditor/ckeditor5-table/src/tableproperties';
+import TableCellProperties from '@ckeditor/ckeditor5-table/src/tablecellproperties';
 
 ClassicEditor.builtinPlugins.push( Table );
 ClassicEditor.builtinPlugins.push( TableToolbar );
 
 window.ClassicEditor = ClassicEditor;
+window.CKEditorPlugins = {
+	TableProperties,
+	TableCellProperties
+};

+ 54 - 0
packages/ckeditor5-table/docs/_snippets/features/table-styling.html

@@ -0,0 +1,54 @@
+<div id="snippet-table-styling">
+	<p>The ultimate table of <a href="https://en.wikipedia.org/wiki/List_of_coffee_drinks">coffee drinks</a>:</p>
+	<table>
+		<thead>
+			<tr>
+				<th rowspan="2"></th>
+				<th scope="col" rowspan="2">Espresso</th>
+				<th scope="colgroup" colspan="2">Milk</th>
+				<th scope="col" rowspan="2">Water</th>
+			</tr>
+			<tr>
+				<th scope="col">Steamed</th>
+				<th scope="col">Foam</th>
+			</tr>
+		</thead>
+		<tbody>
+			<tr>
+				<th scope="row">Cappuccino</th>
+				<td>yes</td>
+				<td>yes</td>
+				<td>yes</td>
+				<td>no</td>
+			</tr>
+			<tr>
+				<th scope="row">Macchiato</th>
+				<td>yes</td>
+				<td>no</td>
+				<td>yes</td>
+				<td>no</td>
+			</tr>
+			<tr>
+				<th scope="row">Latte</th>
+				<td>yes</td>
+				<td>yes</td>
+				<td>no</td>
+				<td>no</td>
+			</tr>
+			<tr>
+				<th scope="row">Americano</th>
+				<td>yes</td>
+				<td>no</td>
+				<td>no</td>
+				<td>yes</td>
+			</tr>
+		</tbody>
+	</table>
+</div>
+
+<style>
+	.ck.ck-content table td,
+	.ck.ck-content table th {
+		vertical-align: middle;
+	}
+</style>

+ 32 - 0
packages/ckeditor5-table/docs/_snippets/features/table-styling.js

@@ -0,0 +1,32 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* globals ClassicEditor, CKEditorPlugins, console, window, document */
+
+import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config';
+
+ClassicEditor
+	.create( document.querySelector( '#snippet-table-styling' ), {
+		extraPlugins: [
+			CKEditorPlugins.TableProperties,
+			CKEditorPlugins.TableCellProperties,
+		],
+		cloudServices: CS_CONFIG,
+		toolbar: {
+			items: [
+				'insertTable', '|', 'heading', '|', 'bold', 'italic', '|', 'undo', 'redo'
+			],
+			viewportTopOffset: window.getViewportTopOffsetConfig()
+		},
+		table: {
+			contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells', 'tableProperties', 'tableCellProperties' ]
+		}
+	} )
+	.then( editor => {
+		window.editorStyling = editor;
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );

+ 4 - 0
packages/ckeditor5-table/docs/features/table.md

@@ -11,6 +11,10 @@ The {@link module:table/table~Table} feature offers table creation and editing t
 
 {@snippet features/table}
 
+## Styling tables
+
+{@snippet features/table-styling}
+
 ## Installation
 
 <info-box info>

+ 8 - 5
packages/ckeditor5-table/lang/contexts.json

@@ -18,21 +18,21 @@
 	"Split cell horizontally": "Label for the split table cell horizontally button.",
 	"Merge cells": "Label for the merge table cells button.",
 	"Table toolbar": "The label used by assistive technologies describing a table toolbar attached to a table widget.",
+	"Table properties": "The label describing the form allowing to specify the properties of a selected table.",
 	"Cell properties": "The label describing the form allowing to specify the properties of a selected table cell.",
 	"Border": "The label describing a group of border–related form fields (border style, color, etc.).",
 	"Style": "The label for the dropdown that allows configuring the border style of a table or a table cell.",
-	"Width": "The label for the input that allows configuring the border width of a table or a table cell.",
+	"Width": "The label for the input that allows configuring the width of a table or a table cell or the width of a border.",
+	"Height": "The label for the input that allows configuring the height of a table or a table cell.",
 	"Color": "The label for the input that allows configuring the border color of a table or a table cell.",
 	"Background": "The label for the input that allows configuring the background color of a table or a table cell.",
 	"Padding": "The label for the input that allows configuring the padding of a table cell.",
 	"Dimensions": "The label describing a group of form fields that allows setting dimensions of a table or a table cell.",
-	"Text alignment": "The label for the group of toolbars that allows configuring the text alignment in a table cell.",
+	"Table cell text alignment": "The label for the group of toolbars that allows configuring the text alignment in a table cell.",
 	"Alignment": "The label for the toolbar that allows configuring the alignment of a table.",
 	"Horizontal text alignment toolbar": "The label used by assistive technologies describing a toolbar that allows configuring the horizontal text alignment in a table cell.",
 	"Vertical text alignment toolbar": "The label used by assistive technologies describing a toolbar that allows configuring the vertical text alignment in a table cell.",
 	"Table alignment toolbar": "The label used by assistive technologies describing a toolbar that allows configuring the alignment of a table.",
-	"Save": "The label for the button that saves the changes made to the table or table cell properties.",
-	"Cancel": "The label for the button that rejects the changes made to the table or table cell properties.",
 	"None": "The label for the border style dropdown when no style is applied to a table or a table cell.",
 	"Solid": "The label for the border style dropdown when the solid border is applied to a table or a table cell.",
 	"Dotted": "The label for the border style dropdown when the dotted border is applied to a table or a table cell.",
@@ -51,5 +51,8 @@
 	"Align cell text to the bottom": "The label used by assistive technologies describing a button that aligns the table cell text to the bottom.",
 	"Align table to the left": "The label used by assistive technologies describing a button that aligns the table to the left.",
 	"Center table": "The label used by assistive technologies describing a button that centers the table.",
-	"Align table to the right": "The label used by assistive technologies describing a button that aligns the table to the right."
+	"Align table to the right": "The label used by assistive technologies describing a button that aligns the table to the right.",
+	"Remove color": "The label used by a button next to the color palette in the color picker that removes the color (resets it to an empty value).",
+	"The color is invalid. Try \"#FF0000\" or \"rgb(255,0,0)\" or \"red\".": "The localized error string that can be displayed next to color (background, border) fields that have an invalid value",
+	"The value is invalid. Try \"10px\" or \"2em\" or simply \"2\".": "The localized error string that can be displayed next to length (padding, border width) fields that have an invalid value."
 }

+ 28 - 0
packages/ckeditor5-table/src/commands/utils.js

@@ -86,3 +86,31 @@ export function getSingleValue( objectOrString ) {
 		return top;
 	}
 }
+
+/**
+ * Adds a unit to a value if the value is a number or a string representing a number.
+ *
+ * **Note**: It does nothing to non-numeric values.
+ *
+ *		getSingleValue( 25, 'px' );		// '25px'
+ *		getSingleValue( 25, 'em' );		// '25em'
+ *		getSingleValue( '25em', 'px' );	// '25em'
+ *		getSingleValue( 'foo', 'px' );	// 'foo'
+ *
+ * @param {*} value
+ * @param {String} defaultUnit A default unit added to a numeric value.
+ * @returns {String|*}
+ */
+export function addDefaultUnitToNumericValue( value, defaultUnit ) {
+	const numericValue = parseFloat( value );
+
+	if ( Number.isNaN( numericValue ) ) {
+		return value;
+	}
+
+	if ( String( numericValue ) !== String( value ) ) {
+		return value;
+	}
+
+	return `${ numericValue }${ defaultUnit }`;
+}

+ 3 - 1
packages/ckeditor5-table/src/converters/downcast.js

@@ -397,7 +397,9 @@ function createViewTableCellElement( tableWalkerValue, tableAttributes, insertPo
 		conversionApi.consumable.consume( innerParagraph, 'insert' );
 
 		if ( options.asWidget ) {
-			const fakeParagraph = conversionApi.writer.createContainerElement( 'span' );
+			// Use display:inline-block to force Chrome/Safari to limit text mutations to this element.
+			// See #6062.
+			const fakeParagraph = conversionApi.writer.createContainerElement( 'span', { style: 'display:inline-block' } );
 
 			conversionApi.mapper.bindElements( innerParagraph, fakeParagraph );
 			conversionApi.writer.insert( paragraphInsertPosition, fakeParagraph );

+ 5 - 1
packages/ckeditor5-table/src/converters/tableproperties.js

@@ -105,12 +105,16 @@ export function downcastTableAttribute( conversion, modelAttribute, styleName )
 		const { item, attributeNewValue } = data;
 		const { mapper, writer } = conversionApi;
 
+		if ( !conversionApi.consumable.consume( data.item, evt.name ) ) {
+			return;
+		}
+
 		const table = [ ...mapper.toViewElement( item ).getChildren() ].find( child => child.is( 'table' ) );
 
 		if ( attributeNewValue ) {
 			writer.setStyle( styleName, attributeNewValue, table );
 		} else {
-			writer.removeAttribute( styleName, table );
+			writer.removeStyle( styleName, table );
 		}
 	} ) );
 }

+ 28 - 0
packages/ckeditor5-table/src/table.js

@@ -63,3 +63,31 @@ export default class Table extends Plugin {
  *
  * @member {module:table/table~TableConfig} module:core/editor/editorconfig~EditorConfig#table
  */
+
+/**
+ * An array of colors definitions (either strings or objects).
+ *
+ *		const colors = [
+ *			{
+ *				color: 'hsl(0, 0%, 60%)',
+ *				label: 'Grey'
+ *			},
+ *			'hsl(0, 0%, 80%)',
+ *			{
+ *				color: 'hsl(0, 0%, 90%)',
+ *				label: 'Light grey'
+ *			},
+ *			{
+ *				color: 'hsl(0, 0%, 100%)',
+ *				label: 'White',
+ *				hasBorder: true
+ *			},
+ *			'#FF0000'
+ *		]
+ *
+ * Usually used as a configuration parameter, for instance in
+ * {@link module:table/table~TableConfig#tableProperties `config.table.tableProperties`}
+ * or {@link module:table/table~TableConfig#tableCellProperties `config.table.tableCellProperties`}.
+ *
+ * @typedef {Array.<String|Object>} module:table/table~TableColorConfig
+ */

+ 40 - 0
packages/ckeditor5-table/src/tablecellproperties.js

@@ -35,3 +35,43 @@ export default class TableCellProperties extends Plugin {
 		return [ TableCellPropertiesEditing, TableCellPropertiesUI ];
 	}
 }
+
+/**
+ * A configuration of the table cell properties user interface (balloon). It allows to define:
+ *
+ * * the color palette for the cell border color style field (`tableCellProperties.borderColors`),
+ * * the color palette for the cell background style field (`tableCellProperties.backgroundColors`).
+ *
+ *		const tableConfig = {
+ *			tableCellProperties: {
+ *				borderColors: [
+ *					{
+ *						color: 'hsl(0, 0%, 90%)',
+ *						label: 'Light grey'
+ *					},
+ *					// ...
+ *				],
+ *				backgroundColors: [
+ *					{
+ *						color: 'hsl(120, 75%, 60%)',
+ *						label: 'Green'
+ *					},
+ *					// ...
+ *				]
+ *			}
+ *		};
+ *
+ * **Note**: The configurations do not impact the data being loaded into the editor,
+ * i.e. they do not limit or filter the colors in the data. They are used only in the user interface
+ * allowing users to pick colors in a more convenient way.
+ *
+ * The default color palettes for the cell background and the cell border are the same
+ * ({@link module:table/ui/utils~defaultColors check out their content}).
+ *
+ * Both color palette configurations must follow the
+ * {@link module:table/table~TableColorConfig table color configuration format}.
+ *
+ * Read more about configuring the table feature in {@link module:table/table~TableConfig}.
+ *
+ * @member {Object} module:table/table~TableConfig#tableCellProperties
+ */

+ 16 - 1
packages/ckeditor5-table/src/tablecellproperties/commands/tablecellborderwidthcommand.js

@@ -7,8 +7,8 @@
  * @module table/tablecellproperties/commands/tablecellborderwidthcommand
  */
 
+import { addDefaultUnitToNumericValue, getSingleValue } from '../../commands/utils';
 import TableCellPropertyCommand from './tablecellpropertycommand';
-import { getSingleValue } from '../../commands/utils';
 
 /**
  * The table cell border width command.
@@ -22,6 +22,14 @@ import { getSingleValue } from '../../commands/utils';
  *			value: '5px'
  *		} );
  *
+ * **Note**: This command adds the default `'px'` unit to numeric values. Executing:
+ *
+ *		editor.execute( 'tableCellBorderWidth', {
+ *			value: '5'
+ *		} );
+ *
+ * will set the `borderWidth` attribute to `'5px'` in the model.
+ *
  * @extends module:core/command~Command
  */
 export default class TableCellBorderWidthCommand extends TableCellPropertyCommand {
@@ -44,4 +52,11 @@ export default class TableCellBorderWidthCommand extends TableCellPropertyComman
 
 		return getSingleValue( tableCell.getAttribute( this.attributeName ) );
 	}
+
+	/**
+	 * @inheritDoc
+	 */
+	_getValueToSet( value ) {
+		return addDefaultUnitToNumericValue( value, 'px' );
+	}
 }

+ 16 - 0
packages/ckeditor5-table/src/tablecellproperties/commands/tablecellheightcommand.js

@@ -7,6 +7,7 @@
  * @module table/tablecellproperties/commands/tablecellheightcommand
  */
 
+import { addDefaultUnitToNumericValue } from '../../commands/utils';
 import TableCellPropertyCommand from './tablecellpropertycommand';
 
 /**
@@ -21,6 +22,14 @@ import TableCellPropertyCommand from './tablecellpropertycommand';
  *			value: '50px'
  *		} );
  *
+ * **Note**: This command adds the default `'px'` unit to numeric values. Executing:
+ *
+ *		editor.execute( 'tableCellHeight', {
+ *			value: '50'
+ *		} );
+ *
+ * will set the `height` attribute to `'50px'` in the model.
+ *
  * @extends module:table/tablecellproperties/commands/tablecellpropertycommand
  */
 export default class TableCellHeightCommand extends TableCellPropertyCommand {
@@ -32,4 +41,11 @@ export default class TableCellHeightCommand extends TableCellPropertyCommand {
 	constructor( editor ) {
 		super( editor, 'height' );
 	}
+
+	/**
+	 * @inheritDoc
+	 */
+	_getValueToSet( value ) {
+		return addDefaultUnitToNumericValue( value, 'px' );
+	}
 }

+ 15 - 1
packages/ckeditor5-table/src/tablecellproperties/commands/tablecellpaddingcommand.js

@@ -7,8 +7,8 @@
  * @module table/tablecellproperties/commands/tablecellpaddingcommand
  */
 
+import { addDefaultUnitToNumericValue, getSingleValue } from '../../commands/utils';
 import TableCellPropertyCommand from './tablecellpropertycommand';
-import { getSingleValue } from '../../commands/utils';
 
 /**
  * The table cell padding command.
@@ -22,6 +22,13 @@ import { getSingleValue } from '../../commands/utils';
  *			value: '5px'
  *		} );
  *
+ * **Note**: This command adds the default `'px'` unit to numeric values. Executing:
+ *
+ *		editor.execute( 'tableCellPadding', {
+ *			value: '5'
+ *		} );
+ *
+ * will set the `padding` attribute to `'5px'` in the model.
  * @extends module:table/tablecellproperties/commands/tablecellpropertycommand
  */
 export default class TableCellPaddingCommand extends TableCellPropertyCommand {
@@ -44,4 +51,11 @@ export default class TableCellPaddingCommand extends TableCellPropertyCommand {
 
 		return getSingleValue( tableCell.getAttribute( this.attributeName ) );
 	}
+
+	/**
+	 * @inheritDoc
+	 */
+	_getValueToSet( value ) {
+		return addDefaultUnitToNumericValue( value, 'px' );
+	}
 }

+ 14 - 2
packages/ckeditor5-table/src/tablecellproperties/commands/tablecellpropertycommand.js

@@ -62,10 +62,11 @@ export default class TableCellPropertyCommand extends Command {
 
 		const tableCells = Array.from( selection.getSelectedBlocks() )
 			.map( element => findAncestor( 'tableCell', model.createPositionAt( element, 0 ) ) );
+		const valueToSet = this._getValueToSet( value );
 
 		model.enqueueChange( batch || 'default', writer => {
-			if ( value ) {
-				tableCells.forEach( tableCell => writer.setAttribute( this.attributeName, value, tableCell ) );
+			if ( valueToSet ) {
+				tableCells.forEach( tableCell => writer.setAttribute( this.attributeName, valueToSet, tableCell ) );
 			} else {
 				tableCells.forEach( tableCell => writer.removeAttribute( this.attributeName, tableCell ) );
 			}
@@ -86,4 +87,15 @@ export default class TableCellPropertyCommand extends Command {
 
 		return tableCell.getAttribute( this.attributeName );
 	}
+
+	/**
+	 * Returns the proper model value. Can be used to add a default unit to numeric values.
+	 *
+	 * @private
+	 * @param {*} value
+	 * @returns {*}
+	 */
+	_getValueToSet( value ) {
+		return value;
+	}
 }

+ 2 - 1
packages/ckeditor5-table/src/tablecellproperties/commands/tablecellverticalalignmentcommand.js

@@ -26,7 +26,8 @@ import TableCellPropertyCommand from './tablecellpropertycommand';
  *
  * * `'top'`
  * * `'bottom'`
- * * `'middle'`
+ *
+ * The `'middle'` value is default one so there's no need to set this value.
  *
  * @extends module:table/tablecellproperties/commands/tablecellpropertycommand
  */

+ 16 - 0
packages/ckeditor5-table/src/tablecellproperties/commands/tablecellwidthcommand.js

@@ -7,6 +7,7 @@
  * @module table/tablecellproperties/commands/tablecellwidthcommand
  */
 
+import { addDefaultUnitToNumericValue } from '../../commands/utils';
 import TableCellPropertyCommand from './tablecellpropertycommand';
 
 /**
@@ -21,6 +22,14 @@ import TableCellPropertyCommand from './tablecellpropertycommand';
  *			value: '50px'
  *		} );
  *
+ * **Note**: This command adds a default `'px'` unit to a numeric values. Executing:
+ *
+ *		editor.execute( 'tableCellWidth', {
+ *			value: '50'
+ *		} );
+ *
+ * Will set `width` attribute to `'50px'` in the model.
+ *
  * @extends module:table/tablecellproperties/commands/tablecellpropertycommand
  */
 export default class TableCellWidthCommand extends TableCellPropertyCommand {
@@ -32,4 +41,11 @@ export default class TableCellWidthCommand extends TableCellPropertyCommand {
 	constructor( editor ) {
 		super( editor, 'width' );
 	}
+
+	/**
+	 * @inheritDoc
+	 */
+	_getValueToSet( value ) {
+		return addDefaultUnitToNumericValue( value, 'px' );
+	}
 }

+ 51 - 9
packages/ckeditor5-table/src/tablecellproperties/tablecellpropertiesediting.js

@@ -24,6 +24,8 @@ import TableCellBorderStyleCommand from './commands/tablecellborderstylecommand'
 import TableCellBorderColorCommand from './commands/tablecellbordercolorcommand';
 import TableCellBorderWidthCommand from './commands/tablecellborderwidthcommand';
 
+const VALIGN_VALUES_REG_EXP = /^(top|bottom)$/;
+
 /**
  * The table cell properties editing feature.
  *
@@ -92,7 +94,7 @@ export default class TableCellPropertiesEditing extends Plugin {
 		enableProperty( schema, conversion, 'backgroundColor', 'background-color' );
 		editor.commands.add( 'tableCellBackgroundColor', new TableCellBackgroundColorCommand( editor ) );
 
-		enableProperty( schema, conversion, 'verticalAlignment', 'vertical-align' );
+		enableVerticalAlignmentProperty( schema, conversion );
 		editor.commands.add( 'tableCellVerticalAlignment', new TableCellVerticalAlignmentCommand( editor ) );
 	}
 }
@@ -125,16 +127,9 @@ function enableHorizontalAlignmentProperty( schema, conversion ) {
 		model: {
 			name: 'tableCell',
 			key: 'horizontalAlignment',
-			values: [ 'left', 'right', 'center', 'justify' ]
+			values: [ 'right', 'center', 'justify' ]
 		},
 		view: {
-			// TODO: controversial one but I don't know if we want "default".
-			left: {
-				key: 'style',
-				value: {
-					'text-align': 'left'
-				}
-			},
 			right: {
 				key: 'style',
 				value: {
@@ -157,6 +152,53 @@ function enableHorizontalAlignmentProperty( schema, conversion ) {
 	} );
 }
 
+// Enables the `'verticalAlignment'` attribute for table cells.
+//
+// @param {module:engine/model/schema~Schema} schema
+// @param {module:engine/conversion/conversion~Conversion} conversion
+function enableVerticalAlignmentProperty( schema, conversion ) {
+	schema.extend( 'tableCell', {
+		allowAttributes: [ 'verticalAlignment' ]
+	} );
+
+	conversion.attributeToAttribute( {
+		model: {
+			name: 'tableCell',
+			key: 'verticalAlignment',
+			values: [ 'top', 'bottom' ]
+		},
+		view: {
+			top: {
+				key: 'style',
+				value: {
+					'vertical-align': 'top'
+				}
+			},
+			bottom: {
+				key: 'style',
+				value: {
+					'vertical-align': 'bottom'
+				}
+			}
+		}
+	} );
+
+	conversion.for( 'upcast' )
+		// Support for backwards compatibility and pasting from other sources.
+		.attributeToAttribute( {
+			view: {
+				attributes: {
+					valign: VALIGN_VALUES_REG_EXP
+				}
+			},
+			model: {
+				name: 'tableCell',
+				key: 'verticalAlignment',
+				value: viewElement => viewElement.getAttribute( 'valign' )
+			}
+		} );
+}
+
 // Enables conversion for an attribute for simple view-model mappings.
 //
 // @param {String} modelAttribute

+ 131 - 15
packages/ckeditor5-table/src/tablecellproperties/tablecellpropertiesui.js

@@ -14,11 +14,23 @@ import clickOutsideHandler from '@ckeditor/ckeditor5-ui/src/bindings/clickoutsid
 import ContextualBalloon from '@ckeditor/ckeditor5-ui/src/panel/balloon/contextualballoon';
 import TableCellPropertiesView from './ui/tablecellpropertiesview';
 import tableCellProperties from './../../theme/icons/table-cell-properties.svg';
-import { repositionContextualBalloon, getBalloonCellPositionData } from '../ui/utils';
-
-const DEFAULT_BORDER_STYLE = 'none';
-const DEFAULT_HORIZONTAL_ALIGNMENT = 'left';
-const DEFAULT_VERTICAL_ALIGNMENT = 'middle';
+import {
+	colorFieldValidator,
+	getBalloonCellPositionData,
+	getLocalizedColorErrorText,
+	getLocalizedLengthErrorText,
+	defaultColors,
+	lengthFieldValidator,
+	lineWidthFieldValidator,
+	repositionContextualBalloon
+} from '../ui/utils';
+import {
+	getLocalizedColorOptions,
+	normalizeColorOptions
+} from '@ckeditor/ckeditor5-ui/src/colorgrid/utils';
+import { debounce } from 'lodash-es';
+
+const ERROR_TEXT_TIMEOUT = 500;
 
 /**
  * The table cell properties UI plugin. It introduces the `'tableCellProperties'` button
@@ -44,6 +56,18 @@ export default class TableCellPropertiesUI extends Plugin {
 		return 'TableCellPropertiesUI';
 	}
 
+	/**
+	 * @inheritDoc
+	 */
+	constructor( editor ) {
+		super( editor );
+
+		editor.config.define( 'table.tableCellProperties', {
+			borderColors: defaultColors,
+			backgroundColors: defaultColors
+		} );
+	}
+
 	/**
 	 * @inheritDoc
 	 */
@@ -111,7 +135,16 @@ export default class TableCellPropertiesUI extends Plugin {
 	_createPropertiesView() {
 		const editor = this.editor;
 		const viewDocument = editor.editing.view.document;
-		const view = new TableCellPropertiesView( editor.locale );
+		const config = editor.config.get( 'table.tableCellProperties' );
+		const borderColorsConfig = normalizeColorOptions( config.borderColors );
+		const localizedBorderColors = getLocalizedColorOptions( editor.locale, borderColorsConfig );
+		const backgroundColorsConfig = normalizeColorOptions( config.backgroundColors );
+		const localizedBackgroundColors = getLocalizedColorOptions( editor.locale, backgroundColorsConfig );
+		const view = new TableCellPropertiesView( editor.locale, {
+			borderColors: localizedBorderColors,
+			backgroundColors: localizedBackgroundColors
+		} );
+		const t = editor.t;
 
 		// Render the view so its #element is available for the clickOutsideHandler.
 		view.render();
@@ -121,7 +154,11 @@ export default class TableCellPropertiesUI extends Plugin {
 		} );
 
 		this.listenTo( view, 'cancel', () => {
-			editor.execute( 'undo', this._undoStepBatch );
+			// https://github.com/ckeditor/ckeditor5/issues/6180
+			if ( this._undoStepBatch.operations.length ) {
+				editor.execute( 'undo', this._undoStepBatch );
+			}
+
 			this._hideView();
 		} );
 
@@ -148,15 +185,58 @@ export default class TableCellPropertiesUI extends Plugin {
 			callback: () => this._hideView()
 		} );
 
+		const colorErrorText = getLocalizedColorErrorText( t );
+		const lengthErrorText = getLocalizedLengthErrorText( t );
+
 		// Create the "UI -> editor data" binding.
 		// These listeners update the editor data (via table commands) when any observable
-		// property of the view has changed. This makes the view live, which means the changes are
+		// property of the view has changed. They also validate the value and display errors in the UI
+		// when necessary. This makes the view live, which means the changes are
 		// visible in the editing as soon as the user types or changes fields' values.
 		view.on( 'change:borderStyle', this._getPropertyChangeCallback( 'tableCellBorderStyle' ) );
-		view.on( 'change:borderColor', this._getPropertyChangeCallback( 'tableCellBorderColor' ) );
-		view.on( 'change:borderWidth', this._getPropertyChangeCallback( 'tableCellBorderWidth' ) );
-		view.on( 'change:padding', this._getPropertyChangeCallback( 'tableCellPadding' ) );
-		view.on( 'change:backgroundColor', this._getPropertyChangeCallback( 'tableCellBackgroundColor' ) );
+
+		view.on( 'change:borderColor', this._getValidatedPropertyChangeCallback( {
+			viewField: view.borderColorInput,
+			commandName: 'tableCellBorderColor',
+			errorText: colorErrorText,
+			validator: colorFieldValidator
+		} ) );
+
+		view.on( 'change:borderWidth', this._getValidatedPropertyChangeCallback( {
+			viewField: view.borderWidthInput,
+			commandName: 'tableCellBorderWidth',
+			errorText: lengthErrorText,
+			validator: lineWidthFieldValidator
+		} ) );
+
+		view.on( 'change:padding', this._getValidatedPropertyChangeCallback( {
+			viewField: view.paddingInput,
+			commandName: 'tableCellPadding',
+			errorText: lengthErrorText,
+			validator: lengthFieldValidator
+		} ) );
+
+		view.on( 'change:width', this._getValidatedPropertyChangeCallback( {
+			viewField: view.widthInput,
+			commandName: 'tableCellWidth',
+			errorText: lengthErrorText,
+			validator: lengthFieldValidator
+		} ) );
+
+		view.on( 'change:height', this._getValidatedPropertyChangeCallback( {
+			viewField: view.heightInput,
+			commandName: 'tableCellHeight',
+			errorText: lengthErrorText,
+			validator: lengthFieldValidator
+		} ) );
+
+		view.on( 'change:backgroundColor', this._getValidatedPropertyChangeCallback( {
+			viewField: view.backgroundInput,
+			commandName: 'tableCellBackgroundColor',
+			errorText: colorErrorText,
+			validator: colorFieldValidator
+		} ) );
+
 		view.on( 'change:horizontalAlignment', this._getPropertyChangeCallback( 'tableCellHorizontalAlignment' ) );
 		view.on( 'change:verticalAlignment', this._getPropertyChangeCallback( 'tableCellVerticalAlignment' ) );
 
@@ -177,13 +257,15 @@ export default class TableCellPropertiesUI extends Plugin {
 		const commands = this.editor.commands;
 
 		this.view.set( {
-			borderStyle: commands.get( 'tableCellBorderStyle' ).value || DEFAULT_BORDER_STYLE,
+			borderStyle: commands.get( 'tableCellBorderStyle' ).value || '',
 			borderColor: commands.get( 'tableCellBorderColor' ).value || '',
 			borderWidth: commands.get( 'tableCellBorderWidth' ).value || '',
+			width: commands.get( 'tableCellWidth' ).value || '',
+			height: commands.get( 'tableCellHeight' ).value || '',
 			padding: commands.get( 'tableCellPadding' ).value || '',
 			backgroundColor: commands.get( 'tableCellBackgroundColor' ).value || '',
-			horizontalAlignment: commands.get( 'tableCellHorizontalAlignment' ).value || DEFAULT_HORIZONTAL_ALIGNMENT,
-			verticalAlignment: commands.get( 'tableCellVerticalAlignment' ).value || DEFAULT_VERTICAL_ALIGNMENT,
+			horizontalAlignment: commands.get( 'tableCellHorizontalAlignment' ).value || '',
+			verticalAlignment: commands.get( 'tableCellVerticalAlignment' ).value || ''
 		} );
 	}
 
@@ -275,4 +357,38 @@ export default class TableCellPropertiesUI extends Plugin {
 			} );
 		};
 	}
+
+	/**
+	 * Creates a callback that when executed upon {@link #view view's} property change:
+	 * * executes a related editor command with the new property value if the value is valid,
+	 * * or sets the error text next to the invalid field, if the value did not pass the validation.
+	 *
+	 * @private
+	 * @param {Object} options
+	 * @param {String} options.commandName
+	 * @param {module:ui/view~View} options.viewField
+	 * @param {Function} options.validator
+	 * @param {String} options.errorText
+	 * @returns {Function}
+	 */
+	_getValidatedPropertyChangeCallback( { commandName, viewField, validator, errorText } ) {
+		const setErrorTextDebounced = debounce( () => {
+			viewField.errorText = errorText;
+		}, ERROR_TEXT_TIMEOUT );
+
+		return ( evt, propertyName, newValue ) => {
+			setErrorTextDebounced.cancel();
+
+			if ( validator( newValue ) ) {
+				this.editor.execute( commandName, {
+					value: newValue,
+					batch: this._undoStepBatch
+				} );
+
+				viewField.errorText = null;
+			} else {
+				setErrorTextDebounced();
+			}
+		};
+	}
 }

+ 248 - 59
packages/ckeditor5-table/src/tablecellproperties/ui/tablecellpropertiesview.js

@@ -21,7 +21,12 @@ import LabelView from '@ckeditor/ckeditor5-ui/src/label/labelview';
 import { addListToDropdown } from '@ckeditor/ckeditor5-ui/src/dropdown/utils';
 import ToolbarView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarview';
 import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
-import { fillToolbar, getBorderStyleDefinitions, getBorderStyleLabels } from '../../ui/utils';
+import {
+	fillToolbar,
+	getBorderStyleDefinitions,
+	getBorderStyleLabels,
+	getLabeledColorInputCreator,
+} from '../../ui/utils';
 import FormRowView from '../../ui/formrowview';
 import FormHeaderView from '../../ui/formheaderview';
 
@@ -57,48 +62,27 @@ const ALIGNMENT_ICONS = {
  */
 export default class TableCellPropertiesView extends View {
 	/**
-	 * @inheritDoc
+	 * @param {module:utils/locale~Locale} locale The {@link module:core/editor/editor~Editor#locale} instance.
+	 * @param {Object} options Additional configuration of the view.
+	 * @param {module:table/table~TableColorConfig} options.borderColors A configuration of the border
+	 * color palette used by the
+	 * {@link module:table/tablecellproperties/ui/tablecellpropertiesview~TableCellPropertiesView#borderColorInput}.
+	 * @param {module:table/table~TableColorConfig} options.backgroundColors A configuration of the background
+	 * color palette used by the
+	 * {@link module:table/tablecellproperties/ui/tablecellpropertiesview~TableCellPropertiesView#backgroundInput}.
 	 */
-	constructor( locale ) {
+	constructor( locale, options ) {
 		super( locale );
 
-		const { borderStyleDropdown, borderWidthInput, borderColorInput, borderRowLabel } = this._createBorderFields();
-		const { horizontalAlignmentToolbar, verticalAlignmentToolbar, alignmentLabel } = this._createAlignmentFields();
-		const { saveButtonView, cancelButtonView } = this._createActionButtons();
-
-		/**
-		 * Tracks information about the DOM focus in the form.
-		 *
-		 * @readonly
-		 * @member {module:utils/focustracker~FocusTracker}
-		 */
-		this.focusTracker = new FocusTracker();
-
-		/**
-		 * An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
-		 *
-		 * @readonly
-		 * @member {module:utils/keystrokehandler~KeystrokeHandler}
-		 */
-		this.keystrokes = new KeystrokeHandler();
-
-		/**
-		 * A collection of child views in the form.
-		 *
-		 * @readonly
-		 * @type {module:ui/viewcollection~ViewCollection}
-		 */
-		this.children = this.createCollection();
-
 		this.set( {
 			/**
 			 * The value of the cell border style.
 			 *
 			 * @observable
-			 * @default 'none'
+			 * @default ''
 			 * @member #borderStyle
 			 */
-			borderStyle: 'none',
+			borderStyle: '',
 
 			/**
 			 * The value of the cell border width style.
@@ -136,25 +120,78 @@ export default class TableCellPropertiesView extends View {
 			 */
 			backgroundColor: '',
 
+			/**
+			 * The value of the table cell width style.
+			 *
+			 * @observable
+			 * @default ''
+			 * @member #width
+			 */
+			width: '',
+
+			/**
+			 * The value of the table cell height style.
+			 *
+			 * @observable
+			 * @default ''
+			 * @member #height
+			 */
+			height: '',
+
 			/**
 			 * The value of the horizontal text alignment style.
 			 *
 			 * @observable
-			 * @default 'left'
+			 * @default ''
 			 * @member #horizontalAlignment
 			 */
-			horizontalAlignment: 'left',
+			horizontalAlignment: '',
 
 			/**
 			 * The value of the vertical text alignment style.
 			 *
 			 * @observable
-			 * @default 'middle'
+			 * @default ''
 			 * @member #verticalAlignment
 			 */
-			verticalAlignment: 'middle'
+			verticalAlignment: ''
 		} );
 
+		/**
+		 * Options passed to the view. See {@link #constructor} to learn more.
+		 *
+		 * @member {Object}
+		 */
+		this.options = options;
+
+		const { borderStyleDropdown, borderWidthInput, borderColorInput, borderRowLabel } = this._createBorderFields();
+		const { widthInput, operatorLabel, heightInput, dimensionsLabel } = this._createDimensionFields();
+		const { horizontalAlignmentToolbar, verticalAlignmentToolbar, alignmentLabel } = this._createAlignmentFields();
+
+		/**
+		 * Tracks information about the DOM focus in the form.
+		 *
+		 * @readonly
+		 * @member {module:utils/focustracker~FocusTracker}
+		 */
+		this.focusTracker = new FocusTracker();
+
+		/**
+		 * An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
+		 *
+		 * @readonly
+		 * @member {module:utils/keystrokehandler~KeystrokeHandler}
+		 */
+		this.keystrokes = new KeystrokeHandler();
+
+		/**
+		 * A collection of child views in the form.
+		 *
+		 * @readonly
+		 * @type {module:ui/viewcollection~ViewCollection}
+		 */
+		this.children = this.createCollection();
+
 		/**
 		 * A dropdown that allows selecting the style of the table cell border.
 		 *
@@ -175,7 +212,7 @@ export default class TableCellPropertiesView extends View {
 		 * An input that allows specifying the color of the table cell border.
 		 *
 		 * @readonly
-		 * @member {module:ui/inputtext/inputtextview~InputTextView}
+		 * @member {module:table/ui/colorinputview~ColorInputView}
 		 */
 		this.borderColorInput = borderColorInput;
 
@@ -183,7 +220,7 @@ export default class TableCellPropertiesView extends View {
 		 * An input that allows specifying the table cell background color.
 		 *
 		 * @readonly
-		 * @member {module:ui/inputtext/inputtextview~InputTextView}
+		 * @member {module:table/ui/colorinputview~ColorInputView}
 		 */
 		this.backgroundInput = this._createBackgroundField();
 
@@ -195,6 +232,22 @@ export default class TableCellPropertiesView extends View {
 		 */
 		this.paddingInput = this._createPaddingField();
 
+		/**
+		 * An input that allows specifying the table cell width.
+		 *
+		 * @readonly
+		 * @member {module:ui/inputtext/inputtextview~InputTextView}
+		 */
+		this.widthInput = widthInput;
+
+		/**
+		 * An input that allows specifying the table cell height.
+		 *
+		 * @readonly
+		 * @member {module:ui/inputtext/inputtextview~InputTextView}
+		 */
+		this.heightInput = heightInput;
+
 		/**
 		 * A toolbar with buttons that allow changing the horizontal text alignment in a table cell.
 		 *
@@ -211,6 +264,11 @@ export default class TableCellPropertiesView extends View {
 		 */
 		this.verticalAlignmentToolbar = verticalAlignmentToolbar;
 
+		// Defer creating to make sure other fields are present and the Save button can
+		// bind its #isEnabled to their error messages so there's no way to save unless all
+		// fields are valid.
+		const { saveButtonView, cancelButtonView } = this._createActionButtons();
+
 		/**
 		 * The "Save" button view.
 		 *
@@ -271,11 +329,34 @@ export default class TableCellPropertiesView extends View {
 			class: 'ck-table-form__border-row'
 		} ) );
 
-		// Background and padding row.
+		// Background.
+		this.children.add( new FormRowView( locale, {
+			children: [
+				this.backgroundInput
+			]
+		} ) );
+
+		// Dimensions row and padding.
 		this.children.add( new FormRowView( locale, {
 			children: [
-				this.backgroundInput,
-				this.paddingInput,
+				// Dimensions row.
+				new FormRowView( locale, {
+					labelView: dimensionsLabel,
+					children: [
+						dimensionsLabel,
+						widthInput,
+						operatorLabel,
+						heightInput
+					],
+					class: 'ck-table-form__dimensions-row'
+				} ),
+				// Padding row.
+				new FormRowView( locale, {
+					children: [
+						this.paddingInput
+					],
+					class: 'ck-table-cell-properties-form__padding-row'
+				} )
 			]
 		} ) );
 
@@ -332,6 +413,8 @@ export default class TableCellPropertiesView extends View {
 			this.borderColorInput,
 			this.borderWidthInput,
 			this.backgroundInput,
+			this.widthInput,
+			this.heightInput,
 			this.paddingInput,
 			this.horizontalAlignmentToolbar,
 			this.verticalAlignmentToolbar,
@@ -367,6 +450,10 @@ export default class TableCellPropertiesView extends View {
 	 * @returns {Object.<String,module:ui/view~View>}
 	 */
 	_createBorderFields() {
+		const colorInputCreator = getLabeledColorInputCreator( {
+			colorConfig: this.options.borderColors,
+			columns: 5
+		} );
 		const locale = this.locale;
 		const t = this.t;
 
@@ -391,7 +478,7 @@ export default class TableCellPropertiesView extends View {
 		} );
 
 		borderStyleDropdown.view.buttonView.bind( 'label' ).to( this, 'borderStyle', value => {
-			return styleLabels[ value ];
+			return styleLabels[ value ? value : 'none' ];
 		} );
 
 		borderStyleDropdown.view.on( 'execute', evt => {
@@ -406,28 +493,38 @@ export default class TableCellPropertiesView extends View {
 
 		borderWidthInput.set( {
 			label: t( 'Width' ),
-			class: 'ck-table-form__border-width',
+			class: 'ck-table-form__border-width'
 		} );
 
 		borderWidthInput.view.bind( 'value' ).to( this, 'borderWidth' );
-		borderWidthInput.bind( 'isEnabled' ).to( this, 'borderStyle', value => {
-			return value !== 'none';
-		} );
+		borderWidthInput.bind( 'isEnabled' ).to( this, 'borderStyle', isBorderStyleSet );
 		borderWidthInput.view.on( 'input', () => {
 			this.borderWidth = borderWidthInput.view.element.value;
 		} );
 
 		// -- Color ---------------------------------------------------
 
-		const borderColorInput = new LabeledView( locale, createLabeledInputText );
-		borderColorInput.label = t( 'Color' );
-		borderColorInput.view.bind( 'value' ).to( this, 'borderColor' );
-		borderColorInput.bind( 'isEnabled' ).to( this, 'borderStyle', value => {
-			return value !== 'none';
+		const borderColorInput = new LabeledView( locale, colorInputCreator );
+
+		borderColorInput.set( {
+			label: t( 'Color' ),
+			class: 'ck-table-form__border-color',
 		} );
 
+		borderColorInput.view.bind( 'value' ).to( this, 'borderColor' );
+		borderColorInput.bind( 'isEnabled' ).to( this, 'borderStyle', isBorderStyleSet );
+
 		borderColorInput.view.on( 'input', () => {
-			this.borderColor = borderColorInput.view.element.value;
+			this.borderColor = borderColorInput.view.value;
+		} );
+
+		// Reset the border color and width fields when style is "none".
+		// https://github.com/ckeditor/ckeditor5/issues/6227
+		this.on( 'change:borderStyle', ( evt, name, value ) => {
+			if ( !isBorderStyleSet( value ) ) {
+				this.borderColor = '';
+				this.borderWidth = '';
+			}
 		} );
 
 		return {
@@ -449,8 +546,12 @@ export default class TableCellPropertiesView extends View {
 	_createBackgroundField() {
 		const locale = this.locale;
 		const t = this.t;
+		const colorInputCreator = getLabeledColorInputCreator( {
+			colorConfig: this.options.backgroundColors,
+			columns: 5
+		} );
 
-		const backgroundInput = new LabeledView( locale, createLabeledInputText );
+		const backgroundInput = new LabeledView( locale, colorInputCreator );
 
 		backgroundInput.set( {
 			label: t( 'Background' ),
@@ -459,12 +560,81 @@ export default class TableCellPropertiesView extends View {
 
 		backgroundInput.view.bind( 'value' ).to( this, 'backgroundColor' );
 		backgroundInput.view.on( 'input', () => {
-			this.backgroundColor = backgroundInput.view.element.value;
+			this.backgroundColor = backgroundInput.view.value;
 		} );
 
 		return backgroundInput;
 	}
 
+	/**
+	 * Creates the following form fields:
+	 *
+	 * * {@link #widthInput}.
+	 * * {@link #heightInput}.
+	 *
+	 * @private
+	 * @returns {module:ui/labeledview/labeledview~LabeledView}
+	 */
+	_createDimensionFields() {
+		const locale = this.locale;
+		const t = this.t;
+
+		// -- Label ---------------------------------------------------
+
+		const dimensionsLabel = new LabelView( locale );
+		dimensionsLabel.text = t( 'Dimensions' );
+
+		// -- Width ---------------------------------------------------
+
+		const widthInput = new LabeledView( locale, createLabeledInputText );
+
+		widthInput.set( {
+			label: t( 'Width' ),
+			class: 'ck-table-form__dimensions-row__width',
+		} );
+
+		widthInput.view.bind( 'value' ).to( this, 'width' );
+		widthInput.view.on( 'input', () => {
+			this.width = widthInput.view.element.value;
+		} );
+
+		// -- Operator ---------------------------------------------------
+
+		const operatorLabel = new View( locale );
+		operatorLabel.setTemplate( {
+			tag: 'span',
+			attributes: {
+				class: [
+					'ck-table-form__dimension-operator'
+				]
+			},
+			children: [
+				{ text: '×' }
+			]
+		} );
+
+		// -- Height ---------------------------------------------------
+
+		const heightInput = new LabeledView( locale, createLabeledInputText );
+
+		heightInput.set( {
+			label: t( 'Height' ),
+			class: 'ck-table-form__dimensions-row__height',
+		} );
+
+		heightInput.view.bind( 'value' ).to( this, 'height' );
+		heightInput.view.on( 'input', () => {
+			this.height = heightInput.view.element.value;
+		} );
+
+		return {
+			dimensionsLabel,
+			widthInput,
+			operatorLabel,
+			heightInput
+		};
+	}
+
 	/**
 	 * Creates the following form fields:
 	 *
@@ -507,7 +677,7 @@ export default class TableCellPropertiesView extends View {
 
 		const alignmentLabel = new LabelView( locale );
 
-		alignmentLabel.text = t( 'Text alignment' );
+		alignmentLabel.text = t( 'Table cell text alignment' );
 
 		// -- Horizontal ---------------------------------------------------
 
@@ -523,7 +693,10 @@ export default class TableCellPropertiesView extends View {
 			icons: ALIGNMENT_ICONS,
 			toolbar: horizontalAlignmentToolbar,
 			labels: this._horizontalAlignmentLabels,
-			propertyName: 'horizontalAlignment'
+			propertyName: 'horizontalAlignment',
+			nameToValue: name => {
+				return name === 'left' ? '' : name;
+			}
 		} );
 
 		// -- Vertical -----------------------------------------------------
@@ -540,7 +713,10 @@ export default class TableCellPropertiesView extends View {
 			icons: ALIGNMENT_ICONS,
 			toolbar: verticalAlignmentToolbar,
 			labels: this._verticalAlignmentLabels,
-			propertyName: 'verticalAlignment'
+			propertyName: 'verticalAlignment',
+			nameToValue: name => {
+				return name === 'middle' ? '' : name;
+			}
 		} );
 
 		return {
@@ -562,9 +738,14 @@ export default class TableCellPropertiesView extends View {
 	_createActionButtons() {
 		const locale = this.locale;
 		const t = this.t;
-
 		const saveButtonView = new ButtonView( locale );
 		const cancelButtonView = new ButtonView( locale );
+		const fieldsThatShouldValidateToSave = [
+			this.borderWidthInput,
+			this.borderColorInput,
+			this.backgroundInput,
+			this.paddingInput
+		];
 
 		saveButtonView.set( {
 			label: t( 'Save' ),
@@ -574,6 +755,10 @@ export default class TableCellPropertiesView extends View {
 			withText: true,
 		} );
 
+		saveButtonView.bind( 'isEnabled' ).toMany( fieldsThatShouldValidateToSave, 'errorText', ( ...errorTexts ) => {
+			return errorTexts.every( errorText => !errorText );
+		} );
+
 		cancelButtonView.set( {
 			label: t( 'Cancel' ),
 			icon: cancelIcon,
@@ -622,3 +807,7 @@ export default class TableCellPropertiesView extends View {
 		};
 	}
 }
+
+function isBorderStyleSet( value ) {
+	return !!value;
+}

+ 40 - 0
packages/ckeditor5-table/src/tableproperties.js

@@ -36,3 +36,43 @@ export default class TableProperties extends Plugin {
 		return [ TablePropertiesEditing, TablePropertiesUI ];
 	}
 }
+
+/**
+ * A configuration of the table properties user interface (balloon). It allows to define:
+ *
+ * * the color palette for the table border color style field (`tableProperties.borderColors`),
+ * * the color palette for the table background style field (`tableProperties.backgroundColors`).
+ *
+ *		const tableConfig = {
+ *			tableProperties: {
+ *				borderColors: [
+ *					{
+ *						color: 'hsl(0, 0%, 90%)',
+ *						label: 'Light grey'
+ *					},
+ *					// ...
+ *				],
+ *				backgroundColors: [
+ *					{
+ *						color: 'hsl(120, 75%, 60%)',
+ *						label: 'Green'
+ *					},
+ *					// ...
+ *				]
+ *			}
+ *		};
+ *
+ * **Note**: The configurations do not impact the data being loaded into the editor,
+ * i.e. they do not limit or filter the colors in the data. They are used only in the user interface
+ * allowing users to pick colors in a more convenient way.
+ *
+ * The default color palettes for the table background and the table border are the same
+ * ({@link module:table/ui/utils~defaultColors check out their content}).
+ *
+ * Both color palette configurations must follow the
+ * {@link module:table/table~TableColorConfig table color configuration format}.
+ *
+ * Read more about configuring the table feature in {@link module:table/table~TableConfig}.
+ *
+ * @member {Object} module:table/table~TableConfig#tableProperties
+ */

+ 16 - 1
packages/ckeditor5-table/src/tableproperties/commands/tableborderwidthcommand.js

@@ -7,8 +7,8 @@
  * @module table/tableproperties/commands/tableborderwidthcommand
  */
 
+import { addDefaultUnitToNumericValue, getSingleValue } from '../../commands/utils';
 import TablePropertyCommand from './tablepropertycommand';
-import { getSingleValue } from '../../commands/utils';
 
 /**
  * The table width border command.
@@ -22,6 +22,14 @@ import { getSingleValue } from '../../commands/utils';
  *			value: '5px'
  *		} );
  *
+ * **Note**: This command adds the default `'px'` unit to numeric values. Executing:
+ *
+ *		editor.execute( 'tableBorderWidth', {
+ *			value: '5'
+ *		} );
+ *
+ * Will set the `borderWidth` attribute to `'5px'` in the model.
+ *
  * @extends module:table/tableproperties/commands/tablepropertycommand
  */
 export default class TableBorderWidthCommand extends TablePropertyCommand {
@@ -44,4 +52,11 @@ export default class TableBorderWidthCommand extends TablePropertyCommand {
 
 		return getSingleValue( table.getAttribute( this.attributeName ) );
 	}
+
+	/**
+	 * @inheritDoc
+	 */
+	_getValueToSet( value ) {
+		return addDefaultUnitToNumericValue( value, 'px' );
+	}
 }

+ 16 - 0
packages/ckeditor5-table/src/tableproperties/commands/tableheightcommand.js

@@ -8,6 +8,7 @@
  */
 
 import TablePropertyCommand from './tablepropertycommand';
+import { addDefaultUnitToNumericValue } from '../../commands/utils';
 
 /**
  * The table height command.
@@ -21,6 +22,14 @@ import TablePropertyCommand from './tablepropertycommand';
  *			value: '500px'
  *		} );
  *
+ * **Note**: This command adds the default `'px'` unit to numeric values. Executing:
+ *
+ *		editor.execute( 'tableHeight', {
+ *			value: '50'
+ *		} );
+ *
+ * Will set the `height` attribute to `'50px'` in the model.
+ *
  * @extends module:core/command~Command
  */
 export default class TableHeightCommand extends TablePropertyCommand {
@@ -32,4 +41,11 @@ export default class TableHeightCommand extends TablePropertyCommand {
 	constructor( editor ) {
 		super( editor, 'height' );
 	}
+
+	/**
+	 * @inheritDoc
+	 */
+	_getValueToSet( value ) {
+		return addDefaultUnitToNumericValue( value, 'px' );
+	}
 }

+ 14 - 2
packages/ckeditor5-table/src/tableproperties/commands/tablepropertycommand.js

@@ -61,10 +61,11 @@ export default class TablePropertyCommand extends Command {
 		const { value, batch } = options;
 
 		const table = findAncestor( 'table', selection.getFirstPosition() );
+		const valueToSet = this._getValueToSet( value );
 
 		model.enqueueChange( batch || 'default', writer => {
-			if ( value ) {
-				writer.setAttribute( this.attributeName, value, table );
+			if ( valueToSet ) {
+				writer.setAttribute( this.attributeName, valueToSet, table );
 			} else {
 				writer.removeAttribute( this.attributeName, table );
 			}
@@ -85,4 +86,15 @@ export default class TablePropertyCommand extends Command {
 
 		return table.getAttribute( this.attributeName );
 	}
+
+	/**
+	 * Returns the proper model value. Can be used to add a default unit to numeric values.
+	 *
+	 * @private
+	 * @param {*} value
+	 * @returns {*}
+	 */
+	_getValueToSet( value ) {
+		return value;
+	}
 }

+ 16 - 0
packages/ckeditor5-table/src/tableproperties/commands/tablewidthcommand.js

@@ -7,6 +7,7 @@
  * @module table/tableproperties/commands/tablewidthcommand
  */
 
+import { addDefaultUnitToNumericValue } from '../../commands/utils';
 import TablePropertyCommand from './tablepropertycommand';
 
 /**
@@ -21,6 +22,14 @@ import TablePropertyCommand from './tablepropertycommand';
  *			value: '400px'
  *		} );
  *
+ * **Note**: This command adds the default `'px'` unit to numeric values. Executing:
+ *
+ *		editor.execute( 'tableWidth', {
+ *			value: '50'
+ *		} );
+ *
+ * Will set the `width` attribute to `'50px'` in the model.
+ *
  * @extends module:table/tableproperties/commands/tablepropertycommand
  */
 export default class TableWidthCommand extends TablePropertyCommand {
@@ -32,4 +41,11 @@ export default class TableWidthCommand extends TablePropertyCommand {
 	constructor( editor ) {
 		super( editor, 'width' );
 	}
+
+	/**
+	 * @inheritDoc
+	 */
+	_getValueToSet( value ) {
+		return addDefaultUnitToNumericValue( value, 'px' );
+	}
 }

+ 43 - 53
packages/ckeditor5-table/src/tableproperties/tablepropertiesediting.js

@@ -11,8 +11,13 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import { addBorderRules } from '@ckeditor/ckeditor5-engine/src/view/styles/border';
 import { addBackgroundRules } from '@ckeditor/ckeditor5-engine/src/view/styles/background';
 
-import TableEditing from './../tableediting';
-import { downcastTableAttribute, upcastStyleToAttribute, upcastBorderStyles } from './../converters/tableproperties';
+import TableEditing from '../tableediting';
+import {
+	downcastAttributeToStyle,
+	downcastTableAttribute,
+	upcastBorderStyles,
+	upcastStyleToAttribute
+} from '../converters/tableproperties';
 import TableBackgroundColorCommand from './commands/tablebackgroundcolorcommand';
 import TableBorderColorCommand from './commands/tablebordercolorcommand';
 import TableBorderStyleCommand from './commands/tableborderstylecommand';
@@ -21,9 +26,7 @@ import TableWidthCommand from './commands/tablewidthcommand';
 import TableHeightCommand from './commands/tableheightcommand';
 import TableAlignmentCommand from './commands/tablealignmentcommand';
 
-// RegExp used for matching margin style in converters.
-const MARGIN_REG_EXP = /^(auto|0(%|[a-z]{2,4})?)$/;
-const ALIGN_VALUES_REG_EXP = /^(left|right|center)$/;
+const ALIGN_VALUES_REG_EXP = /^(left|right)$/;
 
 /**
  * The table properties editing feature.
@@ -77,10 +80,10 @@ export default class TablePropertiesEditing extends Plugin {
 		enableAlignmentProperty( schema, conversion );
 		editor.commands.add( 'tableAlignment', new TableAlignmentCommand( editor ) );
 
-		enableProperty( schema, conversion, 'width', 'width' );
+		enableTableToFigureProperty( schema, conversion, 'width', 'width' );
 		editor.commands.add( 'tableWidth', new TableWidthCommand( editor ) );
 
-		enableProperty( schema, conversion, 'height', 'height' );
+		enableTableToFigureProperty( schema, conversion, 'height', 'height' );
 		editor.commands.add( 'tableHeight', new TableHeightCommand( editor ) );
 
 		viewDoc.addStyleProcessorRules( addBackgroundRules );
@@ -111,31 +114,32 @@ function enableAlignmentProperty( schema, conversion ) {
 	schema.extend( 'table', {
 		allowAttributes: [ 'alignment' ]
 	} );
-	conversion.for( 'upcast' )
+
+	conversion
 		.attributeToAttribute( {
-			view: {
-				styles: {
-					'margin-right': MARGIN_REG_EXP,
-					'margin-left': MARGIN_REG_EXP
-				}
-			},
 			model: {
 				name: 'table',
 				key: 'alignment',
-				value: viewElement => {
-					// At this point we only have auto or 0 value (with a unit).
-					if ( viewElement.getStyle( 'margin-right' ) != 'auto' ) {
-						return 'right';
+				values: [ 'left', 'right' ]
+			},
+			view: {
+				left: {
+					key: 'style',
+					value: {
+						float: 'left'
 					}
-
-					if ( viewElement.getStyle( 'margin-left' ) != 'auto' ) {
-						return 'left';
+				},
+				right: {
+					key: 'style',
+					value: {
+						float: 'right'
 					}
-
-					return 'center';
 				}
-			}
-		} )
+			},
+			converterPriority: 'high'
+		} );
+
+	conversion.for( 'upcast' )
 		// Support for backwards compatibility and pasting from other sources.
 		.attributeToAttribute( {
 			view: {
@@ -149,34 +153,6 @@ function enableAlignmentProperty( schema, conversion ) {
 				value: viewElement => viewElement.getAttribute( 'align' )
 			}
 		} );
-	conversion.for( 'downcast' ).add( dispatcher => dispatcher.on( 'attribute:alignment:table', ( evt, data, conversionApi ) => {
-		const { item, attributeNewValue } = data;
-		const { mapper, writer } = conversionApi;
-
-		const table = [ ...mapper.toViewElement( item ).getChildren() ].find( child => child.is( 'table' ) );
-
-		if ( !attributeNewValue ) {
-			writer.removeStyle( 'margin-left', table );
-			writer.removeStyle( 'margin-right', table );
-
-			return;
-		}
-
-		const styles = {
-			'margin-right': 'auto',
-			'margin-left': 'auto'
-		};
-
-		if ( attributeNewValue == 'left' ) {
-			styles[ 'margin-left' ] = '0';
-		}
-
-		if ( attributeNewValue == 'right' ) {
-			styles[ 'margin-right' ] = '0';
-		}
-
-		writer.setStyle( styles, table );
-	} ) );
 }
 
 // Enables conversion for an attribute for simple view-model mappings.
@@ -192,3 +168,17 @@ function enableProperty( schema, conversion, modelAttribute, styleName ) {
 	upcastStyleToAttribute( conversion, 'table', modelAttribute, styleName );
 	downcastTableAttribute( conversion, modelAttribute, styleName );
 }
+
+// Enables conversion for an attribute for simple view (figure) to model (table) mappings.
+//
+// @param {String} modelAttribute
+// @param {String} styleName
+// @param {module:engine/model/schema~Schema} schema
+// @param {module:engine/conversion/conversion~Conversion} conversion
+function enableTableToFigureProperty( schema, conversion, modelAttribute, styleName ) {
+	schema.extend( 'table', {
+		allowAttributes: [ modelAttribute ]
+	} );
+	upcastStyleToAttribute( conversion, 'table', modelAttribute, styleName );
+	downcastAttributeToStyle( conversion, 'table', modelAttribute, styleName );
+}

+ 117 - 13
packages/ckeditor5-table/src/tableproperties/tablepropertiesui.js

@@ -15,12 +15,22 @@ import ContextualBalloon from '@ckeditor/ckeditor5-ui/src/panel/balloon/contextu
 import TablePropertiesView from './ui/tablepropertiesview';
 import tableProperties from './../../theme/icons/table-properties.svg';
 import {
+	colorFieldValidator,
+	getBalloonTablePositionData,
+	getLocalizedColorErrorText,
+	getLocalizedLengthErrorText,
+	lengthFieldValidator,
+	lineWidthFieldValidator,
 	repositionContextualBalloon,
-	getBalloonTablePositionData
+	defaultColors,
 } from '../ui/utils';
+import {
+	getLocalizedColorOptions,
+	normalizeColorOptions
+} from '@ckeditor/ckeditor5-ui/src/colorgrid/utils';
+import { debounce } from 'lodash-es';
 
-const DEFAULT_BORDER_STYLE = 'none';
-const DEFAULT_ALIGNMENT = 'center';
+const ERROR_TEXT_TIMEOUT = 500;
 
 /**
  * The table properties UI plugin. It introduces the `'tableProperties'` button
@@ -46,6 +56,18 @@ export default class TablePropertiesUI extends Plugin {
 		return 'TablePropertiesUI';
 	}
 
+	/**
+	 * @inheritDoc
+	 */
+	constructor( editor ) {
+		super( editor );
+
+		editor.config.define( 'table.tableProperties', {
+			borderColors: defaultColors,
+			backgroundColors: defaultColors
+		} );
+	}
+
 	/**
 	 * @inheritDoc
 	 */
@@ -113,7 +135,16 @@ export default class TablePropertiesUI extends Plugin {
 	_createPropertiesView() {
 		const editor = this.editor;
 		const viewDocument = editor.editing.view.document;
-		const view = new TablePropertiesView( editor.locale );
+		const config = editor.config.get( 'table.tableProperties' );
+		const borderColorsConfig = normalizeColorOptions( config.borderColors );
+		const localizedBorderColors = getLocalizedColorOptions( editor.locale, borderColorsConfig );
+		const backgroundColorsConfig = normalizeColorOptions( config.backgroundColors );
+		const localizedBackgroundColors = getLocalizedColorOptions( editor.locale, backgroundColorsConfig );
+		const view = new TablePropertiesView( editor.locale, {
+			borderColors: localizedBorderColors,
+			backgroundColors: localizedBackgroundColors
+		} );
+		const t = editor.t;
 
 		// Render the view so its #element is available for the clickOutsideHandler.
 		view.render();
@@ -123,7 +154,11 @@ export default class TablePropertiesUI extends Plugin {
 		} );
 
 		this.listenTo( view, 'cancel', () => {
-			editor.execute( 'undo', this._undoStepBatch );
+			// https://github.com/ckeditor/ckeditor5/issues/6180
+			if ( this._undoStepBatch.operations.length ) {
+				editor.execute( 'undo', this._undoStepBatch );
+			}
+
 			this._hideView();
 		} );
 
@@ -150,16 +185,51 @@ export default class TablePropertiesUI extends Plugin {
 			callback: () => this._hideView()
 		} );
 
+		const colorErrorText = getLocalizedColorErrorText( t );
+		const lengthErrorText = getLocalizedLengthErrorText( t );
+
 		// Create the "UI -> editor data" binding.
 		// These listeners update the editor data (via table commands) when any observable
-		// property of the view has changed. This makes the view live, which means the changes are
+		// property of the view has changed. They also validate the value and display errors in the UI
+		// when necessary. This makes the view live, which means the changes are
 		// visible in the editing as soon as the user types or changes fields' values.
 		view.on( 'change:borderStyle', this._getPropertyChangeCallback( 'tableBorderStyle' ) );
-		view.on( 'change:borderColor', this._getPropertyChangeCallback( 'tableBorderColor' ) );
-		view.on( 'change:borderWidth', this._getPropertyChangeCallback( 'tableBorderWidth' ) );
-		view.on( 'change:backgroundColor', this._getPropertyChangeCallback( 'tableBackgroundColor' ) );
-		view.on( 'change:width', this._getPropertyChangeCallback( 'tableWidth' ) );
-		view.on( 'change:height', this._getPropertyChangeCallback( 'tableHeight' ) );
+
+		view.on( 'change:borderColor', this._getValidatedPropertyChangeCallback( {
+			viewField: view.borderColorInput,
+			commandName: 'tableBorderColor',
+			errorText: colorErrorText,
+			validator: colorFieldValidator
+		} ) );
+
+		view.on( 'change:borderWidth', this._getValidatedPropertyChangeCallback( {
+			viewField: view.borderWidthInput,
+			commandName: 'tableBorderWidth',
+			errorText: lengthErrorText,
+			validator: lineWidthFieldValidator
+		} ) );
+
+		view.on( 'change:backgroundColor', this._getValidatedPropertyChangeCallback( {
+			viewField: view.backgroundInput,
+			commandName: 'tableBackgroundColor',
+			errorText: colorErrorText,
+			validator: colorFieldValidator
+		} ) );
+
+		view.on( 'change:width', this._getValidatedPropertyChangeCallback( {
+			viewField: view.widthInput,
+			commandName: 'tableWidth',
+			errorText: lengthErrorText,
+			validator: lengthFieldValidator
+		} ) );
+
+		view.on( 'change:height', this._getValidatedPropertyChangeCallback( {
+			viewField: view.heightInput,
+			commandName: 'tableHeight',
+			errorText: lengthErrorText,
+			validator: lengthFieldValidator
+		} ) );
+
 		view.on( 'change:alignment', this._getPropertyChangeCallback( 'tableAlignment' ) );
 
 		return view;
@@ -179,13 +249,13 @@ export default class TablePropertiesUI extends Plugin {
 		const commands = this.editor.commands;
 
 		this.view.set( {
-			borderStyle: commands.get( 'tableBorderStyle' ).value || DEFAULT_BORDER_STYLE,
+			borderStyle: commands.get( 'tableBorderStyle' ).value || '',
 			borderColor: commands.get( 'tableBorderColor' ).value || '',
 			borderWidth: commands.get( 'tableBorderWidth' ).value || '',
 			backgroundColor: commands.get( 'tableBackgroundColor' ).value || '',
 			width: commands.get( 'tableWidth' ).value || '',
 			height: commands.get( 'tableHeight' ).value || '',
-			alignment: commands.get( 'tableAlignment' ).value || DEFAULT_ALIGNMENT,
+			alignment: commands.get( 'tableAlignment' ).value || '',
 		} );
 	}
 
@@ -277,4 +347,38 @@ export default class TablePropertiesUI extends Plugin {
 			} );
 		};
 	}
+
+	/**
+	 * Creates a callback that when executed upon {@link #view view's} property change:
+	 * * executes a related editor command with the new property value if the value is valid,
+	 * * or sets the error text next to the invalid field, if the value did not pass the validation.
+	 *
+	 * @private
+	 * @param {Object} options
+	 * @param {String} options.commandName
+	 * @param {module:ui/view~View} options.viewField
+	 * @param {Function} options.validator
+	 * @param {String} options.errorText
+	 * @returns {Function}
+	 */
+	_getValidatedPropertyChangeCallback( { commandName, viewField, validator, errorText } ) {
+		const setErrorTextDebounced = debounce( () => {
+			viewField.errorText = errorText;
+		}, ERROR_TEXT_TIMEOUT );
+
+		return ( evt, propertyName, newValue ) => {
+			setErrorTextDebounced.cancel();
+
+			if ( validator( newValue ) ) {
+				this.editor.execute( commandName, {
+					value: newValue,
+					batch: this._undoStepBatch
+				} );
+
+				viewField.errorText = null;
+			} else {
+				setErrorTextDebounced();
+			}
+		};
+	}
 }

+ 117 - 57
packages/ckeditor5-table/src/tableproperties/ui/tablepropertiesview.js

@@ -21,7 +21,12 @@ import LabelView from '@ckeditor/ckeditor5-ui/src/label/labelview';
 import { addListToDropdown } from '@ckeditor/ckeditor5-ui/src/dropdown/utils';
 import ToolbarView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarview';
 import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
-import { fillToolbar, getBorderStyleDefinitions, getBorderStyleLabels } from '../../ui/utils';
+import {
+	fillToolbar,
+	getBorderStyleDefinitions,
+	getBorderStyleLabels,
+	getLabeledColorInputCreator
+} from '../../ui/utils';
 import FormRowView from '../../ui/formrowview';
 import FormHeaderView from '../../ui/formheaderview';
 
@@ -49,49 +54,27 @@ const ALIGNMENT_ICONS = {
  */
 export default class TablePropertiesView extends View {
 	/**
-	 * @inheritDoc
+	 * @param {module:utils/locale~Locale} locale The {@link module:core/editor/editor~Editor#locale} instance.
+	 * @param {Object} options Additional configuration of the view.
+	 * @param {module:table/table~TableColorConfig} options.borderColors A configuration of the border
+	 * color palette used by the
+	 * {@link module:table/tableproperties/ui/tablepropertiesview~TablePropertiesView#borderColorInput}.
+	 * @param {module:table/table~TableColorConfig} options.backgroundColors A configuration of the background
+	 * color palette used by the
+	 * {@link module:table/tableproperties/ui/tablepropertiesview~TablePropertiesView#backgroundInput}.
 	 */
-	constructor( locale ) {
+	constructor( locale, options ) {
 		super( locale );
 
-		const { borderStyleDropdown, borderWidthInput, borderColorInput, borderRowLabel } = this._createBorderFields();
-		const { widthInput, operatorLabel, heightInput, dimensionsLabel } = this._createDimensionFields();
-		const { alignmentToolbar, alignmentLabel } = this._createAlignmentFields();
-		const { saveButtonView, cancelButtonView } = this._createActionButtons();
-
-		/**
-		 * Tracks information about the DOM focus in the form.
-		 *
-		 * @readonly
-		 * @member {module:utils/focustracker~FocusTracker}
-		 */
-		this.focusTracker = new FocusTracker();
-
-		/**
-		 * An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
-		 *
-		 * @readonly
-		 * @member {module:utils/keystrokehandler~KeystrokeHandler}
-		 */
-		this.keystrokes = new KeystrokeHandler();
-
-		/**
-		 * A collection of child views in the form.
-		 *
-		 * @readonly
-		 * @type {module:ui/viewcollection~ViewCollection}
-		 */
-		this.children = this.createCollection();
-
 		this.set( {
 			/**
 			 * The value of the border style.
 			 *
 			 * @observable
-			 * @default 'none'
+			 * @default ''
 			 * @member #borderStyle
 			 */
-			borderStyle: 'none',
+			borderStyle: '',
 
 			/**
 			 * The value of the border width style.
@@ -142,12 +125,48 @@ export default class TablePropertiesView extends View {
 			 * The value of the table alignment style.
 			 *
 			 * @observable
-			 * @default 'center'
+			 * @default ''
 			 * @member #alignment
 			 */
-			alignment: 'center',
+			alignment: ''
 		} );
 
+		/**
+		 * Options passed to the view. See {@link #constructor} to learn more.
+		 *
+		 * @protected
+		 * @member {Object}
+		 */
+		this.options = options;
+
+		const { borderStyleDropdown, borderWidthInput, borderColorInput, borderRowLabel } = this._createBorderFields();
+		const { widthInput, operatorLabel, heightInput, dimensionsLabel } = this._createDimensionFields();
+		const { alignmentToolbar, alignmentLabel } = this._createAlignmentFields();
+
+		/**
+		 * Tracks information about the DOM focus in the form.
+		 *
+		 * @readonly
+		 * @member {module:utils/focustracker~FocusTracker}
+		 */
+		this.focusTracker = new FocusTracker();
+
+		/**
+		 * An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
+		 *
+		 * @readonly
+		 * @member {module:utils/keystrokehandler~KeystrokeHandler}
+		 */
+		this.keystrokes = new KeystrokeHandler();
+
+		/**
+		 * A collection of child views in the form.
+		 *
+		 * @readonly
+		 * @type {module:ui/viewcollection~ViewCollection}
+		 */
+		this.children = this.createCollection();
+
 		/**
 		 * A dropdown that allows selecting the style of the table border.
 		 *
@@ -168,7 +187,7 @@ export default class TablePropertiesView extends View {
 		 * An input that allows specifying the color of the table border.
 		 *
 		 * @readonly
-		 * @member {module:ui/inputtext/inputtextview~InputTextView}
+		 * @member {module:table/ui/colorinputview~ColorInputView}
 		 */
 		this.borderColorInput = borderColorInput;
 
@@ -176,7 +195,7 @@ export default class TablePropertiesView extends View {
 		 * An input that allows specifying the table background color.
 		 *
 		 * @readonly
-		 * @member {module:ui/inputtext/inputtextview~InputTextView}
+		 * @member {module:table/ui/colorinputview~ColorInputView}
 		 */
 		this.backgroundInput = this._createBackgroundField();
 
@@ -204,6 +223,11 @@ export default class TablePropertiesView extends View {
 		 */
 		this.alignmentToolbar = alignmentToolbar;
 
+		// Defer creating to make sure other fields are present and the Save button can
+		// bind its #isEnabled to their error messages so there's no way to save unless all
+		// fields are valid.
+		const { saveButtonView, cancelButtonView } = this._createActionButtons();
+
 		/**
 		 * The "Save" button view.
 		 *
@@ -282,7 +306,7 @@ export default class TablePropertiesView extends View {
 						operatorLabel,
 						heightInput
 					],
-					class: 'ck-table-properties-form__dimensions-row'
+					class: 'ck-table-form__dimensions-row'
 				} ),
 				// Alignment row.
 				new FormRowView( locale, {
@@ -373,6 +397,10 @@ export default class TablePropertiesView extends View {
 	 * @returns {Object.<String,module:ui/view~View>}
 	 */
 	_createBorderFields() {
+		const colorInputCreator = getLabeledColorInputCreator( {
+			colorConfig: this.options.borderColors,
+			columns: 5
+		} );
 		const locale = this.locale;
 		const t = this.t;
 
@@ -397,7 +425,7 @@ export default class TablePropertiesView extends View {
 		} );
 
 		borderStyleDropdown.view.buttonView.bind( 'label' ).to( this, 'borderStyle', value => {
-			return styleLabels[ value ];
+			return styleLabels[ value ? value : 'none' ];
 		} );
 
 		borderStyleDropdown.view.on( 'execute', evt => {
@@ -412,35 +440,45 @@ export default class TablePropertiesView extends View {
 
 		borderWidthInput.set( {
 			label: t( 'Width' ),
-			class: 'ck-table-form__border-width',
+			class: 'ck-table-form__border-width'
 		} );
 
 		borderWidthInput.view.bind( 'value' ).to( this, 'borderWidth' );
-		borderWidthInput.bind( 'isEnabled' ).to( this, 'borderStyle', value => {
-			return value !== 'none';
-		} );
+		borderWidthInput.bind( 'isEnabled' ).to( this, 'borderStyle', isBorderStyleSet );
 		borderWidthInput.view.on( 'input', () => {
 			this.borderWidth = borderWidthInput.view.element.value;
 		} );
 
 		// -- Color ---------------------------------------------------
 
-		const borderColorInput = new LabeledView( locale, createLabeledInputText );
-		borderColorInput.label = t( 'Color' );
-		borderColorInput.view.bind( 'value' ).to( this, 'borderColor' );
-		borderColorInput.bind( 'isEnabled' ).to( this, 'borderStyle', value => {
-			return value !== 'none';
+		const borderColorInput = new LabeledView( locale, colorInputCreator );
+
+		borderColorInput.set( {
+			label: t( 'Color' ),
+			class: 'ck-table-form__border-color',
 		} );
 
+		borderColorInput.view.bind( 'value' ).to( this, 'borderColor' );
+		borderColorInput.bind( 'isEnabled' ).to( this, 'borderStyle', isBorderStyleSet );
+
 		borderColorInput.view.on( 'input', () => {
-			this.borderColor = borderColorInput.view.element.value;
+			this.borderColor = borderColorInput.view.value;
+		} );
+
+		// Reset the border color and width fields when style is "none".
+		// https://github.com/ckeditor/ckeditor5/issues/6227
+		this.on( 'change:borderStyle', ( evt, name, value ) => {
+			if ( !isBorderStyleSet( value ) ) {
+				this.borderColor = '';
+				this.borderWidth = '';
+			}
 		} );
 
 		return {
 			borderRowLabel,
 			borderStyleDropdown,
 			borderColorInput,
-			borderWidthInput,
+			borderWidthInput
 		};
 	}
 
@@ -453,10 +491,14 @@ export default class TablePropertiesView extends View {
 	 * @returns {module:ui/labeledview/labeledview~LabeledView}
 	 */
 	_createBackgroundField() {
+		const backgroundInputCreator = getLabeledColorInputCreator( {
+			colorConfig: this.options.backgroundColors,
+			columns: 5
+		} );
 		const locale = this.locale;
 		const t = this.t;
 
-		const backgroundInput = new LabeledView( locale, createLabeledInputText );
+		const backgroundInput = new LabeledView( locale, backgroundInputCreator );
 
 		backgroundInput.set( {
 			label: t( 'Background' ),
@@ -465,7 +507,7 @@ export default class TablePropertiesView extends View {
 
 		backgroundInput.view.bind( 'value' ).to( this, 'backgroundColor' );
 		backgroundInput.view.on( 'input', () => {
-			this.backgroundColor = backgroundInput.view.element.value;
+			this.backgroundColor = backgroundInput.view.value;
 		} );
 
 		return backgroundInput;
@@ -495,7 +537,7 @@ export default class TablePropertiesView extends View {
 
 		widthInput.set( {
 			label: t( 'Width' ),
-			class: 'ck-table-properties-form__width',
+			class: 'ck-table-form__dimensions-row__width',
 		} );
 
 		widthInput.view.bind( 'value' ).to( this, 'width' );
@@ -510,7 +552,7 @@ export default class TablePropertiesView extends View {
 			tag: 'span',
 			attributes: {
 				class: [
-					'ck-table-properties-form__dimension-operator'
+					'ck-table-form__dimension-operator'
 				]
 			},
 			children: [
@@ -524,7 +566,7 @@ export default class TablePropertiesView extends View {
 
 		heightInput.set( {
 			label: t( 'Height' ),
-			class: 'ck-table-properties-form__height',
+			class: 'ck-table-form__dimensions-row__height',
 		} );
 
 		heightInput.view.bind( 'value' ).to( this, 'height' );
@@ -570,7 +612,10 @@ export default class TablePropertiesView extends View {
 			icons: ALIGNMENT_ICONS,
 			toolbar: alignmentToolbar,
 			labels: this._alignmentLabels,
-			propertyName: 'alignment'
+			propertyName: 'alignment',
+			nameToValue: name => {
+				return name === 'center' ? '' : name;
+			}
 		} );
 
 		return {
@@ -594,6 +639,13 @@ export default class TablePropertiesView extends View {
 
 		const saveButtonView = new ButtonView( locale );
 		const cancelButtonView = new ButtonView( locale );
+		const fieldsThatShouldValidateToSave = [
+			this.borderWidthInput,
+			this.borderColorInput,
+			this.backgroundInput,
+			this.widthInput,
+			this.heightInput
+		];
 
 		saveButtonView.set( {
 			label: t( 'Save' ),
@@ -603,6 +655,10 @@ export default class TablePropertiesView extends View {
 			withText: true,
 		} );
 
+		saveButtonView.bind( 'isEnabled' ).toMany( fieldsThatShouldValidateToSave, 'errorText', ( ...errorTexts ) => {
+			return errorTexts.every( errorText => !errorText );
+		} );
+
 		cancelButtonView.set( {
 			label: t( 'Cancel' ),
 			icon: cancelIcon,
@@ -634,3 +690,7 @@ export default class TablePropertiesView extends View {
 		};
 	}
 }
+
+function isBorderStyleSet( value ) {
+	return !!value;
+}

+ 20 - 12
packages/ckeditor5-table/src/tableui.js

@@ -54,21 +54,29 @@ export default class TableUI extends Plugin {
 				tooltip: true
 			} );
 
-			// Prepare custom view for dropdown's panel.
-			const insertTableView = new InsertTableView( locale );
-			dropdownView.panelView.children.add( insertTableView );
+			let insertTableView;
 
-			insertTableView.delegate( 'execute' ).to( dropdownView );
+			dropdownView.on( 'change:isOpen', () => {
+				if ( insertTableView ) {
+					return;
+				}
 
-			dropdownView.buttonView.on( 'open', () => {
-				// Reset the chooser before showing it to the user.
-				insertTableView.rows = 0;
-				insertTableView.columns = 0;
-			} );
+				// Prepare custom view for dropdown's panel.
+				insertTableView = new InsertTableView( locale );
+				dropdownView.panelView.children.add( insertTableView );
+
+				insertTableView.delegate( 'execute' ).to( dropdownView );
+
+				dropdownView.buttonView.on( 'open', () => {
+					// Reset the chooser before showing it to the user.
+					insertTableView.rows = 0;
+					insertTableView.columns = 0;
+				} );
 
-			dropdownView.on( 'execute', () => {
-				editor.execute( 'insertTable', { rows: insertTableView.rows, columns: insertTableView.columns } );
-				editor.editing.view.focus();
+				dropdownView.on( 'execute', () => {
+					editor.execute( 'insertTable', { rows: insertTableView.rows, columns: insertTableView.columns } );
+					editor.editing.view.focus();
+				} );
 			} );
 
 			return dropdownView;

+ 252 - 0
packages/ckeditor5-table/src/ui/colorinputview.js

@@ -0,0 +1,252 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module table/ui/colorinputview
+ */
+
+import View from '@ckeditor/ckeditor5-ui/src/view';
+import InputTextView from '@ckeditor/ckeditor5-ui/src/inputtext/inputtextview';
+import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
+import { createDropdown } from '@ckeditor/ckeditor5-ui/src/dropdown/utils';
+import ColorGrid from '@ckeditor/ckeditor5-ui/src/colorgrid/colorgridview';
+import removeButtonIcon from '@ckeditor/ckeditor5-core/theme/icons/eraser.svg';
+import '../../theme/colorinput.css';
+
+/**
+ * The color input view class. It allows the user to type in a color (hex, rgb, etc.)
+ * or choose it from the configurable color palette with a preview.
+ *
+ * @private
+ * @extends module:ui/view~View
+ */
+export default class ColorInputView extends View {
+	/**
+	 * Creates an instance of the color input view.
+	 *
+	 * @param {module:utils/locale~Locale} locale The locale instance.
+	 * @param {Object} options The input options.
+	 * @param {module:ui/colorgrid/colorgrid~ColorDefinition} options.colorDefinitions The colors to be displayed
+	 * in the palette inside the input's dropdown.
+	 * @param {Number} options.columns The number of columns in which the colors will be displayed.
+	 */
+	constructor( locale, options ) {
+		super( locale );
+
+		const bind = this.bindTemplate;
+
+		/**
+		 * The value of the input.
+		 *
+		 * @observable
+		 * @member {String} #value
+		 * @default ''
+		 */
+		this.set( 'value', '' );
+
+		/**
+		 * The `id` attribute of the input (i.e. to pair with a `<label>` element).
+		 *
+		 * @observable
+		 * @member {String} #id
+		 */
+		this.set( 'id' );
+
+		/**
+		 * Controls whether the input view is in read-only mode.
+		 *
+		 * @observable
+		 * @member {Boolean} #isReadOnly
+		 * @default false
+		 */
+		this.set( 'isReadOnly', false );
+
+		/**
+		 * Set to `true` when the field has some error. Usually controlled via
+		 * {@link module:ui/labeledinput/labeledinputview~LabeledInputView#errorText}.
+		 *
+		 * @observable
+		 * @member {Boolean} #hasError
+		 * @default false
+		 */
+		this.set( 'hasError', false );
+
+		/**
+		 * The `id` of the element describing this field, e.g. when it has
+		 * some error, it helps screen readers read the error text.
+		 *
+		 * @observable
+		 * @member {Boolean} #ariaDescribedById
+		 */
+		this.set( 'ariaDescribedById' );
+
+		/**
+		 * Cached reference to the options passed to the constructor.
+		 *
+		 * @member {Object}
+		 */
+		this.options = options;
+
+		/**
+		 * An instance of the dropdown allowing to select a color from a grid.
+		 *
+		 * @protected
+		 * @member {module:ui/dropdown/dropdown~DropdownView}
+		 */
+		this._dropdownView = this._createDropdownView( locale );
+
+		/**
+		 * An instance of the input allowing the user to type a color value.
+		 *
+		 * @protected
+		 * @member {module:ui/dropdown/dropdown~DropdownView}
+		 */
+		this._inputView = this._createInputTextView( locale );
+
+		this.setTemplate( {
+			tag: 'div',
+			attributes: {
+				class: [
+					'ck',
+					'ck-input-color',
+					bind.if( 'hasError', 'ck-error' )
+				],
+				id: bind.to( 'id' ),
+				'aria-invalid': bind.if( 'hasError', true ),
+				'aria-describedby': bind.to( 'ariaDescribedById' )
+			},
+			children: [
+				this._inputView,
+				this._dropdownView
+			],
+		} );
+	}
+
+	/**
+	 * Focuses the input.
+	 */
+	focus() {
+		this._inputView.focus();
+	}
+
+	/**
+	 * Creates and configures the {@link #_dropdownView}.
+	 *
+	 * @private
+	 */
+	_createDropdownView() {
+		const locale = this.locale;
+		const bind = this.bindTemplate;
+		const colorGrid = this._createColorGrid( locale );
+		const dropdown = createDropdown( locale );
+		const colorPreview = new View();
+		const removeColorButton = this._createRemoveColorButton( locale );
+
+		colorPreview.setTemplate( {
+			tag: 'span',
+			attributes: {
+				class: [
+					'ck',
+					'ck-input-color__button__preview'
+				],
+				style: {
+					backgroundColor: bind.to( 'value' )
+				}
+			},
+			children: [ {
+				tag: 'span',
+				attributes: {
+					class: [
+						'ck',
+						'ck-input-color__button__preview__no-color-indicator',
+						bind.if( 'value', 'ck-hidden', value => value != '' )
+					]
+				}
+			} ]
+		} );
+
+		dropdown.buttonView.extendTemplate( {
+			attributes: {
+				class: 'ck-input-color__button'
+			},
+		} );
+
+		dropdown.buttonView.children.add( colorPreview );
+
+		dropdown.panelPosition = 'sw';
+		dropdown.panelView.children.add( removeColorButton );
+		dropdown.panelView.children.add( colorGrid );
+		dropdown.bind( 'isEnabled' ).to( this, 'isReadOnly', value => !value );
+
+		return dropdown;
+	}
+
+	/**
+	 * Creates and configures the {@link #_inputView}.
+	 *
+	 * @private
+	 */
+	_createInputTextView() {
+		const locale = this.locale;
+		const input = new InputTextView( locale );
+
+		input.bind( 'value' ).to( this );
+		input.bind( 'isReadOnly' ).to( this );
+		input.bind( 'hasError' ).to( this );
+
+		input.on( 'input', () => {
+			this.value = input.element.value;
+		} );
+
+		input.delegate( 'input' ).to( this );
+
+		return input;
+	}
+
+	/**
+	 * Creates and configures the button that clears the color.
+	 *
+	 * @private
+	 */
+	_createRemoveColorButton() {
+		const locale = this.locale;
+		const t = locale.t;
+		const removeColorButton = new ButtonView( locale );
+
+		removeColorButton.class = 'ck-input-color__remove-color';
+		removeColorButton.withText = true;
+		removeColorButton.icon = removeButtonIcon;
+		removeColorButton.label = t( 'Remove color' );
+		removeColorButton.on( 'execute', () => {
+			this.value = '';
+			this._dropdownView.isOpen = false;
+			this.fire( 'input' );
+		} );
+
+		return removeColorButton;
+	}
+
+	/**
+	 * Creates and configures the color grid inside the {@link #_dropdownView}.
+	 *
+	 * @private
+	 */
+	_createColorGrid( locale ) {
+		const colorGrid = new ColorGrid( locale, {
+			colorDefinitions: this.options.colorDefinitions,
+			columns: this.options.columns
+		} );
+
+		colorGrid.on( 'execute', ( evtData, data ) => {
+			this.value = data.value;
+			this._dropdownView.isOpen = false;
+			this.fire( 'input' );
+		} );
+
+		colorGrid.bind( 'selectedColor' ).to( this, 'value' );
+
+		return colorGrid;
+	}
+}

+ 302 - 10
packages/ckeditor5-table/src/ui/utils.js

@@ -11,6 +11,8 @@ import BalloonPanelView from '@ckeditor/ckeditor5-ui/src/panel/balloon/balloonpa
 import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
 import Collection from '@ckeditor/ckeditor5-utils/src/collection';
 import Model from '@ckeditor/ckeditor5-ui/src/model';
+import ColorInputView from './colorinputview';
+import { isColor, isLength, isPercentage } from '@ckeditor/ckeditor5-engine/src/view/styles/utils';
 import { getTableWidgetAncestor } from '../utils';
 import { findAncestor } from '../commands/utils';
 
@@ -24,6 +26,8 @@ const BALLOON_POSITIONS = [
 	DEFAULT_BALLOON_POSITIONS.southArrowNorthEast
 ];
 
+const isEmpty = val => val === '';
+
 /**
  * A helper utility that positions the
  * {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon contextual balloon} instance
@@ -110,11 +114,81 @@ export function getBorderStyleLabels( t ) {
 	};
 }
 
+/**
+ * Returns a localized error string that can be displayed next to color (background, border)
+ * fields that have an invalid value.
+ *
+ * @param {module:utils/locale~Locale#t} t The "t" function provided by the editor
+ * that is used to localize strings.
+ * @returns {String}
+ */
+export function getLocalizedColorErrorText( t ) {
+	return t( 'The color is invalid. Try "#FF0000" or "rgb(255,0,0)" or "red".' );
+}
+
+/**
+ * Returns a localized error string that can be displayed next to length (padding, border width)
+ * fields that have an invalid value.
+ *
+ * @param {module:utils/locale~Locale#t} t The "t" function provided by the editor
+ * that is used to localize strings.
+ * @returns {String}
+ */
+export function getLocalizedLengthErrorText( t ) {
+	return t( 'The value is invalid. Try "10px" or "2em" or simply "2".' );
+}
+
+/**
+ * Returns `true` when the passed value is an empty string or a valid CSS color expression.
+ * Otherwise, `false` is returned.
+ *
+ * See {@link module:engine/view/styles/utils~isColor}.
+ *
+ * @param {String} value
+ * @returns {Boolean}
+ */
+export function colorFieldValidator( value ) {
+	value = value.trim();
+
+	return isEmpty( value ) || isColor( value );
+}
+
+/**
+ * Returns `true` when the passed value is an empty string, number without unit or a valid CSS length expression.
+ * Otherwise, `false` is returned.
+ *
+ * See {@link module:engine/view/styles/utils~isLength}.
+ * See {@link module:engine/view/styles/utils~isPercentage}.
+ *
+ * @param {String} value
+ * @returns {Boolean}
+ */
+export function lengthFieldValidator( value ) {
+	value = value.trim();
+
+	return isEmpty( value ) || isNumberString( value ) || isLength( value ) || isPercentage( value );
+}
+
+/**
+ * Returns `true` when the passed value is an empty string, number without unit or a valid CSS length expression.
+ * Otherwise, `false` is returned.
+ *
+ * See {@link module:engine/view/styles/utils~isLength}.
+ *
+ * @param {String} value
+ * @returns {Boolean}
+ */
+export function lineWidthFieldValidator( value ) {
+	value = value.trim();
+
+	return isEmpty( value ) || isNumberString( value ) || isLength( value );
+}
+
 /**
  * Generates item definitions for a UI dropdown that allows changing the border style of a table or a table cell.
  *
  * @param {module:table/tablecellproperties/ui/tablecellpropertiesview~TableCellPropertiesView|
- * module:table/tableproperties/ui/tablepropertiesview~TablePropertiesView}
+ * module:table/tableproperties/ui/tablepropertiesview~TablePropertiesView} view
  * @returns {Iterable.<module:ui/dropdown/utils~ListDropdownItemDefinition>}
  */
 export function getBorderStyleDefinitions( view ) {
@@ -125,15 +199,19 @@ export function getBorderStyleDefinitions( view ) {
 		const definition = {
 			type: 'button',
 			model: new Model( {
-				_borderStyleValue: style,
+				_borderStyleValue: style === 'none' ? '' : style,
 				label: styleLabels[ style ],
-				withText: true,
+				withText: true
 			} )
 		};
 
-		definition.model.bind( 'isOn' ).to( view, 'borderStyle', value => {
-			return value === style;
-		} );
+		if ( style === 'none' ) {
+			definition.model.bind( 'isOn' ).to( view, 'borderStyle', value => !value );
+		} else {
+			definition.model.bind( 'isOn' ).to( view, 'borderStyle', value => {
+				return value === style;
+			} );
+		}
 
 		itemDefinitions.add( definition );
 	}
@@ -155,24 +233,238 @@ export function getBorderStyleDefinitions( view ) {
  * @param {module:ui/toolbar/toolbarview~ToolbarView} options.toolbar
  * @param {Object.<String,String>} labels
  * @param {String} propertyName
+ * @param {Function} nameToValue Function that maps button name to value. By default names are the same as values.
  */
-export function fillToolbar( { view, icons, toolbar, labels, propertyName } ) {
+export function fillToolbar( { view, icons, toolbar, labels, propertyName, nameToValue } ) {
 	for ( const name in labels ) {
 		const button = new ButtonView( view.locale );
 
 		button.set( {
 			label: labels[ name ],
-			icon: icons[ name ],
+			icon: icons[ name ]
 		} );
 
 		button.bind( 'isOn' ).to( view, propertyName, value => {
-			return value === name;
+			return value === nameToValue( name );
 		} );
 
 		button.on( 'execute', () => {
-			view[ propertyName ] = name;
+			view[ propertyName ] = nameToValue( name );
 		} );
 
 		toolbar.items.add( button );
 	}
 }
+
+/**
+ * A default color palette used by various user interfaces related to tables, for instance,
+ * by {@link module:table/tablecellproperties/tablecellpropertiesui~TableCellPropertiesUI} or
+ * {@link module:table/tableproperties/tablepropertiesui~TablePropertiesUI}.
+ *
+ * The color palette follows the {@link module:table/table~TableColorConfig table color configuration format}
+ * and contains the following color definitions:
+ *
+ *		const defaultColors = [
+ *			{
+ *				color: 'hsl(0, 0%, 0%)',
+ *				label: 'Black'
+ *			},
+ *			{
+ *				color: 'hsl(0, 0%, 30%)',
+ *				label: 'Dim grey'
+ *			},
+ *			{
+ *				color: 'hsl(0, 0%, 60%)',
+ *				label: 'Grey'
+ *			},
+ *			{
+ *				color: 'hsl(0, 0%, 90%)',
+ *				label: 'Light grey'
+ *			},
+ *			{
+ *				color: 'hsl(0, 0%, 100%)',
+ *				label: 'White',
+ *				hasBorder: true
+ *			},
+ *			{
+ *				color: 'hsl(0, 75%, 60%)',
+ *				label: 'Red'
+ *			},
+ *			{
+ *				color: 'hsl(30, 75%, 60%)',
+ *				label: 'Orange'
+ *			},
+ *			{
+ *				color: 'hsl(60, 75%, 60%)',
+ *				label: 'Yellow'
+ *			},
+ *			{
+ *				color: 'hsl(90, 75%, 60%)',
+ *				label: 'Light green'
+ *			},
+ *			{
+ *				color: 'hsl(120, 75%, 60%)',
+ *				label: 'Green'
+ *			},
+ *			{
+ *				color: 'hsl(150, 75%, 60%)',
+ *				label: 'Aquamarine'
+ *			},
+ *			{
+ *				color: 'hsl(180, 75%, 60%)',
+ *				label: 'Turquoise'
+ *			},
+ *			{
+ *				color: 'hsl(210, 75%, 60%)',
+ *				label: 'Light blue'
+ *			},
+ *			{
+ *				color: 'hsl(240, 75%, 60%)',
+ *				label: 'Blue'
+ *			},
+ *			{
+ *				color: 'hsl(270, 75%, 60%)',
+ *				label: 'Purple'
+ *			}
+ *		];
+ */
+export const defaultColors = [
+	{
+		color: 'hsl(0, 0%, 0%)',
+		label: 'Black'
+	},
+	{
+		color: 'hsl(0, 0%, 30%)',
+		label: 'Dim grey'
+	},
+	{
+		color: 'hsl(0, 0%, 60%)',
+		label: 'Grey'
+	},
+	{
+		color: 'hsl(0, 0%, 90%)',
+		label: 'Light grey'
+	},
+	{
+		color: 'hsl(0, 0%, 100%)',
+		label: 'White',
+		hasBorder: true
+	},
+	{
+		color: 'hsl(0, 75%, 60%)',
+		label: 'Red'
+	},
+	{
+		color: 'hsl(30, 75%, 60%)',
+		label: 'Orange'
+	},
+	{
+		color: 'hsl(60, 75%, 60%)',
+		label: 'Yellow'
+	},
+	{
+		color: 'hsl(90, 75%, 60%)',
+		label: 'Light green'
+	},
+	{
+		color: 'hsl(120, 75%, 60%)',
+		label: 'Green'
+	},
+	{
+		color: 'hsl(150, 75%, 60%)',
+		label: 'Aquamarine'
+	},
+	{
+		color: 'hsl(180, 75%, 60%)',
+		label: 'Turquoise'
+	},
+	{
+		color: 'hsl(210, 75%, 60%)',
+		label: 'Light blue'
+	},
+	{
+		color: 'hsl(240, 75%, 60%)',
+		label: 'Blue'
+	},
+	{
+		color: 'hsl(270, 75%, 60%)',
+		label: 'Purple'
+	}
+];
+
+/**
+ * Returns a creator for color input with a label.
+ *
+ * For given options, it returns a function that creates an instance of
+ * {@link module:table/ui/colorinputview~ColorInputView color input} logically related to
+ * a {@link module:ui/labeledview/labeledview~LabeledView labeled view} in DOM.
+ *
+ * The helper does the following:
+ *
+ * * It sets color input's `id` and `ariaDescribedById` attributes.
+ * * It binds color input's `isReadOnly` to the labeled view.
+ * * It binds color input's `hasError` to the labeled view.
+ * * It enables a logic that cleans up the error when user starts typing in the color input.
+ *
+ * Usage:
+ *
+ *		const colorInputCreator = getLabeledColorInputCreator( {
+ *			colorConfig: [ ... ],
+ *			columns: 3,
+ *		} );
+ *
+ *		const labeledInputView = new LabeledView( locale, colorInputCreator );
+ *		console.log( labeledInputView.view ); // An color input instance.
+ *
+ * @private
+ * @param options Color input options.
+ * @param {module:table/table~TableColorConfig} options.colorConfig The configuration of the color palette
+ * displayed in the input's dropdown.
+ * @param {Number} options.columns The configuration of the number of columns the color palette consists of
+ * in the input's dropdown.
+ * @returns {Function}
+ */
+export function getLabeledColorInputCreator( options ) {
+	return ( labeledView, viewUid, statusUid ) => {
+		const inputView = new ColorInputView( labeledView.locale, {
+			colorDefinitions: colorConfigToColorGridDefinitions( options.colorConfig ),
+			columns: options.columns
+		} );
+
+		inputView.set( {
+			id: viewUid,
+			ariaDescribedById: statusUid
+		} );
+
+		inputView.bind( 'isReadOnly' ).to( labeledView, 'isEnabled', value => !value );
+		inputView.bind( 'errorText' ).to( labeledView );
+
+		inputView.on( 'input', () => {
+			// UX: Make the error text disappear and disable the error indicator as the user
+			// starts fixing the errors.
+			labeledView.errorText = null;
+		} );
+
+		return inputView;
+	};
+}
+
+// A simple helper method to detect number strings.
+// I allows full number notation, so omitting 0 is not allowed:
+function isNumberString( value ) {
+	const parsedValue = parseFloat( value );
+
+	return !Number.isNaN( parsedValue ) && value === String( parsedValue );
+}
+
+// @param {Array.<Object>} colorConfig
+// @returns {Array.<module:ui/colorgrid/colorgrid~ColorDefinition>}
+function colorConfigToColorGridDefinitions( colorConfig ) {
+	return colorConfig.map( item => ( {
+		color: item.model,
+		label: item.label,
+		options: {
+			hasBorder: item.hasBorder
+		}
+	} ) );
+}

+ 15 - 6
packages/ckeditor5-table/tests/_utils/utils.js

@@ -297,13 +297,19 @@ export function assertTRBLAttribute( element, key, top, right = top, bottom = to
  * An assertion helper for testing the `<table>` style attribute.
  *
  * @param {module:core/editor/editor~Editor} editor
- * @param {String} tableStyle A style to assert on table.
+ * @param {String} [tableStyle=''] A style to assert on <table>.
+ * @param {String} [figureStyle=''] A style to assert on <figure>.
  */
-export function assertTableStyle( editor, tableStyle ) {
-	const styleEntry = tableStyle ? ` style="${ tableStyle }"` : '';
+export function assertTableStyle( editor, tableStyle, figureStyle ) {
+	const tableStyleEntry = tableStyle ? ` style="${ tableStyle }"` : '';
+	const figureStyleEntry = figureStyle ? ` style="${ figureStyle }"` : '';
 
 	assertEqualMarkup( editor.getData(),
-		`<figure class="table"><table${ styleEntry }><tbody><tr><td>foo</td></tr></tbody></table></figure>`
+		`<figure class="table"${ figureStyleEntry }>` +
+			`<table${ tableStyleEntry }>` +
+				'<tbody><tr><td>foo</td></tr></tbody>' +
+			'</table>' +
+		'</figure>'
 	);
 }
 
@@ -311,7 +317,7 @@ export function assertTableStyle( editor, tableStyle ) {
  * An assertion helper for testing the `<td>` style attribute.
  *
  * @param {module:core/editor/editor~Editor} editor
- * @param {String} tableCellStyle A style to assert on td.
+ * @param {String} [tableCellStyle=''] A style to assert on td.
  */
 export function assertTableCellStyle( editor, tableCellStyle ) {
 	assertEqualMarkup( editor.getData(),
@@ -372,7 +378,10 @@ function makeRows( tableData, options ) {
 				}
 
 				if ( !( contents.replace( '[', '' ).replace( ']', '' ).startsWith( '<' ) ) && enforceWrapping ) {
-					contents = `<${ wrappingElement }>${ contents }</${ wrappingElement }>`;
+					contents =
+						`<${ wrappingElement == 'span' ? 'span style="display:inline-block"' : wrappingElement }>` +
+							contents +
+						`</${ wrappingElement }>`;
 				}
 
 				const formattedAttributes = formatAttributes( attributes );

+ 10 - 5
packages/ckeditor5-table/tests/converters/downcast.js

@@ -26,7 +26,10 @@ function paragraphInTableCell() {
 
 				if ( viewElement && viewElement.name === 'span' ) {
 					conversionApi.mapper.unbindModelElement( tableCell );
+
+					conversionApi.writer.removeStyle( 'display', viewElement );
 					conversionApi.writer.rename( 'p', viewElement );
+
 					conversionApi.mapper.bindElements( child, viewElement );
 				}
 			}
@@ -339,7 +342,9 @@ describe( 'downcast converters', () => {
 						'<table>' +
 							'<tbody>' +
 								'<tr>' +
-									'<td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true"><span></span></td>' +
+									'<td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true">' +
+										'<span style="display:inline-block"></span>' +
+									'</td>' +
 								'</tr>' +
 							'</tbody>' +
 						'</table>' +
@@ -572,7 +577,7 @@ describe( 'downcast converters', () => {
 							'<tbody>' +
 								'<tr>' +
 									'<td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true">' +
-										'<span>00</span>' +
+										'<span style="display:inline-block">00</span>' +
 									'</td>' +
 								'</tr>' +
 								'<tr>' +
@@ -718,7 +723,7 @@ describe( 'downcast converters', () => {
 							'<tbody>' +
 								'<tr>' +
 									'<td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true">' +
-										'<span>00</span>' +
+										'<span style="display:inline-block">00</span>' +
 									'</td>' +
 									'<td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true"></td>' +
 								'</tr>' +
@@ -895,7 +900,7 @@ describe( 'downcast converters', () => {
 							'<thead>' +
 								'<tr>' +
 									'<th class="ck-editor__editable ck-editor__nested-editable" contenteditable="true">' +
-										'<span>00</span>' +
+										'<span style="display:inline-block">00</span>' +
 									'</th>' +
 								'</tr>' +
 							'</thead>' +
@@ -1109,7 +1114,7 @@ describe( 'downcast converters', () => {
 							'<tbody>' +
 								'<tr>' +
 									'<th class="ck-editor__editable ck-editor__nested-editable" contenteditable="true">' +
-										'<span>00</span>' +
+										'<span style="display:inline-block">00</span>' +
 									'</th>' +
 								'</tr>' +
 							'</tbody>' +

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

@@ -169,7 +169,7 @@ describe( 'Table cell refresh post-fixer', () => {
 		} );
 
 		assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [
-			[ '<span>00</span>' ]
+			[ '<span style="display:inline-block">00</span>' ]
 		], { asWidget: true } ) );
 		sinon.assert.calledOnce( refreshItemSpy );
 	} );
@@ -279,7 +279,7 @@ describe( 'Table cell refresh post-fixer', () => {
 		} );
 
 		assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [
-			[ '<span>00</span>' ]
+			[ '<span style="display:inline-block">00</span>' ]
 		], { asWidget: true } ) );
 		sinon.assert.calledOnce( refreshItemSpy );
 	} );

+ 0 - 1
packages/ckeditor5-table/tests/manual/tableproperties.js

@@ -36,4 +36,3 @@ ClassicEditor
 	.catch( err => {
 		console.error( err.stack );
 	} );
-

+ 20 - 0
packages/ckeditor5-table/tests/manual/tickets/6062.html

@@ -0,0 +1,20 @@
+<div id="editor">
+	<figure class="table">
+		<table>
+			<thead>
+				<tr>
+					<th>at the end:</th>
+					<th>at the beginning:</th>
+					<th>entire cell:</th>
+				</tr>
+			</thead>
+			<tbody>
+				<tr>
+					<td>Hello, commter</td>
+					<td>Commter, hello!</td>
+					<td>Commter</td>
+				</tr>
+			</tbody>
+		</table>
+	</figure>
+</div>

+ 27 - 0
packages/ckeditor5-table/tests/manual/tickets/6062.js

@@ -0,0 +1,27 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* globals console, window, document */
+
+import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
+import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
+
+ClassicEditor
+	.create( document.querySelector( '#editor' ), {
+		plugins: [ ArticlePluginSet ],
+		toolbar: [
+			'heading', '|', 'insertTable', '|', 'bold', 'italic', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo'
+		],
+		table: {
+			contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ],
+			tableToolbar: [ 'bold', 'italic' ]
+		}
+	} )
+	.then( editor => {
+		window.editor = editor;
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );

+ 9 - 0
packages/ckeditor5-table/tests/manual/tickets/6062.md

@@ -0,0 +1,9 @@
+# Ticket test: [#6062](https://github.com/ckeditor/ckeditor5/issues/6062)
+
+1. Right-click the misspelled word.
+2. Choose one of the corrections.
+
+Expected:
+
+* No errors.
+* The misspelled word should be correctly replaced. E.g., you shouldn't see a new paragraph being created.

+ 56 - 0
packages/ckeditor5-table/tests/tablecellproperties/commands/tablecellborderwidthcommand.js

@@ -121,6 +121,62 @@ describe( 'table cell properties', () => {
 					sinon.assert.calledWith( spy, batch );
 				} );
 
+				it( 'should add default unit for numeric values (number passed)', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+					command.execute( { value: 25 } );
+
+					assertTableCellStyle( editor, 'border-bottom:25px;border-left:25px;border-right:25px;border-top:25px;' );
+				} );
+
+				it( 'should add default unit for numeric values (string passed)', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+					command.execute( { value: 25 } );
+
+					assertTableCellStyle( editor, 'border-bottom:25px;border-left:25px;border-right:25px;border-top:25px;' );
+				} );
+
+				it( 'should not add default unit for numeric values with unit', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+					command.execute( { value: '25pt' } );
+
+					assertTableCellStyle( editor, 'border-bottom:25pt;border-left:25pt;border-right:25pt;border-top:25pt;' );
+				} );
+
+				it( 'should add default unit to floats (number passed)', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+					command.execute( { value: 25.1 } );
+
+					assertTableCellStyle( editor, 'border-bottom:25.1px;border-left:25.1px;border-right:25.1px;border-top:25.1px;' );
+				} );
+
+				it( 'should add default unit to floats (string passed)', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+					command.execute( { value: '0.1' } );
+
+					assertTableCellStyle( editor, 'border-bottom:0.1px;border-left:0.1px;border-right:0.1px;border-top:0.1px;' );
+				} );
+
+				it( 'should pass invalid values', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+					command.execute( { value: 'bar' } );
+
+					assertTableCellStyle( editor, 'border-bottom:bar;border-left:bar;border-right:bar;border-top:bar;' );
+				} );
+
+				it( 'should pass invalid value (string passed, CSS float without leading 0)', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+					command.execute( { value: '.2' } );
+
+					assertTableCellStyle( editor, 'border-bottom:.2;border-left:.2;border-right:.2;border-top:.2;' );
+				} );
+
 				describe( 'collapsed selection', () => {
 					it( 'should set selected table cell borderWidth to a passed value', () => {
 						setData( model, modelTable( [ [ 'foo[]' ] ] ) );

+ 56 - 0
packages/ckeditor5-table/tests/tablecellproperties/commands/tablecellheightcommand.js

@@ -96,6 +96,62 @@ describe( 'table cell properties', () => {
 					sinon.assert.calledWith( spy, batch );
 				} );
 
+				it( 'should add default unit for numeric values (number passed)', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+					command.execute( { value: 25 } );
+
+					assertTableCellStyle( editor, 'height:25px;' );
+				} );
+
+				it( 'should add default unit for numeric values (string passed)', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+					command.execute( { value: 25 } );
+
+					assertTableCellStyle( editor, 'height:25px;' );
+				} );
+
+				it( 'should not add default unit for numeric values with unit', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+					command.execute( { value: '25pt' } );
+
+					assertTableCellStyle( editor, 'height:25pt;' );
+				} );
+
+				it( 'should add default unit to floats (number passed)', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+					command.execute( { value: 25.1 } );
+
+					assertTableCellStyle( editor, 'height:25.1px;' );
+				} );
+
+				it( 'should add default unit to floats (string passed)', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+					command.execute( { value: '0.1' } );
+
+					assertTableCellStyle( editor, 'height:0.1px;' );
+				} );
+
+				it( 'should pass invalid values', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+					command.execute( { value: 'bar' } );
+
+					assertTableCellStyle( editor, 'height:bar;' );
+				} );
+
+				it( 'should pass invalid value (string passed, CSS float without leading 0)', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+					command.execute( { value: '.2' } );
+
+					assertTableCellStyle( editor, 'height:.2;' );
+				} );
+
 				describe( 'collapsed selection', () => {
 					it( 'should set selected table cell height to a passed value', () => {
 						setData( model, modelTable( [ [ 'foo[]' ] ] ) );

+ 56 - 0
packages/ckeditor5-table/tests/tablecellproperties/commands/tablecellpaddingcommand.js

@@ -121,6 +121,62 @@ describe( 'table cell properties', () => {
 					sinon.assert.calledWith( spy, batch );
 				} );
 
+				it( 'should add default unit for numeric values (number passed)', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+					command.execute( { value: 25 } );
+
+					assertTableCellStyle( editor, 'padding:25px;' );
+				} );
+
+				it( 'should add default unit for numeric values (string passed)', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+					command.execute( { value: 25 } );
+
+					assertTableCellStyle( editor, 'padding:25px;' );
+				} );
+
+				it( 'should not add default unit for numeric values with unit', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+					command.execute( { value: '25pt' } );
+
+					assertTableCellStyle( editor, 'padding:25pt;' );
+				} );
+
+				it( 'should add default unit to floats (number passed)', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+					command.execute( { value: 25.1 } );
+
+					assertTableCellStyle( editor, 'padding:25.1px;' );
+				} );
+
+				it( 'should add default unit to floats (string passed)', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+					command.execute( { value: '0.1' } );
+
+					assertTableCellStyle( editor, 'padding:0.1px;' );
+				} );
+
+				it( 'should pass invalid values', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+					command.execute( { value: 'bar' } );
+
+					assertTableCellStyle( editor, 'padding:bar;' );
+				} );
+
+				it( 'should pass invalid value (string passed, CSS float without leading 0)', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+					command.execute( { value: '.2' } );
+
+					assertTableCellStyle( editor, 'padding:.2;' );
+				} );
+
 				describe( 'collapsed selection', () => {
 					it( 'should set selected table cell padding to a passed value', () => {
 						setData( model, modelTable( [ [ 'foo[]' ] ] ) );

+ 56 - 0
packages/ckeditor5-table/tests/tablecellproperties/commands/tablecellwidthcommand.js

@@ -96,6 +96,62 @@ describe( 'table cell properties', () => {
 					sinon.assert.calledWith( spy, batch );
 				} );
 
+				it( 'should add default unit for numeric values (number passed)', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+					command.execute( { value: 25 } );
+
+					assertTableCellStyle( editor, 'width:25px;' );
+				} );
+
+				it( 'should add default unit for numeric values (string passed)', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+					command.execute( { value: 25 } );
+
+					assertTableCellStyle( editor, 'width:25px;' );
+				} );
+
+				it( 'should not add default unit for numeric values with unit', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+					command.execute( { value: '25pt' } );
+
+					assertTableCellStyle( editor, 'width:25pt;' );
+				} );
+
+				it( 'should add default unit to floats (number passed)', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+					command.execute( { value: 25.1 } );
+
+					assertTableCellStyle( editor, 'width:25.1px;' );
+				} );
+
+				it( 'should add default unit to floats (string passed)', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+					command.execute( { value: '0.1' } );
+
+					assertTableCellStyle( editor, 'width:0.1px;' );
+				} );
+
+				it( 'should pass invalid values', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+					command.execute( { value: 'bar' } );
+
+					assertTableCellStyle( editor, 'width:bar;' );
+				} );
+
+				it( 'should pass invalid value (string passed, CSS float without leading 0)', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+					command.execute( { value: '.2' } );
+
+					assertTableCellStyle( editor, 'width:.2;' );
+				} );
+
 				describe( 'collapsed selection', () => {
 					it( 'should set selected table cell width to a passed value', () => {
 						setData( model, modelTable( [ [ 'foo[]' ] ] ) );

+ 308 - 7
packages/ckeditor5-table/tests/tablecellproperties/tablecellpropertiesediting.js

@@ -267,6 +267,26 @@ describe( 'table cell properties', () => {
 					tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
 				} );
 
+				it( 'should consume converted item borderColor attribute', () => {
+					editor.conversion.for( 'downcast' )
+						.add( dispatcher => dispatcher.on( 'attribute:borderColor:tableCell', ( evt, data, conversionApi ) => {
+							expect( conversionApi.consumable.consume( data.item, evt.name ) ).to.be.false;
+						} ) );
+
+					model.change( writer => writer.setAttribute( 'borderColor', '#f00', tableCell ) );
+				} );
+
+				it( 'should be overridable', () => {
+					editor.conversion.for( 'downcast' )
+						.add( dispatcher => dispatcher.on( 'attribute:borderColor:tableCell', ( evt, data, conversionApi ) => {
+							conversionApi.consumable.consume( data.item, evt.name );
+						}, { priority: 'high' } ) );
+
+					model.change( writer => writer.setAttribute( 'borderColor', '#f00', tableCell ) );
+
+					assertTableCellStyle( editor, '' );
+				} );
+
 				it( 'should downcast borderColor attribute (same top, right, bottom, left)', () => {
 					model.change( writer => writer.setAttribute( 'borderColor', {
 						top: '#f00',
@@ -294,6 +314,26 @@ describe( 'table cell properties', () => {
 					);
 				} );
 
+				it( 'should consume converted item borderStyle attribute', () => {
+					editor.conversion.for( 'downcast' )
+						.add( dispatcher => dispatcher.on( 'attribute:borderStyle:tableCell', ( evt, data, conversionApi ) => {
+							expect( conversionApi.consumable.consume( data.item, evt.name ) ).to.be.false;
+						} ) );
+
+					model.change( writer => writer.setAttribute( 'borderStyle', 'ridge', tableCell ) );
+				} );
+
+				it( 'should be overridable', () => {
+					editor.conversion.for( 'downcast' )
+						.add( dispatcher => dispatcher.on( 'attribute:borderStyle:tableCell', ( evt, data, conversionApi ) => {
+							conversionApi.consumable.consume( data.item, evt.name );
+						}, { priority: 'high' } ) );
+
+					model.change( writer => writer.setAttribute( 'borderStyle', 'ridge', tableCell ) );
+
+					assertTableCellStyle( editor, '' );
+				} );
+
 				it( 'should downcast borderStyle attribute (same top, right, bottom, left)', () => {
 					model.change( writer => writer.setAttribute( 'borderStyle', {
 						top: 'solid',
@@ -316,6 +356,26 @@ describe( 'table cell properties', () => {
 					assertTableCellStyle( editor, 'border-bottom:dotted;border-left:dashed;border-right:ridge;border-top:solid;' );
 				} );
 
+				it( 'should consume converted item borderWidth attribute', () => {
+					editor.conversion.for( 'downcast' )
+						.add( dispatcher => dispatcher.on( 'attribute:borderWidth:tableCell', ( evt, data, conversionApi ) => {
+							expect( conversionApi.consumable.consume( data.item, evt.name ) ).to.be.false;
+						} ) );
+
+					model.change( writer => writer.setAttribute( 'borderWidth', '2px', tableCell ) );
+				} );
+
+				it( 'should be overridable', () => {
+					editor.conversion.for( 'downcast' )
+						.add( dispatcher => dispatcher.on( 'attribute:borderWidth:tableCell', ( evt, data, conversionApi ) => {
+							conversionApi.consumable.consume( data.item, evt.name );
+						}, { priority: 'high' } ) );
+
+					model.change( writer => writer.setAttribute( 'borderWidth', '2px', tableCell ) );
+
+					assertTableCellStyle( editor, '' );
+				} );
+
 				it( 'should downcast borderWidth attribute (same top, right, bottom, left)', () => {
 					model.change( writer => writer.setAttribute( 'borderWidth', {
 						top: '42px',
@@ -540,6 +600,20 @@ describe( 'table cell properties', () => {
 
 					expect( tableCell.getAttribute( 'backgroundColor' ) ).to.equal( '#f00' );
 				} );
+
+				it( 'should upcast from background shorthand', () => {
+					editor.setData( '<table><tr><td style="background:#f00 center center">foo</td></tr></table>' );
+					const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
+
+					expect( tableCell.getAttribute( 'backgroundColor' ) ).to.equal( '#f00' );
+				} );
+
+				it( 'should upcast from background shorthand (rbg color value with spaces)', () => {
+					editor.setData( '<table><tr><td style="background:rgb(253, 253, 119) center center">foo</td></tr></table>' );
+					const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
+
+					expect( tableCell.getAttribute( 'backgroundColor' ) ).to.equal( 'rgb(253, 253, 119)' );
+				} );
 			} );
 
 			describe( 'downcast conversion', () => {
@@ -559,11 +633,49 @@ describe( 'table cell properties', () => {
 					tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
 				} );
 
+				it( 'should consume converted item backgroundColor attribute', () => {
+					editor.conversion.for( 'downcast' )
+						.add( dispatcher => dispatcher.on( 'attribute:backgroundColor:tableCell', ( evt, data, conversionApi ) => {
+							expect( conversionApi.consumable.consume( data.item, evt.name ) ).to.be.false;
+						} ) );
+
+					model.change( writer => writer.setAttribute( 'backgroundColor', '#f00', tableCell ) );
+				} );
+
+				it( 'should be overridable', () => {
+					editor.conversion.for( 'downcast' )
+						.add( dispatcher => dispatcher.on( 'attribute:backgroundColor:tableCell', ( evt, data, conversionApi ) => {
+							conversionApi.consumable.consume( data.item, evt.name );
+						}, { priority: 'high' } ) );
+
+					model.change( writer => writer.setAttribute( 'backgroundColor', '#f00', tableCell ) );
+
+					assertTableCellStyle( editor, '' );
+				} );
+
 				it( 'should downcast backgroundColor', () => {
 					model.change( writer => writer.setAttribute( 'backgroundColor', '#f00', tableCell ) );
 
 					assertTableCellStyle( editor, 'background-color:#f00;' );
 				} );
+
+				it( 'should downcast backgroundColor removal', () => {
+					model.change( writer => writer.setAttribute( 'backgroundColor', '#f00', tableCell ) );
+
+					model.change( writer => writer.removeAttribute( 'backgroundColor', tableCell ) );
+
+					assertTableCellStyle( editor );
+				} );
+
+				it( 'should downcast backgroundColor change', () => {
+					model.change( writer => writer.setAttribute( 'backgroundColor', '#f00', tableCell ) );
+
+					assertTableCellStyle( editor, 'background-color:#f00;' );
+
+					model.change( writer => writer.setAttribute( 'backgroundColor', '#ba7', tableCell ) );
+
+					assertTableCellStyle( editor, 'background-color:#ba7;' );
+				} );
 			} );
 		} );
 
@@ -573,11 +685,11 @@ describe( 'table cell properties', () => {
 			} );
 
 			describe( 'upcast conversion', () => {
-				it( 'should upcast text-align:left style', () => {
+				it( 'should not upcast text-align:left style', () => {
 					editor.setData( '<table><tr><td style="text-align:left">foo</td></tr></table>' );
 					const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
 
-					expect( tableCell.getAttribute( 'horizontalAlignment' ) ).to.equal( 'left' );
+					expect( tableCell.getAttribute( 'horizontalAlignment' ) ).to.be.undefined;
 				} );
 
 				it( 'should upcast text-align:right style', () => {
@@ -619,10 +731,30 @@ describe( 'table cell properties', () => {
 					tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
 				} );
 
-				it( 'should downcast horizontalAlignment=left', () => {
+				it( 'should consume converted item horizontalAlignment attribute', () => {
+					editor.conversion.for( 'downcast' )
+						.add( dispatcher => dispatcher.on( 'attribute:horizontalAlignment:tableCell', ( evt, data, conversionApi ) => {
+							expect( conversionApi.consumable.consume( data.item, evt.name ) ).to.be.false;
+						} ) );
+
+					model.change( writer => writer.setAttribute( 'horizontalAlignment', 'right', tableCell ) );
+				} );
+
+				it( 'should be overridable', () => {
+					editor.conversion.for( 'downcast' )
+						.add( dispatcher => dispatcher.on( 'attribute:horizontalAlignment:tableCell', ( evt, data, conversionApi ) => {
+							conversionApi.consumable.consume( data.item, evt.name );
+						}, { priority: 'high' } ) );
+
+					model.change( writer => writer.setAttribute( 'horizontalAlignment', 'right', tableCell ) );
+
+					assertTableCellStyle( editor, '' );
+				} );
+
+				it( 'should not downcast horizontalAlignment=left', () => {
 					model.change( writer => writer.setAttribute( 'horizontalAlignment', 'left', tableCell ) );
 
-					assertTableCellStyle( editor, 'text-align:left;' );
+					assertTableCellStyle( editor );
 				} );
 
 				it( 'should downcast horizontalAlignment=right', () => {
@@ -651,12 +783,47 @@ describe( 'table cell properties', () => {
 			} );
 
 			describe( 'upcast conversion', () => {
-				it( 'should upcast vertical-align', () => {
+				it( 'should upcast "top" vertical-align', () => {
 					editor.setData( '<table><tr><td style="vertical-align:top">foo</td></tr></table>' );
 					const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
 
 					expect( tableCell.getAttribute( 'verticalAlignment' ) ).to.equal( 'top' );
 				} );
+
+				it( 'should upcast "bottom" vertical-align', () => {
+					editor.setData( '<table><tr><td style="vertical-align:bottom">foo</td></tr></table>' );
+					const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
+
+					expect( tableCell.getAttribute( 'verticalAlignment' ) ).to.equal( 'bottom' );
+				} );
+
+				it( 'should not upcast "middle" vertical-align', () => {
+					editor.setData( '<table><tr><td style="vertical-align:middle">foo</td></tr></table>' );
+					const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
+
+					expect( tableCell.getAttribute( 'verticalAlignment' ) ).to.be.undefined;
+				} );
+
+				it( 'should upcast "top" valign attribute', () => {
+					editor.setData( '<table><tr><td valign="top">foo</td></tr></table>' );
+					const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
+
+					expect( tableCell.getAttribute( 'verticalAlignment' ) ).to.equal( 'top' );
+				} );
+
+				it( 'should upcast "bottom" valign attribute', () => {
+					editor.setData( '<table><tr><td valign="bottom">foo</td></tr></table>' );
+					const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
+
+					expect( tableCell.getAttribute( 'verticalAlignment' ) ).to.equal( 'bottom' );
+				} );
+
+				it( 'should not upcast "middle" valign attribute', () => {
+					editor.setData( '<table><tr><td valign="middle">foo</td></tr></table>' );
+					const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
+
+					expect( tableCell.getAttribute( 'verticalAlignment' ) ).to.be.undefined;
+				} );
 			} );
 
 			describe( 'downcast conversion', () => {
@@ -676,10 +843,30 @@ describe( 'table cell properties', () => {
 					tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
 				} );
 
+				it( 'should consume converted item verticalAlignment attribute', () => {
+					editor.conversion.for( 'downcast' )
+						.add( dispatcher => dispatcher.on( 'attribute:verticalAlignment:tableCell', ( evt, data, conversionApi ) => {
+							expect( conversionApi.consumable.consume( data.item, evt.name ) ).to.be.false;
+						} ) );
+
+					model.change( writer => writer.setAttribute( 'verticalAlignment', 'top', tableCell ) );
+				} );
+
+				it( 'should be overridable', () => {
+					editor.conversion.for( 'downcast' )
+						.add( dispatcher => dispatcher.on( 'attribute:verticalAlignment:tableCell', ( evt, data, conversionApi ) => {
+							conversionApi.consumable.consume( data.item, evt.name );
+						}, { priority: 'high' } ) );
+
+					model.change( writer => writer.setAttribute( 'verticalAlignment', 'top', tableCell ) );
+
+					assertTableCellStyle( editor, '' );
+				} );
+
 				it( 'should downcast verticalAlignment', () => {
-					model.change( writer => writer.setAttribute( 'verticalAlignment', 'middle', tableCell ) );
+					model.change( writer => writer.setAttribute( 'verticalAlignment', 'top', tableCell ) );
 
-					assertTableCellStyle( editor, 'vertical-align:middle;' );
+					assertTableCellStyle( editor, 'vertical-align:top;' );
 				} );
 			} );
 		} );
@@ -715,6 +902,26 @@ describe( 'table cell properties', () => {
 					tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
 				} );
 
+				it( 'should consume converted item borderColor attribute', () => {
+					editor.conversion.for( 'downcast' )
+						.add( dispatcher => dispatcher.on( 'attribute:padding:tableCell', ( evt, data, conversionApi ) => {
+							expect( conversionApi.consumable.consume( data.item, evt.name ) ).to.be.false;
+						} ) );
+
+					model.change( writer => writer.setAttribute( 'padding', '1px', tableCell ) );
+				} );
+
+				it( 'should be overridable', () => {
+					editor.conversion.for( 'downcast' )
+						.add( dispatcher => dispatcher.on( 'attribute:padding:tableCell', ( evt, data, conversionApi ) => {
+							conversionApi.consumable.consume( data.item, evt.name );
+						}, { priority: 'high' } ) );
+
+					model.change( writer => writer.setAttribute( 'padding', '1px', tableCell ) );
+
+					assertTableCellStyle( editor, '' );
+				} );
+
 				it( 'should downcast padding (same top, right, bottom, left)', () => {
 					model.change( writer => writer.setAttribute( 'padding', {
 						top: '2px',
@@ -736,6 +943,24 @@ describe( 'table cell properties', () => {
 
 					assertTableCellStyle( editor, 'padding:2px 3px 4px 5px;' );
 				} );
+
+				it( 'should downcast padding removal', () => {
+					model.change( writer => writer.setAttribute( 'padding', '1337px', tableCell ) );
+
+					model.change( writer => writer.removeAttribute( 'padding', tableCell ) );
+
+					assertTableCellStyle( editor );
+				} );
+
+				it( 'should downcast padding change', () => {
+					model.change( writer => writer.setAttribute( 'padding', '1337px', tableCell ) );
+
+					assertTableCellStyle( editor, 'padding:1337px;' );
+
+					model.change( writer => writer.setAttribute( 'padding', '1410em', tableCell ) );
+
+					assertTableCellStyle( editor, 'padding:1410em;' );
+				} );
 			} );
 		} );
 
@@ -771,6 +996,26 @@ describe( 'table cell properties', () => {
 					tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
 				} );
 
+				it( 'should consume converted item width attribute', () => {
+					editor.conversion.for( 'downcast' )
+						.add( dispatcher => dispatcher.on( 'attribute:width:tableCell', ( evt, data, conversionApi ) => {
+							expect( conversionApi.consumable.consume( data.item, evt.name ) ).to.be.false;
+						} ) );
+
+					model.change( writer => writer.setAttribute( 'width', '40px', tableCell ) );
+				} );
+
+				it( 'should be overridable', () => {
+					editor.conversion.for( 'downcast' )
+						.add( dispatcher => dispatcher.on( 'attribute:width:tableCell', ( evt, data, conversionApi ) => {
+							conversionApi.consumable.consume( data.item, evt.name );
+						}, { priority: 'high' } ) );
+
+					model.change( writer => writer.setAttribute( 'width', '40px', tableCell ) );
+
+					assertTableCellStyle( editor, '' );
+				} );
+
 				it( 'should downcast width attribute', () => {
 					model.change( writer => writer.setAttribute( 'width', '20px', tableCell ) );
 
@@ -779,6 +1024,24 @@ describe( 'table cell properties', () => {
 						'<figure class="table"><table><tbody><tr><td style="width:20px;">foo</td></tr></tbody></table></figure>'
 					);
 				} );
+
+				it( 'should downcast width removal', () => {
+					model.change( writer => writer.setAttribute( 'width', '1337px', tableCell ) );
+
+					model.change( writer => writer.removeAttribute( 'width', tableCell ) );
+
+					assertTableCellStyle( editor );
+				} );
+
+				it( 'should downcast width change', () => {
+					model.change( writer => writer.setAttribute( 'width', '1337px', tableCell ) );
+
+					assertTableCellStyle( editor, 'width:1337px;' );
+
+					model.change( writer => writer.setAttribute( 'width', '1410em', tableCell ) );
+
+					assertTableCellStyle( editor, 'width:1410em;' );
+				} );
 			} );
 		} );
 
@@ -814,6 +1077,26 @@ describe( 'table cell properties', () => {
 					tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
 				} );
 
+				it( 'should consume converted item height attribute', () => {
+					editor.conversion.for( 'downcast' )
+						.add( dispatcher => dispatcher.on( 'attribute:height:tableCell', ( evt, data, conversionApi ) => {
+							expect( conversionApi.consumable.consume( data.item, evt.name ) ).to.be.false;
+						} ) );
+
+					model.change( writer => writer.setAttribute( 'height', '40px', tableCell ) );
+				} );
+
+				it( 'should be overridable', () => {
+					editor.conversion.for( 'downcast' )
+						.add( dispatcher => dispatcher.on( 'attribute:height:tableCell', ( evt, data, conversionApi ) => {
+							conversionApi.consumable.consume( data.item, evt.name );
+						}, { priority: 'high' } ) );
+
+					model.change( writer => writer.setAttribute( 'height', '40px', tableCell ) );
+
+					assertTableCellStyle( editor, '' );
+				} );
+
 				it( 'should downcast height attribute', () => {
 					model.change( writer => writer.setAttribute( 'height', '20px', tableCell ) );
 
@@ -822,6 +1105,24 @@ describe( 'table cell properties', () => {
 						'<figure class="table"><table><tbody><tr><td style="height:20px;">foo</td></tr></tbody></table></figure>'
 					);
 				} );
+
+				it( 'should downcast height removal', () => {
+					model.change( writer => writer.setAttribute( 'height', '1337px', tableCell ) );
+
+					model.change( writer => writer.removeAttribute( 'height', tableCell ) );
+
+					assertTableCellStyle( editor );
+				} );
+
+				it( 'should downcast height change', () => {
+					model.change( writer => writer.setAttribute( 'height', '1337px', tableCell ) );
+
+					assertTableCellStyle( editor, 'height:1337px;' );
+
+					model.change( writer => writer.setAttribute( 'height', '1410em', tableCell ) );
+
+					assertTableCellStyle( editor, 'height:1410em;' );
+				} );
 			} );
 		} );
 	} );

+ 330 - 55
packages/ckeditor5-table/tests/tablecellproperties/tablecellpropertiesui.js

@@ -20,15 +20,18 @@ import Table from '../../src/table';
 import TableCellPropertiesEditing from '../../src/tablecellproperties/tablecellpropertiesediting';
 import TableCellPropertiesUI from '../../src/tablecellproperties/tablecellpropertiesui';
 import TableCellPropertiesUIView from '../../src/tablecellproperties/ui/tablecellpropertiesview';
+import { defaultColors } from '../../src/ui/utils';
 
 describe( 'table cell properties', () => {
 	describe( 'TableCellPropertiesUI', () => {
 		let editor, editorElement, contextualBalloon,
-			tableCellPropertiesUI, tableCellPropertiesView, tableCellPropertiesButton;
+			tableCellPropertiesUI, tableCellPropertiesView, tableCellPropertiesButton,
+			clock;
 
 		testUtils.createSinonSandbox();
 
 		beforeEach( () => {
+			clock = sinon.useFakeTimers();
 			editorElement = document.createElement( 'div' );
 			document.body.appendChild( editorElement );
 
@@ -52,6 +55,7 @@ describe( 'table cell properties', () => {
 		} );
 
 		afterEach( () => {
+			clock.restore();
 			editorElement.remove();
 
 			return editor.destroy();
@@ -65,6 +69,15 @@ describe( 'table cell properties', () => {
 			expect( editor.plugins.get( ContextualBalloon ) ).to.be.instanceOf( ContextualBalloon );
 		} );
 
+		describe( 'constructor()', () => {
+			it( 'should define table.tableCellProperties config', () => {
+				expect( editor.config.get( 'table.tableCellProperties' ) ).to.deep.equal( {
+					borderColors: defaultColors,
+					backgroundColors: defaultColors
+				} );
+			} );
+		} );
+
 		describe( 'init()', () => {
 			it( 'should set a batch', () => {
 				expect( tableCellPropertiesUI._undoStepBatch ).to.be.null;
@@ -78,6 +91,14 @@ describe( 'table cell properties', () => {
 				it( 'should be rendered', () => {
 					expect( tableCellPropertiesUI.view.isRendered ).to.be.true;
 				} );
+
+				it( 'should get the border colors configurations', () => {
+					expect( tableCellPropertiesView.options.borderColors ).to.have.length( 15 );
+				} );
+
+				it( 'should get the background colors configurations', () => {
+					expect( tableCellPropertiesView.options.backgroundColors ).to.have.length( 15 );
+				} );
 			} );
 
 			describe( 'toolbar button', () => {
@@ -127,45 +148,64 @@ describe( 'table cell properties', () => {
 				expect( contextualBalloon.visibleView ).to.be.null;
 			} );
 
-			it( 'should undo the entire batch of changes on #cancel', () => {
-				// Show the view. New batch will be created.
-				tableCellPropertiesButton.fire( 'execute' );
+			describe( '#cancel event', () => {
+				// https://github.com/ckeditor/ckeditor5/issues/6180
+				it( 'should not undo if it there were no changes made to the property fields', () => {
+					const spy = sinon.spy( editor, 'execute' );
 
-				// Do the changes like a user.
-				tableCellPropertiesView.borderStyle = 'dotted';
-				tableCellPropertiesView.backgroundColor = 'red';
-
-				expect( getModelData( editor.model ) ).to.equal(
-					'<table>' +
-						'<tableRow>' +
-							'<tableCell backgroundColor="red" borderStyle="dotted">' +
-								'<paragraph>[]foo</paragraph>' +
-							'</tableCell>' +
-						'</tableRow>' +
-					'</table>' +
-					'<paragraph>bar</paragraph>'
-				);
-
-				tableCellPropertiesView.fire( 'cancel' );
-
-				expect( getModelData( editor.model ) ).to.equal(
-					'<table>' +
-						'<tableRow>' +
-							'<tableCell>' +
-								'<paragraph>[]foo</paragraph>' +
-							'</tableCell>' +
-						'</tableRow>' +
-					'</table>' +
-					'<paragraph>bar</paragraph>'
-				);
-			} );
+					// Show the view. New batch will be created.
+					tableCellPropertiesButton.fire( 'execute' );
 
-			it( 'should hide on #cancel', () => {
-				tableCellPropertiesButton.fire( 'execute' );
-				expect( contextualBalloon.visibleView ).to.equal( tableCellPropertiesView );
+					// Cancel the view immediately.
+					tableCellPropertiesView.fire( 'cancel' );
 
-				tableCellPropertiesView.fire( 'cancel' );
-				expect( contextualBalloon.visibleView ).to.be.null;
+					sinon.assert.notCalled( spy );
+				} );
+
+				it( 'should undo the entire batch of changes if there were some', () => {
+					const spy = sinon.spy( editor, 'execute' );
+
+					// Show the view. New batch will be created.
+					tableCellPropertiesButton.fire( 'execute' );
+
+					// Do the changes like a user.
+					tableCellPropertiesView.borderStyle = 'dotted';
+					tableCellPropertiesView.backgroundColor = 'red';
+
+					expect( getModelData( editor.model ) ).to.equal(
+						'<table>' +
+							'<tableRow>' +
+								'<tableCell backgroundColor="red" borderStyle="dotted">' +
+									'<paragraph>[]foo</paragraph>' +
+								'</tableCell>' +
+							'</tableRow>' +
+						'</table>' +
+						'<paragraph>bar</paragraph>'
+					);
+
+					tableCellPropertiesView.fire( 'cancel' );
+
+					expect( getModelData( editor.model ) ).to.equal(
+						'<table>' +
+							'<tableRow>' +
+								'<tableCell>' +
+									'<paragraph>[]foo</paragraph>' +
+								'</tableCell>' +
+							'</tableRow>' +
+						'</table>' +
+						'<paragraph>bar</paragraph>'
+					);
+
+					sinon.assert.calledWith( spy, 'undo', tableCellPropertiesUI._undoStepBatch );
+				} );
+
+				it( 'should hide the view', () => {
+					tableCellPropertiesButton.fire( 'execute' );
+					expect( contextualBalloon.visibleView ).to.equal( tableCellPropertiesView );
+
+					tableCellPropertiesView.fire( 'cancel' );
+					expect( contextualBalloon.visibleView ).to.be.null;
+				} );
 			} );
 
 			it( 'should hide on the Esc key press', () => {
@@ -215,14 +255,242 @@ describe( 'table cell properties', () => {
 			} );
 
 			describe( 'property changes', () => {
-				it( 'should affect the editor state', () => {
-					const spy = testUtils.sinon.stub( editor, 'execute' );
-
+				beforeEach( () => {
 					tableCellPropertiesUI._undoStepBatch = 'foo';
-					tableCellPropertiesView.borderStyle = 'dotted';
+				} );
 
-					sinon.assert.calledOnce( spy );
-					sinon.assert.calledWithExactly( spy, 'tableCellBorderStyle', { value: 'dotted', batch: 'foo' } );
+				describe( '#borderStyle', () => {
+					it( 'should affect the editor state', () => {
+						const spy = testUtils.sinon.stub( editor, 'execute' );
+
+						tableCellPropertiesView.borderStyle = 'dotted';
+
+						sinon.assert.calledOnce( spy );
+						sinon.assert.calledWithExactly( spy, 'tableCellBorderStyle', { value: 'dotted', batch: 'foo' } );
+					} );
+				} );
+
+				describe( '#borderColor', () => {
+					it( 'should affect the editor state', () => {
+						const spy = testUtils.sinon.stub( editor, 'execute' );
+
+						tableCellPropertiesView.borderColor = '#FFAAFF';
+
+						sinon.assert.calledOnce( spy );
+						sinon.assert.calledWithExactly( spy, 'tableCellBorderColor', { value: '#FFAAFF', batch: 'foo' } );
+					} );
+
+					it( 'should display an error message if value is invalid', () => {
+						const spy = testUtils.sinon.stub( editor, 'execute' );
+
+						// First, let's pass an invalid value and check what happens.
+						tableCellPropertiesView.borderColor = '42';
+
+						clock.tick( 500 );
+
+						expect( tableCellPropertiesView.borderColorInput.errorText ).to.match( /^The color is invalid/ );
+						sinon.assert.notCalled( spy );
+
+						// And now let's pass a valid value and check if the error text will be gone.
+						tableCellPropertiesView.borderColor = '#AAA';
+
+						clock.tick( 500 );
+
+						expect( tableCellPropertiesView.borderColorInput.errorText ).to.be.null;
+						sinon.assert.calledWithExactly( spy, 'tableCellBorderColor', { value: '#AAA', batch: 'foo' } );
+					} );
+				} );
+
+				describe( '#borderWidth', () => {
+					it( 'should affect the editor state', () => {
+						const spy = testUtils.sinon.stub( editor, 'execute' );
+
+						tableCellPropertiesView.borderWidth = '12px';
+
+						sinon.assert.calledOnce( spy );
+						sinon.assert.calledWithExactly( spy, 'tableCellBorderWidth', { value: '12px', batch: 'foo' } );
+					} );
+
+					it( 'should display an error message if value is invalid', () => {
+						const spy = testUtils.sinon.stub( editor, 'execute' );
+
+						// First, let's pass an invalid value and check what happens.
+						tableCellPropertiesView.borderWidth = 'wrong';
+
+						clock.tick( 500 );
+
+						expect( tableCellPropertiesView.borderWidthInput.errorText ).to.match( /^The value is invalid/ );
+						sinon.assert.notCalled( spy );
+
+						// And now let's pass a valid value and check if the error text will be gone.
+						tableCellPropertiesView.borderWidth = '3em';
+
+						clock.tick( 500 );
+
+						expect( tableCellPropertiesView.backgroundInput.errorText ).to.be.null;
+						sinon.assert.calledWithExactly( spy, 'tableCellBorderWidth', { value: '3em', batch: 'foo' } );
+					} );
+				} );
+
+				describe( '#width', () => {
+					it( 'should affect the editor state', () => {
+						const spy = testUtils.sinon.stub( editor, 'execute' );
+
+						tableCellPropertiesView.width = '12px';
+
+						sinon.assert.calledOnce( spy );
+						sinon.assert.calledWithExactly( spy, 'tableCellWidth', { value: '12px', batch: 'foo' } );
+					} );
+
+					it( 'should display an error message if value is invalid', () => {
+						const spy = testUtils.sinon.stub( editor, 'execute' );
+
+						// First, let's pass an invalid value and check what happens.
+						tableCellPropertiesView.width = 'wrong';
+
+						clock.tick( 500 );
+
+						expect( tableCellPropertiesView.widthInput.errorText ).to.match( /^The value is invalid/ );
+						sinon.assert.notCalled( spy );
+
+						// And now let's pass a valid value and check if the error text will be gone.
+						tableCellPropertiesView.width = '3em';
+
+						clock.tick( 500 );
+
+						expect( tableCellPropertiesView.backgroundInput.errorText ).to.be.null;
+						sinon.assert.calledWithExactly( spy, 'tableCellWidth', { value: '3em', batch: 'foo' } );
+					} );
+				} );
+
+				describe( '#height', () => {
+					it( 'should affect the editor state', () => {
+						const spy = testUtils.sinon.stub( editor, 'execute' );
+
+						tableCellPropertiesView.height = '12px';
+
+						sinon.assert.calledOnce( spy );
+						sinon.assert.calledWithExactly( spy, 'tableCellHeight', { value: '12px', batch: 'foo' } );
+					} );
+
+					it( 'should display an error message if value is invalid', () => {
+						const spy = testUtils.sinon.stub( editor, 'execute' );
+
+						// First, let's pass an invalid value and check what happens.
+						tableCellPropertiesView.height = 'wrong';
+
+						clock.tick( 500 );
+
+						expect( tableCellPropertiesView.heightInput.errorText ).to.match( /^The value is invalid/ );
+						sinon.assert.notCalled( spy );
+
+						// And now let's pass a valid value and check if the error text will be gone.
+						tableCellPropertiesView.height = '3em';
+
+						clock.tick( 500 );
+
+						expect( tableCellPropertiesView.backgroundInput.errorText ).to.be.null;
+						sinon.assert.calledWithExactly( spy, 'tableCellHeight', { value: '3em', batch: 'foo' } );
+					} );
+				} );
+
+				describe( '#padding', () => {
+					it( 'should affect the editor state', () => {
+						const spy = testUtils.sinon.stub( editor, 'execute' );
+
+						tableCellPropertiesView.padding = '12px';
+
+						sinon.assert.calledOnce( spy );
+						sinon.assert.calledWithExactly( spy, 'tableCellPadding', { value: '12px', batch: 'foo' } );
+					} );
+
+					it( 'should display an error message if value is invalid', () => {
+						const spy = testUtils.sinon.stub( editor, 'execute' );
+
+						// First, let's pass an invalid value and check what happens.
+						tableCellPropertiesView.padding = 'wrong';
+
+						clock.tick( 500 );
+
+						expect( tableCellPropertiesView.paddingInput.errorText ).to.match( /^The value is invalid/ );
+						sinon.assert.notCalled( spy );
+
+						// And now let's pass a valid value and check if the error text will be gone.
+						tableCellPropertiesView.padding = '3em';
+
+						clock.tick( 500 );
+
+						expect( tableCellPropertiesView.backgroundInput.errorText ).to.be.null;
+						sinon.assert.calledWithExactly( spy, 'tableCellPadding', { value: '3em', batch: 'foo' } );
+					} );
+				} );
+
+				describe( '#backgroundColor', () => {
+					it( 'should affect the editor state', () => {
+						const spy = testUtils.sinon.stub( editor, 'execute' );
+
+						tableCellPropertiesView.backgroundColor = '#FFAAFF';
+
+						sinon.assert.calledOnce( spy );
+						sinon.assert.calledWithExactly( spy, 'tableCellBackgroundColor', { value: '#FFAAFF', batch: 'foo' } );
+					} );
+
+					it( 'should display an error message if value is invalid', () => {
+						const spy = testUtils.sinon.stub( editor, 'execute' );
+
+						// First, let's pass an invalid value and check what happens.
+						tableCellPropertiesView.backgroundColor = '42';
+
+						clock.tick( 500 );
+
+						expect( tableCellPropertiesView.backgroundInput.errorText ).to.match( /^The color is invalid/ );
+						sinon.assert.notCalled( spy );
+
+						// And now let's pass a valid value and check if the error text will be gone.
+						tableCellPropertiesView.backgroundColor = '#AAA';
+
+						clock.tick( 500 );
+
+						expect( tableCellPropertiesView.backgroundInput.errorText ).to.be.null;
+						sinon.assert.calledWithExactly( spy, 'tableCellBackgroundColor', { value: '#AAA', batch: 'foo' } );
+					} );
+				} );
+
+				describe( '#horizontalAlignment', () => {
+					it( 'should affect the editor state', () => {
+						const spy = testUtils.sinon.stub( editor, 'execute' );
+
+						tableCellPropertiesView.horizontalAlignment = 'right';
+
+						sinon.assert.calledOnce( spy );
+						sinon.assert.calledWithExactly( spy, 'tableCellHorizontalAlignment', { value: 'right', batch: 'foo' } );
+					} );
+				} );
+
+				describe( '#verticalAlignment', () => {
+					it( 'should affect the editor state', () => {
+						const spy = testUtils.sinon.stub( editor, 'execute' );
+
+						tableCellPropertiesView.verticalAlignment = 'right';
+
+						sinon.assert.calledOnce( spy );
+						sinon.assert.calledWithExactly( spy, 'tableCellVerticalAlignment', { value: 'right', batch: 'foo' } );
+					} );
+				} );
+
+				it( 'should not display an error text if user managed to fix the value before the UI timeout', () => {
+					// First, let's pass an invalid value.
+					tableCellPropertiesView.borderColor = '#';
+
+					clock.tick( 100 );
+
+					// Then the user managed to quickly type the correct value.
+					tableCellPropertiesView.borderColor = '#aaa';
+
+					clock.tick( 400 );
+
+					// Because they were quick, they should see no error
+					expect( tableCellPropertiesView.borderColorInput.errorText ).to.be.null;
 				} );
 
 				it( 'should not affect the editor state if internal property has changed', () => {
@@ -230,7 +498,6 @@ describe( 'table cell properties', () => {
 
 					tableCellPropertiesView.set( 'internalProp', 'foo' );
 
-					tableCellPropertiesUI._undoStepBatch = 'foo';
 					tableCellPropertiesView.internalProp = 'bar';
 
 					sinon.assert.notCalled( spy );
@@ -267,10 +534,12 @@ describe( 'table cell properties', () => {
 					editor.commands.get( 'tableCellBorderStyle' ).value = 'a';
 					editor.commands.get( 'tableCellBorderColor' ).value = 'b';
 					editor.commands.get( 'tableCellBorderWidth' ).value = 'c';
-					editor.commands.get( 'tableCellPadding' ).value = 'd';
-					editor.commands.get( 'tableCellBackgroundColor' ).value = 'e';
-					editor.commands.get( 'tableCellHorizontalAlignment' ).value = 'f';
-					editor.commands.get( 'tableCellVerticalAlignment' ).value = 'g';
+					editor.commands.get( 'tableCellWidth' ).value = 'd';
+					editor.commands.get( 'tableCellHeight' ).value = 'e';
+					editor.commands.get( 'tableCellPadding' ).value = 'f';
+					editor.commands.get( 'tableCellBackgroundColor' ).value = 'g';
+					editor.commands.get( 'tableCellHorizontalAlignment' ).value = 'h';
+					editor.commands.get( 'tableCellVerticalAlignment' ).value = 'i';
 
 					tableCellPropertiesButton.fire( 'execute' );
 
@@ -279,10 +548,12 @@ describe( 'table cell properties', () => {
 						borderStyle: 'a',
 						borderColor: 'b',
 						borderWidth: 'c',
-						padding: 'd',
-						backgroundColor: 'e',
-						horizontalAlignment: 'f',
-						verticalAlignment: 'g'
+						width: 'd',
+						height: 'e',
+						padding: 'f',
+						backgroundColor: 'g',
+						horizontalAlignment: 'h',
+						verticalAlignment: 'i'
 					} );
 				} );
 
@@ -290,6 +561,8 @@ describe( 'table cell properties', () => {
 					editor.commands.get( 'tableCellBorderStyle' ).value = null;
 					editor.commands.get( 'tableCellBorderColor' ).value = null;
 					editor.commands.get( 'tableCellBorderWidth' ).value = null;
+					editor.commands.get( 'tableCellWidth' ).value = null;
+					editor.commands.get( 'tableCellHeight' ).value = null;
 					editor.commands.get( 'tableCellPadding' ).value = null;
 					editor.commands.get( 'tableCellBackgroundColor' ).value = null;
 					editor.commands.get( 'tableCellHorizontalAlignment' ).value = null;
@@ -299,13 +572,15 @@ describe( 'table cell properties', () => {
 
 					expect( contextualBalloon.visibleView ).to.equal( tableCellPropertiesView );
 					expect( tableCellPropertiesView ).to.include( {
-						borderStyle: 'none',
+						borderStyle: '',
 						borderColor: '',
 						borderWidth: '',
+						width: '',
+						height: '',
 						padding: '',
 						backgroundColor: '',
-						horizontalAlignment: 'left',
-						verticalAlignment: 'middle'
+						horizontalAlignment: '',
+						verticalAlignment: ''
 					} );
 				} );
 			} );

+ 216 - 22
packages/ckeditor5-table/tests/tablecellproperties/ui/tablecellpropertiesview.js

@@ -16,6 +16,29 @@ import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import ToolbarView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarview';
 import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
 import InputTextView from '@ckeditor/ckeditor5-ui/src/inputtext/inputtextview';
+import ColorInputView from '../../../src/ui/colorinputview';
+
+const VIEW_OPTIONS = {
+	borderColors: [
+		{
+			model: 'rgb(255,0,0)',
+			label: 'Red',
+			hasBorder: false
+		},
+		{
+			model: 'rgb(0,0,255)',
+			label: 'Blue',
+			hasBorder: false
+		}
+	],
+	backgroundColors: [
+		{
+			model: 'rgb(0,255,0)',
+			label: 'Green',
+			hasBorder: false
+		},
+	]
+};
 
 describe( 'table cell properties', () => {
 	describe( 'TableCellPropertiesView', () => {
@@ -25,7 +48,7 @@ describe( 'table cell properties', () => {
 
 		beforeEach( () => {
 			locale = { t: val => val };
-			view = new TableCellPropertiesView( locale );
+			view = new TableCellPropertiesView( locale, VIEW_OPTIONS );
 			view.render();
 		} );
 
@@ -34,6 +57,10 @@ describe( 'table cell properties', () => {
 		} );
 
 		describe( 'constructor()', () => {
+			it( 'should set view#options', () => {
+				expect( view.options ).to.deep.equal( VIEW_OPTIONS );
+			} );
+
 			it( 'should set view#locale', () => {
 				expect( view.locale ).to.equal( locale );
 			} );
@@ -44,13 +71,13 @@ describe( 'table cell properties', () => {
 
 			it( 'should define the public data interface (observable properties)', () => {
 				expect( view ).to.include( {
-					borderStyle: 'none',
+					borderStyle: '',
 					borderWidth: '',
 					borderColor: '',
 					padding: '',
 					backgroundColor: '',
-					horizontalAlignment: 'left',
-					verticalAlignment: 'middle'
+					horizontalAlignment: '',
+					verticalAlignment: ''
 				} );
 			} );
 
@@ -124,7 +151,7 @@ describe( 'table cell properties', () => {
 
 						it( 'should change #borderStyle when executed', () => {
 							labeledDropdown.view.listView.items.first.children.first.fire( 'execute' );
-							expect( view.borderStyle ).to.equal( 'none' );
+							expect( view.borderStyle ).to.equal( '' );
 
 							labeledDropdown.view.listView.items.last.children.first.fire( 'execute' );
 							expect( view.borderStyle ).to.equal( 'outset' );
@@ -134,9 +161,20 @@ describe( 'table cell properties', () => {
 							expect( labeledDropdown.view.listView.items.map( item => {
 								return item.children.first.label;
 							} ) ).to.have.ordered.members( [
-								'None', 'Solid', 'Dotted', 'Dashed', 'Double', 'Groove', 'Ridge', 'Inset', 'Outset',
+								'None', 'Solid', 'Dotted', 'Dashed', 'Double', 'Groove', 'Ridge', 'Inset', 'Outset'
 							] );
 						} );
+
+						it( 'should reset border width and color inputs when setting style to none', () => {
+							view.borderStyle = 'dotted';
+							view.borderWidth = '1px';
+							view.borderColor = 'red';
+
+							view.borderStyle = '';
+
+							expect( view.borderColor ).to.equal( '' );
+							expect( view.borderWidth ).to.equal( '' );
+						} );
 					} );
 
 					describe( 'border width input', () => {
@@ -161,7 +199,7 @@ describe( 'table cell properties', () => {
 						} );
 
 						it( 'should be enabled only when #borderStyle is different than "none"', () => {
-							view.borderStyle = 'none';
+							view.borderStyle = '';
 							expect( labeledInput.isEnabled ).to.be.false;
 
 							view.borderStyle = 'dotted';
@@ -187,10 +225,29 @@ describe( 'table cell properties', () => {
 						} );
 
 						it( 'should be created', () => {
-							expect( labeledInput.view ).to.be.instanceOf( InputTextView );
+							expect( labeledInput.view ).to.be.instanceOf( ColorInputView );
 							expect( labeledInput.label ).to.equal( 'Color' );
 						} );
 
+						it( 'should get the color configuration', () => {
+							expect( labeledInput.view.options.colorDefinitions ).to.deep.equal( [
+								{
+									color: 'rgb(255,0,0)',
+									label: 'Red',
+									options: {
+										hasBorder: false
+									}
+								},
+								{
+									color: 'rgb(0,0,255)',
+									label: 'Blue',
+									options: {
+										hasBorder: false
+									}
+								}
+							] );
+						} );
+
 						it( 'should reflect #borderColor property', () => {
 							view.borderColor = 'foo';
 							expect( labeledInput.view.value ).to.equal( 'foo' );
@@ -200,7 +257,7 @@ describe( 'table cell properties', () => {
 						} );
 
 						it( 'should be enabled only when #borderStyle is different than "none"', () => {
-							view.borderStyle = 'none';
+							view.borderStyle = '';
 							expect( labeledInput.isEnabled ).to.be.false;
 
 							view.borderStyle = 'dotted';
@@ -208,24 +265,23 @@ describe( 'table cell properties', () => {
 						} );
 
 						it( 'should update #borderColor on DOM "input" event', () => {
-							labeledInput.view.element.value = 'foo';
+							labeledInput.view.value = 'foo';
 							labeledInput.view.fire( 'input' );
 							expect( view.borderColor ).to.equal( 'foo' );
 
-							labeledInput.view.element.value = 'bar';
+							labeledInput.view.value = 'bar';
 							labeledInput.view.fire( 'input' );
 							expect( view.borderColor ).to.equal( 'bar' );
 						} );
 					} );
 				} );
 
-				describe( 'background and padding row', () => {
+				describe( 'background row', () => {
 					it( 'should be defined', () => {
 						const row = view.element.childNodes[ 2 ];
 
 						expect( row.classList.contains( 'ck-form__row' ) ).to.be.true;
 						expect( row.childNodes[ 0 ] ).to.equal( view.backgroundInput.element );
-						expect( row.childNodes[ 1 ] ).to.equal( view.paddingInput.element );
 					} );
 
 					describe( 'background color input', () => {
@@ -236,11 +292,23 @@ describe( 'table cell properties', () => {
 						} );
 
 						it( 'should be created', () => {
-							expect( labeledInput.view ).to.be.instanceOf( InputTextView );
+							expect( labeledInput.view ).to.be.instanceOf( ColorInputView );
 							expect( labeledInput.label ).to.equal( 'Background' );
 							expect( labeledInput.class ).to.equal( 'ck-table-cell-properties-form__background' );
 						} );
 
+						it( 'should get the color configuration', () => {
+							expect( labeledInput.view.options.colorDefinitions ).to.deep.equal( [
+								{
+									color: 'rgb(0,255,0)',
+									label: 'Green',
+									options: {
+										hasBorder: false
+									}
+								}
+							] );
+						} );
+
 						it( 'should reflect #backgroundColor property', () => {
 							view.backgroundColor = 'foo';
 							expect( labeledInput.view.value ).to.equal( 'foo' );
@@ -250,15 +318,102 @@ describe( 'table cell properties', () => {
 						} );
 
 						it( 'should update #backgroundColor on DOM "input" event', () => {
-							labeledInput.view.element.value = 'foo';
+							labeledInput.view.value = 'foo';
 							labeledInput.view.fire( 'input' );
 							expect( view.backgroundColor ).to.equal( 'foo' );
 
-							labeledInput.view.element.value = 'bar';
+							labeledInput.view.value = 'bar';
 							labeledInput.view.fire( 'input' );
 							expect( view.backgroundColor ).to.equal( 'bar' );
 						} );
 					} );
+				} );
+
+				describe( 'dimensions row', () => {
+					it( 'should be defined', () => {
+						const row = view.element.childNodes[ 3 ].childNodes[ 0 ];
+
+						expect( row.classList.contains( 'ck-form__row' ) ).to.be.true;
+						expect( row.classList.contains( 'ck-table-form__dimensions-row' ) ).to.be.true;
+						expect( row.childNodes[ 0 ].textContent ).to.equal( 'Dimensions' );
+						expect( row.childNodes[ 1 ] ).to.equal( view.widthInput.element );
+						expect( row.childNodes[ 2 ].textContent ).to.equal( '×' );
+						expect( row.childNodes[ 3 ] ).to.equal( view.heightInput.element );
+					} );
+
+					describe( 'width input', () => {
+						let labeledInput;
+
+						beforeEach( () => {
+							labeledInput = view.widthInput;
+						} );
+
+						it( 'should be created', () => {
+							expect( labeledInput.view ).to.be.instanceOf( InputTextView );
+							expect( labeledInput.label ).to.equal( 'Width' );
+							expect( labeledInput.class ).to.equal( 'ck-table-form__dimensions-row__width' );
+						} );
+
+						it( 'should reflect #width property', () => {
+							view.width = 'foo';
+							expect( labeledInput.view.value ).to.equal( 'foo' );
+
+							view.width = 'bar';
+							expect( labeledInput.view.value ).to.equal( 'bar' );
+						} );
+
+						it( 'should update #width on DOM "input" event', () => {
+							labeledInput.view.element.value = 'foo';
+							labeledInput.view.fire( 'input' );
+							expect( view.width ).to.equal( 'foo' );
+
+							labeledInput.view.element.value = 'bar';
+							labeledInput.view.fire( 'input' );
+							expect( view.width ).to.equal( 'bar' );
+						} );
+					} );
+
+					describe( 'height input', () => {
+						let labeledInput;
+
+						beforeEach( () => {
+							labeledInput = view.heightInput;
+						} );
+
+						it( 'should be created', () => {
+							expect( labeledInput.view ).to.be.instanceOf( InputTextView );
+							expect( labeledInput.label ).to.equal( 'Height' );
+							expect( labeledInput.class ).to.equal( 'ck-table-form__dimensions-row__height' );
+						} );
+
+						it( 'should reflect #height property', () => {
+							view.height = 'foo';
+							expect( labeledInput.view.value ).to.equal( 'foo' );
+
+							view.height = 'bar';
+							expect( labeledInput.view.value ).to.equal( 'bar' );
+						} );
+
+						it( 'should update #height on DOM "input" event', () => {
+							labeledInput.view.element.value = 'foo';
+							labeledInput.view.fire( 'input' );
+							expect( view.height ).to.equal( 'foo' );
+
+							labeledInput.view.element.value = 'bar';
+							labeledInput.view.fire( 'input' );
+							expect( view.height ).to.equal( 'bar' );
+						} );
+					} );
+				} );
+
+				describe( 'padding row', () => {
+					it( 'should be defined', () => {
+						const row = view.element.childNodes[ 3 ].childNodes[ 1 ];
+
+						expect( row.classList.contains( 'ck-form__row' ) ).to.be.true;
+						expect( row.classList.contains( 'ck-table-cell-properties-form__padding-row' ) ).to.be.true;
+						expect( row.childNodes[ 0 ] ).to.equal( view.paddingInput.element );
+					} );
 
 					describe( 'padding input', () => {
 						let labeledInput;
@@ -295,11 +450,11 @@ describe( 'table cell properties', () => {
 
 				describe( 'text alignment row', () => {
 					it( 'should be defined', () => {
-						const row = view.element.childNodes[ 3 ];
+						const row = view.element.childNodes[ 4 ];
 
 						expect( row.classList.contains( 'ck-form__row' ) ).to.be.true;
 						expect( row.classList.contains( 'ck-table-cell-properties-form__alignment-row' ) ).to.be.true;
-						expect( row.childNodes[ 0 ].textContent ).to.equal( 'Text alignment' );
+						expect( row.childNodes[ 0 ].textContent ).to.equal( 'Table cell text alignment' );
 						expect( row.childNodes[ 1 ] ).to.equal( view.horizontalAlignmentToolbar.element );
 						expect( row.childNodes[ 2 ] ).to.equal( view.verticalAlignmentToolbar.element );
 					} );
@@ -338,7 +493,7 @@ describe( 'table cell properties', () => {
 							expect( toolbar.items.last.isOn ).to.be.true;
 
 							toolbar.items.first.fire( 'execute' );
-							expect( view.horizontalAlignment ).to.equal( 'left' );
+							expect( view.horizontalAlignment ).to.equal( '' );
 							expect( toolbar.items.last.isOn ).to.be.false;
 							expect( toolbar.items.first.isOn ).to.be.true;
 						} );
@@ -386,7 +541,7 @@ describe( 'table cell properties', () => {
 
 				describe( 'action row', () => {
 					it( 'should be defined', () => {
-						const row = view.element.childNodes[ 4 ];
+						const row = view.element.childNodes[ 5 ];
 
 						expect( row.classList.contains( 'ck-form__row' ) ).to.be.true;
 						expect( row.classList.contains( 'ck-table-form__action-row' ) ).to.be.true;
@@ -414,6 +569,43 @@ describe( 'table cell properties', () => {
 
 						expect( spy.calledOnce ).to.be.true;
 					} );
+
+					it( 'should make sure the #saveButtonView is disabled until text fields are without errors', () => {
+						view.borderWidthInput.errorText = 'foo';
+						view.borderColorInput.errorText = 'foo';
+						view.backgroundInput.errorText = 'foo';
+						view.paddingInput.errorText = 'foo';
+
+						expect( view.saveButtonView.isEnabled ).to.be.false;
+
+						view.borderWidthInput.errorText = 'foo';
+						view.borderColorInput.errorText = 'foo';
+						view.backgroundInput.errorText = 'foo';
+						view.paddingInput.errorText = null;
+
+						expect( view.saveButtonView.isEnabled ).to.be.false;
+
+						view.borderWidthInput.errorText = 'foo';
+						view.borderColorInput.errorText = 'foo';
+						view.backgroundInput.errorText = null;
+						view.paddingInput.errorText = null;
+
+						expect( view.saveButtonView.isEnabled ).to.be.false;
+
+						view.borderWidthInput.errorText = 'foo';
+						view.borderColorInput.errorText = null;
+						view.backgroundInput.errorText = null;
+						view.paddingInput.errorText = null;
+
+						expect( view.saveButtonView.isEnabled ).to.be.false;
+
+						view.borderWidthInput.errorText = null;
+						view.borderColorInput.errorText = null;
+						view.backgroundInput.errorText = null;
+						view.paddingInput.errorText = null;
+
+						expect( view.saveButtonView.isEnabled ).to.be.true;
+					} );
 				} );
 			} );
 
@@ -441,6 +633,8 @@ describe( 'table cell properties', () => {
 					view.borderColorInput,
 					view.borderWidthInput,
 					view.backgroundInput,
+					view.widthInput,
+					view.heightInput,
 					view.paddingInput,
 					view.horizontalAlignmentToolbar,
 					view.verticalAlignmentToolbar,
@@ -451,7 +645,7 @@ describe( 'table cell properties', () => {
 
 			it( 'should register child views\' #element in #focusTracker', () => {
 				const spy = testUtils.sinon.spy( FocusTracker.prototype, 'add' );
-				const view = new TableCellPropertiesView( { t: val => val } );
+				const view = new TableCellPropertiesView( { t: val => val }, VIEW_OPTIONS );
 				view.render();
 
 				sinon.assert.calledWith( spy, view.borderStyleDropdown.element );
@@ -468,7 +662,7 @@ describe( 'table cell properties', () => {
 			} );
 
 			it( 'starts listening for #keystrokes coming from #element', () => {
-				const view = new TableCellPropertiesView( { t: val => val } );
+				const view = new TableCellPropertiesView( { t: val => val }, VIEW_OPTIONS );
 				const spy = sinon.spy( view.keystrokes, 'listenTo' );
 
 				view.render();

+ 4 - 4
packages/ckeditor5-table/tests/tableproperties/commands/tablealignmentcommand.js

@@ -102,7 +102,7 @@ describe( 'table properties', () => {
 
 						command.execute( { value: 'right' } );
 
-						assertTableStyle( editor, 'margin-left:auto;margin-right:0;' );
+						assertTableStyle( editor, null, 'float:right;' );
 					} );
 
 					it( 'should change selected table alignment to a passed value', () => {
@@ -110,7 +110,7 @@ describe( 'table properties', () => {
 
 						command.execute( { value: 'right' } );
 
-						assertTableStyle( editor, 'margin-left:auto;margin-right:0;' );
+						assertTableStyle( editor, null, 'float:right;' );
 					} );
 
 					it( 'should remove alignment from a selected table if no value is passed', () => {
@@ -128,7 +128,7 @@ describe( 'table properties', () => {
 
 						command.execute( { value: 'right' } );
 
-						assertTableStyle( editor, 'margin-left:auto;margin-right:0;' );
+						assertTableStyle( editor, null, 'float:right;' );
 					} );
 
 					it( 'should change selected table alignment to a passed value', () => {
@@ -136,7 +136,7 @@ describe( 'table properties', () => {
 
 						command.execute( { value: 'right' } );
 
-						assertTableStyle( editor, 'margin-left:auto;margin-right:0;' );
+						assertTableStyle( editor, null, 'float:right;' );
 					} );
 
 					it( 'should remove alignment from a selected table if no value is passed', () => {

+ 56 - 0
packages/ckeditor5-table/tests/tableproperties/commands/tableborderwidthcommand.js

@@ -121,6 +121,62 @@ describe( 'table properties', () => {
 					sinon.assert.calledWith( spy, batch );
 				} );
 
+				it( 'should add default unit for numeric values (number passed)', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+					command.execute( { value: 25 } );
+
+					assertTableStyle( editor, 'border-bottom:25px;border-left:25px;border-right:25px;border-top:25px;' );
+				} );
+
+				it( 'should add default unit for numeric values (string passed)', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+					command.execute( { value: 25 } );
+
+					assertTableStyle( editor, 'border-bottom:25px;border-left:25px;border-right:25px;border-top:25px;' );
+				} );
+
+				it( 'should not add default unit for numeric values with unit', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+					command.execute( { value: '25pt' } );
+
+					assertTableStyle( editor, 'border-bottom:25pt;border-left:25pt;border-right:25pt;border-top:25pt;' );
+				} );
+
+				it( 'should add default unit to floats (number passed)', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+					command.execute( { value: 25.1 } );
+
+					assertTableStyle( editor, 'border-bottom:25.1px;border-left:25.1px;border-right:25.1px;border-top:25.1px;' );
+				} );
+
+				it( 'should add default unit to floats (string passed)', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+					command.execute( { value: '0.1' } );
+
+					assertTableStyle( editor, 'border-bottom:0.1px;border-left:0.1px;border-right:0.1px;border-top:0.1px;' );
+				} );
+
+				it( 'should pass invalid values', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+					command.execute( { value: 'bar' } );
+
+					assertTableStyle( editor, 'border-bottom:bar;border-left:bar;border-right:bar;border-top:bar;' );
+				} );
+
+				it( 'should pass invalid value (string passed, CSS float without leading 0)', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+					command.execute( { value: '.2' } );
+
+					assertTableStyle( editor, 'border-bottom:.2;border-left:.2;border-right:.2;border-top:.2;' );
+				} );
+
 				describe( 'collapsed selection', () => {
 					it( 'should set selected table borderWidth to a passed value', () => {
 						setData( model, modelTable( [ [ 'foo[]' ] ] ) );

+ 60 - 4
packages/ckeditor5-table/tests/tableproperties/commands/tableheightcommand.js

@@ -96,13 +96,69 @@ describe( 'table properties', () => {
 					sinon.assert.calledWith( spy, batch );
 				} );
 
+				it( 'should add default unit for numeric values (number passed)', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+					command.execute( { value: 25 } );
+
+					assertTableStyle( editor, null, 'height:25px;' );
+				} );
+
+				it( 'should add default unit for numeric values (string passed)', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+					command.execute( { value: 25 } );
+
+					assertTableStyle( editor, null, 'height:25px;' );
+				} );
+
+				it( 'should not add default unit for numeric values with unit', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+					command.execute( { value: '25pt' } );
+
+					assertTableStyle( editor, null, 'height:25pt;' );
+				} );
+
+				it( 'should add default unit to floats (number passed)', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+					command.execute( { value: 25.1 } );
+
+					assertTableStyle( editor, null, 'height:25.1px;' );
+				} );
+
+				it( 'should add default unit to floats (string passed)', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+					command.execute( { value: '0.1' } );
+
+					assertTableStyle( editor, null, 'height:0.1px;' );
+				} );
+
+				it( 'should pass invalid values', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+					command.execute( { value: 'bar' } );
+
+					assertTableStyle( editor, null, 'height:bar;' );
+				} );
+
+				it( 'should pass invalid value (string passed, CSS float without leading 0)', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+					command.execute( { value: '.2' } );
+
+					assertTableStyle( editor, null, 'height:.2;' );
+				} );
+
 				describe( 'collapsed selection', () => {
 					it( 'should set selected table height to a passed value', () => {
 						setData( model, modelTable( [ [ 'foo[]' ] ] ) );
 
 						command.execute( { value: '25px' } );
 
-						assertTableStyle( editor, 'height:25px;' );
+						assertTableStyle( editor, null, 'height:25px;' );
 					} );
 
 					it( 'should change selected table height to a passed value', () => {
@@ -110,7 +166,7 @@ describe( 'table properties', () => {
 
 						command.execute( { value: '25px' } );
 
-						assertTableStyle( editor, 'height:25px;' );
+						assertTableStyle( editor, null, 'height:25px;' );
 					} );
 
 					it( 'should remove height from a selected table if no value is passed', () => {
@@ -128,7 +184,7 @@ describe( 'table properties', () => {
 
 						command.execute( { value: '25px' } );
 
-						assertTableStyle( editor, 'height:25px;' );
+						assertTableStyle( editor, null, 'height:25px;' );
 					} );
 
 					it( 'should change selected table height to a passed value', () => {
@@ -136,7 +192,7 @@ describe( 'table properties', () => {
 
 						command.execute( { value: '25px' } );
 
-						assertTableStyle( editor, 'height:25px;' );
+						assertTableStyle( editor, null, 'height:25px;' );
 					} );
 
 					it( 'should remove height from a selected table if no value is passed', () => {

+ 60 - 4
packages/ckeditor5-table/tests/tableproperties/commands/tablewidthcommand.js

@@ -96,13 +96,69 @@ describe( 'table properties', () => {
 					sinon.assert.calledWith( spy, batch );
 				} );
 
+				it( 'should add default unit for numeric values (number passed)', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+					command.execute( { value: 25 } );
+
+					assertTableStyle( editor, null, 'width:25px;' );
+				} );
+
+				it( 'should add default unit for numeric values (string passed)', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+					command.execute( { value: 25 } );
+
+					assertTableStyle( editor, null, 'width:25px;' );
+				} );
+
+				it( 'should not add default unit for numeric values with unit', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+					command.execute( { value: '25pt' } );
+
+					assertTableStyle( editor, null, 'width:25pt;' );
+				} );
+
+				it( 'should add default unit to floats (number passed)', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+					command.execute( { value: 25.1 } );
+
+					assertTableStyle( editor, null, 'width:25.1px;' );
+				} );
+
+				it( 'should add default unit to floats (string passed)', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+					command.execute( { value: '0.1' } );
+
+					assertTableStyle( editor, null, 'width:0.1px;' );
+				} );
+
+				it( 'should pass invalid values', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+					command.execute( { value: 'bar' } );
+
+					assertTableStyle( editor, null, 'width:bar;' );
+				} );
+
+				it( 'should pass invalid value (string passed, CSS float without leading 0)', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+					command.execute( { value: '.2' } );
+
+					assertTableStyle( editor, null, 'width:.2;' );
+				} );
+
 				describe( 'collapsed selection', () => {
 					it( 'should set selected table width to a passed value', () => {
 						setData( model, modelTable( [ [ 'foo[]' ] ] ) );
 
 						command.execute( { value: '25px' } );
 
-						assertTableStyle( editor, 'width:25px;' );
+						assertTableStyle( editor, null, 'width:25px;' );
 					} );
 
 					it( 'should change selected table width to a passed value', () => {
@@ -110,7 +166,7 @@ describe( 'table properties', () => {
 
 						command.execute( { value: '25px' } );
 
-						assertTableStyle( editor, 'width:25px;' );
+						assertTableStyle( editor, null, 'width:25px;' );
 					} );
 
 					it( 'should remove width from a selected table if no value is passed', () => {
@@ -128,7 +184,7 @@ describe( 'table properties', () => {
 
 						command.execute( { value: '25px' } );
 
-						assertTableStyle( editor, 'width:25px;' );
+						assertTableStyle( editor, null, 'width:25px;' );
 					} );
 
 					it( 'should change selected table width to a passed value', () => {
@@ -136,7 +192,7 @@ describe( 'table properties', () => {
 
 						command.execute( { value: '25px' } );
 
-						assertTableStyle( editor, 'width:25px;' );
+						assertTableStyle( editor, null, 'width:25px;' );
 					} );
 
 					it( 'should remove width from a selected table if no value is passed', () => {

+ 63 - 0
packages/ckeditor5-table/tests/tableproperties/tablepropertiesediting-integration.js

@@ -0,0 +1,63 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+
+import TableEditing from '../../src/tableediting';
+import TablePropertiesEditing from '../../src/tableproperties/tablepropertiesediting';
+
+import AlignmentEditing from '@ckeditor/ckeditor5-alignment/src/alignmentediting';
+import { assertTableStyle } from '../_utils/utils';
+
+describe( 'table properties', () => {
+	describe( 'TablePropertiesEditing integration', () => {
+		let editor, model;
+
+		afterEach( async () => {
+			await editor.destroy();
+		} );
+
+		describe( 'Alignment', () => {
+			let table;
+
+			beforeEach( async () => {
+				editor = await createEditorWithAdditionalPlugins( [ AlignmentEditing ] );
+
+				model = editor.model;
+
+				table = createEmptyTable();
+			} );
+
+			it( 'should properly downcast table with Alignment plugin enabled', () => {
+				model.change( writer => writer.setAttribute( 'alignment', 'right', table ) );
+
+				assertTableStyle( editor, null, 'float:right;' );
+			} );
+		} );
+
+		function createEmptyTable() {
+			setModelData(
+				model,
+				'<table headingRows="0" headingColumns="0">' +
+					'<tableRow>' +
+						'<tableCell>' +
+							'<paragraph>foo</paragraph>' +
+						'</tableCell>' +
+					'</tableRow>' +
+				'</table>'
+			);
+
+			return model.document.getRoot().getNodeByPath( [ 0 ] );
+		}
+	} );
+
+	function createEditorWithAdditionalPlugins( plugins ) {
+		return VirtualTestEditor.create( {
+			plugins: [ ...plugins, TablePropertiesEditing, Paragraph, TableEditing ]
+		} );
+	}
+} );

+ 254 - 47
packages/ckeditor5-table/tests/tableproperties/tablepropertiesediting.js

@@ -219,6 +219,26 @@ describe( 'table properties', () => {
 					table = createEmptyTable();
 				} );
 
+				it( 'should consume converted item borderColor attribute', () => {
+					editor.conversion.for( 'downcast' )
+						.add( dispatcher => dispatcher.on( 'attribute:borderColor:table', ( evt, data, conversionApi ) => {
+							expect( conversionApi.consumable.consume( data.item, evt.name ) ).to.be.false;
+						} ) );
+
+					model.change( writer => writer.setAttribute( 'borderColor', '#f00', table ) );
+				} );
+
+				it( 'should be overridable', () => {
+					editor.conversion.for( 'downcast' )
+						.add( dispatcher => dispatcher.on( 'attribute:borderColor:table', ( evt, data, conversionApi ) => {
+							conversionApi.consumable.consume( data.item, evt.name );
+						}, { priority: 'high' } ) );
+
+					model.change( writer => writer.setAttribute( 'borderColor', '#f00', table ) );
+
+					assertTableStyle( editor, '' );
+				} );
+
 				it( 'should downcast borderColor attribute (same top, right, bottom, left)', () => {
 					model.change( writer => writer.setAttribute( 'borderColor', {
 						top: '#f00',
@@ -246,6 +266,26 @@ describe( 'table properties', () => {
 					);
 				} );
 
+				it( 'should consume converted item borderStyle attribute', () => {
+					editor.conversion.for( 'downcast' )
+						.add( dispatcher => dispatcher.on( 'attribute:borderStyle:table', ( evt, data, conversionApi ) => {
+							expect( conversionApi.consumable.consume( data.item, evt.name ) ).to.be.false;
+						} ) );
+
+					model.change( writer => writer.setAttribute( 'borderStyle', 'solid', table ) );
+				} );
+
+				it( 'should be overridable', () => {
+					editor.conversion.for( 'downcast' )
+						.add( dispatcher => dispatcher.on( 'attribute:borderStyle:table', ( evt, data, conversionApi ) => {
+							conversionApi.consumable.consume( data.item, evt.name );
+						}, { priority: 'high' } ) );
+
+					model.change( writer => writer.setAttribute( 'borderStyle', 'solid', table ) );
+
+					assertTableStyle( editor, '' );
+				} );
+
 				it( 'should downcast borderStyle attribute (same top, right, bottom, left)', () => {
 					model.change( writer => writer.setAttribute( 'borderStyle', {
 						top: 'solid',
@@ -268,6 +308,26 @@ describe( 'table properties', () => {
 					assertTableStyle( editor, 'border-bottom:dotted;border-left:dashed;border-right:ridge;border-top:solid;' );
 				} );
 
+				it( 'should consume converted item borderWidth attribute', () => {
+					editor.conversion.for( 'downcast' )
+						.add( dispatcher => dispatcher.on( 'attribute:borderWidth:table', ( evt, data, conversionApi ) => {
+							expect( conversionApi.consumable.consume( data.item, evt.name ) ).to.be.false;
+						} ) );
+
+					model.change( writer => writer.setAttribute( 'borderWidth', '2px', table ) );
+				} );
+
+				it( 'should be overridable', () => {
+					editor.conversion.for( 'downcast' )
+						.add( dispatcher => dispatcher.on( 'attribute:borderWidth:table', ( evt, data, conversionApi ) => {
+							conversionApi.consumable.consume( data.item, evt.name );
+						}, { priority: 'high' } ) );
+
+					model.change( writer => writer.setAttribute( 'borderWidth', '2px', table ) );
+
+					assertTableStyle( editor, '' );
+				} );
+
 				it( 'should downcast borderWidth attribute (same top, right, bottom, left)', () => {
 					model.change( writer => writer.setAttribute( 'borderWidth', {
 						top: '42px',
@@ -499,6 +559,13 @@ describe( 'table properties', () => {
 
 					expect( table.getAttribute( 'backgroundColor' ) ).to.equal( '#f00' );
 				} );
+
+				it( 'should upcast from background shorthand (rbg color value with spaces)', () => {
+					editor.setData( '<table style="background:rgb(253, 253, 119) center center"><tr><td>foo</td></tr></table>' );
+					const table = model.document.getRoot().getNodeByPath( [ 0 ] );
+
+					expect( table.getAttribute( 'backgroundColor' ) ).to.equal( 'rgb(253, 253, 119)' );
+				} );
 			} );
 
 			describe( 'downcast conversion', () => {
@@ -508,11 +575,49 @@ describe( 'table properties', () => {
 					table = createEmptyTable();
 				} );
 
+				it( 'should consume converted item', () => {
+					editor.conversion.for( 'downcast' )
+						.add( dispatcher => dispatcher.on( 'attribute:backgroundColor:table', ( evt, data, conversionApi ) => {
+							expect( conversionApi.consumable.consume( data.item, evt.name ) ).to.be.false;
+						} ) );
+
+					model.change( writer => writer.setAttribute( 'backgroundColor', '#f00', table ) );
+				} );
+
+				it( 'should be overridable', () => {
+					editor.conversion.for( 'downcast' )
+						.add( dispatcher => dispatcher.on( 'attribute:backgroundColor:table', ( evt, data, conversionApi ) => {
+							conversionApi.consumable.consume( data.item, evt.name );
+						}, { priority: 'high' } ) );
+
+					model.change( writer => writer.setAttribute( 'backgroundColor', '#f00', table ) );
+
+					assertTableStyle( editor, '' );
+				} );
+
 				it( 'should downcast backgroundColor', () => {
 					model.change( writer => writer.setAttribute( 'backgroundColor', '#f00', table ) );
 
 					assertTableStyle( editor, 'background-color:#f00;' );
 				} );
+
+				it( 'should downcast backgroundColor removal', () => {
+					model.change( writer => writer.setAttribute( 'backgroundColor', '#f00', table ) );
+
+					model.change( writer => writer.removeAttribute( 'backgroundColor', table ) );
+
+					assertTableStyle( editor );
+				} );
+
+				it( 'should downcast backgroundColor change', () => {
+					model.change( writer => writer.setAttribute( 'backgroundColor', '#f00', table ) );
+
+					assertTableStyle( editor, 'background-color:#f00;' );
+
+					model.change( writer => writer.setAttribute( 'backgroundColor', '#ba7', table ) );
+
+					assertTableStyle( editor, 'background-color:#ba7;' );
+				} );
 			} );
 		} );
 
@@ -522,12 +627,19 @@ describe( 'table properties', () => {
 			} );
 
 			describe( 'upcast conversion', () => {
-				it( 'should upcast width', () => {
+				it( 'should upcast width from <table>', () => {
 					editor.setData( '<table style="width:1337px"><tr><td>foo</td></tr></table>' );
 					const table = model.document.getRoot().getNodeByPath( [ 0 ] );
 
 					expect( table.getAttribute( 'width' ) ).to.equal( '1337px' );
 				} );
+
+				it( 'should upcast width from <figure>', () => {
+					editor.setData( '<figure style="width:1337px"><table><tr><td>foo</td></tr></table></figure>' );
+					const table = model.document.getRoot().getNodeByPath( [ 0 ] );
+
+					expect( table.getAttribute( 'width' ) ).to.equal( '1337px' );
+				} );
 			} );
 
 			describe( 'downcast conversion', () => {
@@ -537,10 +649,48 @@ describe( 'table properties', () => {
 					table = createEmptyTable();
 				} );
 
+				it( 'should consume converted item', () => {
+					editor.conversion.for( 'downcast' )
+						.add( dispatcher => dispatcher.on( 'attribute:width:table', ( evt, data, conversionApi ) => {
+							expect( conversionApi.consumable.consume( data.item, evt.name ) ).to.be.false;
+						} ) );
+
+					model.change( writer => writer.setAttribute( 'width', '400px', table ) );
+				} );
+
+				it( 'should be overridable', () => {
+					editor.conversion.for( 'downcast' )
+						.add( dispatcher => dispatcher.on( 'attribute:width:table', ( evt, data, conversionApi ) => {
+							conversionApi.consumable.consume( data.item, evt.name );
+						}, { priority: 'high' } ) );
+
+					model.change( writer => writer.setAttribute( 'width', '400px', table ) );
+
+					assertTableStyle( editor, '' );
+				} );
+
 				it( 'should downcast width', () => {
 					model.change( writer => writer.setAttribute( 'width', '1337px', table ) );
 
-					assertTableStyle( editor, 'width:1337px;' );
+					assertTableStyle( editor, null, 'width:1337px;' );
+				} );
+
+				it( 'should downcast width removal', () => {
+					model.change( writer => writer.setAttribute( 'width', '1337px', table ) );
+
+					model.change( writer => writer.removeAttribute( 'width', table ) );
+
+					assertTableStyle( editor );
+				} );
+
+				it( 'should downcast width change', () => {
+					model.change( writer => writer.setAttribute( 'width', '1337px', table ) );
+
+					assertTableStyle( editor, null, 'width:1337px;' );
+
+					model.change( writer => writer.setAttribute( 'width', '1410em', table ) );
+
+					assertTableStyle( editor, null, 'width:1410em;' );
 				} );
 			} );
 		} );
@@ -551,12 +701,19 @@ describe( 'table properties', () => {
 			} );
 
 			describe( 'upcast conversion', () => {
-				it( 'should upcast height', () => {
+				it( 'should upcast height from <table>', () => {
 					editor.setData( '<table style="height:1337px"><tr><td>foo</td></tr></table>' );
 					const table = model.document.getRoot().getNodeByPath( [ 0 ] );
 
 					expect( table.getAttribute( 'height' ) ).to.equal( '1337px' );
 				} );
+
+				it( 'should upcast height from <figure>', () => {
+					editor.setData( '<figure style="height:1337px"><table><tr><td>foo</td></tr></table></figure>' );
+					const table = model.document.getRoot().getNodeByPath( [ 0 ] );
+
+					expect( table.getAttribute( 'height' ) ).to.equal( '1337px' );
+				} );
 			} );
 
 			describe( 'downcast conversion', () => {
@@ -569,57 +726,67 @@ describe( 'table properties', () => {
 				it( 'should downcast height', () => {
 					model.change( writer => writer.setAttribute( 'height', '1337px', table ) );
 
-					assertTableStyle( editor, 'height:1337px;' );
+					assertTableStyle( editor, null, 'height:1337px;' );
 				} );
-			} );
-		} );
 
-		describe( 'alignment', () => {
-			it( 'should set proper schema rules', () => {
-				expect( model.schema.checkAttribute( [ '$root', 'table' ], 'alignment' ) ).to.be.true;
-			} );
+				it( 'should downcast height removal', () => {
+					model.change( writer => writer.setAttribute( 'height', '1337px', table ) );
 
-			describe( 'upcast conversion', () => {
-				it( 'should upcast style="margin-left:auto;margin-right:0" to right value', () => {
-					editor.setData( '<table style="margin-left:auto;margin-right:0"><tr><td>foo</td></tr></table>' );
-					const table = model.document.getRoot().getNodeByPath( [ 0 ] );
+					model.change( writer => writer.removeAttribute( 'height', table ) );
 
-					expect( table.getAttribute( 'alignment' ) ).to.equal( 'right' );
+					assertTableStyle( editor );
 				} );
 
-				it( 'should upcast style="margin-left:0;margin-right:auto" to left value', () => {
-					editor.setData( '<table style="margin-left:0;margin-right:auto"><tr><td>foo</td></tr></table>' );
-					const table = model.document.getRoot().getNodeByPath( [ 0 ] );
+				it( 'should downcast height change', () => {
+					model.change( writer => writer.setAttribute( 'height', '1337px', table ) );
 
-					expect( table.getAttribute( 'alignment' ) ).to.equal( 'left' );
+					assertTableStyle( editor, null, 'height:1337px;' );
+
+					model.change( writer => writer.setAttribute( 'height', '1410em', table ) );
+
+					assertTableStyle( editor, null, 'height:1410em;' );
 				} );
 
-				it( 'should upcast style="margin-left:auto;margin-right:auto" to center value', () => {
-					editor.setData( '<table style="margin-left:auto;margin-right:auto"><tr><td>foo</td></tr></table>' );
-					const table = model.document.getRoot().getNodeByPath( [ 0 ] );
+				it( 'should consume converted item', () => {
+					editor.conversion.for( 'downcast' )
+						.add( dispatcher => dispatcher.on( 'attribute:height:table', ( evt, data, conversionApi ) => {
+							expect( conversionApi.consumable.consume( data.item, evt.name ) ).to.be.false;
+						} ) );
 
-					expect( table.getAttribute( 'alignment' ) ).to.equal( 'center' );
+					model.change( writer => writer.setAttribute( 'height', '400px', table ) );
 				} );
 
-				it( 'should upcast style="margin-left:auto;margin-right:0pt" to right value', () => {
-					editor.setData( '<table style="margin-left:auto;margin-right:0pt"><tr><td>foo</td></tr></table>' );
-					const table = model.document.getRoot().getNodeByPath( [ 0 ] );
+				it( 'should be overridable', () => {
+					editor.conversion.for( 'downcast' )
+						.add( dispatcher => dispatcher.on( 'attribute:height:table', ( evt, data, conversionApi ) => {
+							conversionApi.consumable.consume( data.item, evt.name );
+						}, { priority: 'high' } ) );
 
-					expect( table.getAttribute( 'alignment' ) ).to.equal( 'right' );
+					model.change( writer => writer.setAttribute( 'height', '400px', table ) );
+
+					assertTableStyle( editor, '' );
 				} );
+			} );
+		} );
 
-				it( 'should upcast style="margin-left:auto;margin-right:0%" to right value', () => {
-					editor.setData( '<table style="margin-left:auto;margin-right:0%"><tr><td>foo</td></tr></table>' );
+		describe( 'alignment', () => {
+			it( 'should set proper schema rules', () => {
+				expect( model.schema.checkAttribute( [ '$root', 'table' ], 'alignment' ) ).to.be.true;
+			} );
+
+			describe( 'upcast conversion', () => {
+				it( 'should upcast style="float:right" to right value', () => {
+					editor.setData( '<table style="float:right"><tr><td>foo</td></tr></table>' );
 					const table = model.document.getRoot().getNodeByPath( [ 0 ] );
 
 					expect( table.getAttribute( 'alignment' ) ).to.equal( 'right' );
 				} );
 
-				it( 'should not upcast style="margin-left:auto;margin-right:0.23pt" to right value', () => {
-					editor.setData( '<table style="margin-left:auto;margin-right:0.23pt"><tr><td>foo</td></tr></table>' );
+				it( 'should upcast style="float:left;" to left value', () => {
+					editor.setData( '<table style="float:left;"><tr><td>foo</td></tr></table>' );
 					const table = model.document.getRoot().getNodeByPath( [ 0 ] );
 
-					expect( table.hasAttribute( 'alignment' ) ).to.be.false;
+					expect( table.getAttribute( 'alignment' ) ).to.equal( 'left' );
 				} );
 
 				it( 'should upcast align=right attribute', () => {
@@ -636,11 +803,11 @@ describe( 'table properties', () => {
 					expect( table.getAttribute( 'alignment' ) ).to.equal( 'left' );
 				} );
 
-				it( 'should upcast align=center attribute', () => {
+				it( 'should discard align=center attribute', () => {
 					editor.setData( '<table align="center"><tr><td>foo</td></tr></table>' );
 					const table = model.document.getRoot().getNodeByPath( [ 0 ] );
 
-					expect( table.getAttribute( 'alignment' ) ).to.equal( 'center' );
+					expect( table.getAttribute( 'alignment' ) ).to.be.undefined;
 				} );
 
 				it( 'should discard align=justify attribute', () => {
@@ -658,38 +825,78 @@ describe( 'table properties', () => {
 					table = createEmptyTable();
 				} );
 
-				it( 'should downcast right alignment', () => {
+				it( 'should consume converted item', () => {
+					editor.conversion.for( 'downcast' )
+						.add( dispatcher => dispatcher.on( 'attribute:alignment:table', ( evt, data, conversionApi ) => {
+							expect( conversionApi.consumable.consume( data.item, evt.name ) ).to.be.false;
+						} ) );
+
 					model.change( writer => writer.setAttribute( 'alignment', 'right', table ) );
+				} );
 
-					assertTableStyle( editor, 'margin-left:auto;margin-right:0;' );
+				it( 'should be overridable', () => {
+					editor.conversion.for( 'downcast' )
+						.add( dispatcher => dispatcher.on( 'attribute:alignment:table', ( evt, data, conversionApi ) => {
+							conversionApi.consumable.consume( data.item, evt.name );
+						}, { priority: 'highest' } ) );
+
+					model.change( writer => writer.setAttribute( 'alignment', 'right', table ) );
+
+					assertTableStyle( editor, '' );
+				} );
+
+				it( 'should downcast "right" alignment', () => {
+					model.change( writer => writer.setAttribute( 'alignment', 'right', table ) );
+
+					assertTableStyle( editor, null, 'float:right;' );
 				} );
 
-				it( 'should downcast left alignment', () => {
+				it( 'should downcast "left" alignment', () => {
 					model.change( writer => writer.setAttribute( 'alignment', 'left', table ) );
 
-					assertTableStyle( editor, 'margin-left:0;margin-right:auto;' );
+					assertTableStyle( editor, null, 'float:left;' );
 				} );
 
-				it( 'should downcast centered alignment', () => {
+				it( 'should not downcast "center" alignment', () => {
 					model.change( writer => writer.setAttribute( 'alignment', 'center', table ) );
 
-					assertTableStyle( editor, 'margin-left:auto;margin-right:auto;' );
+					assertTableStyle( editor, null, null );
 				} );
 
-				it( 'should downcast changed alignment', () => {
-					model.change( writer => writer.setAttribute( 'alignment', 'center', table ) );
+				it( 'should downcast changed alignment (left -> right)', () => {
+					model.change( writer => writer.setAttribute( 'alignment', 'left', table ) );
 
-					assertTableStyle( editor, 'margin-left:auto;margin-right:auto;' );
+					assertTableStyle( editor, null, 'float:left;' );
 
 					model.change( writer => writer.setAttribute( 'alignment', 'right', table ) );
 
-					assertTableStyle( editor, 'margin-left:auto;margin-right:0;' );
+					assertTableStyle( editor, null, 'float:right;' );
 				} );
 
-				it( 'should downcast removed alignment', () => {
-					model.change( writer => writer.setAttribute( 'alignment', 'center', table ) );
+				it( 'should downcast changed alignment (right -> left)', () => {
+					model.change( writer => writer.setAttribute( 'alignment', 'right', table ) );
+
+					assertTableStyle( editor, null, 'float:right;' );
+
+					model.change( writer => writer.setAttribute( 'alignment', 'left', table ) );
+
+					assertTableStyle( editor, null, 'float:left;' );
+				} );
+
+				it( 'should downcast removed alignment (from left)', () => {
+					model.change( writer => writer.setAttribute( 'alignment', 'left', table ) );
+
+					assertTableStyle( editor, null, 'float:left;' );
+
+					model.change( writer => writer.removeAttribute( 'alignment', table ) );
+
+					assertTableStyle( editor );
+				} );
+
+				it( 'should downcast removed alignment (from right)', () => {
+					model.change( writer => writer.setAttribute( 'alignment', 'right', table ) );
 
-					assertTableStyle( editor, 'margin-left:auto;margin-right:auto;' );
+					assertTableStyle( editor, null, 'float:right;' );
 
 					model.change( writer => writer.removeAttribute( 'alignment', table ) );
 

+ 271 - 46
packages/ckeditor5-table/tests/tableproperties/tablepropertiesui.js

@@ -20,15 +20,18 @@ import Table from '../../src/table';
 import TablePropertiesEditing from '../../src/tableproperties/tablepropertiesediting';
 import TablePropertiesUI from '../../src/tableproperties/tablepropertiesui';
 import TablePropertiesUIView from '../../src/tableproperties/ui/tablepropertiesview';
+import { defaultColors } from '../../src/ui/utils';
 
 describe( 'table properties', () => {
 	describe( 'TablePropertiesUI', () => {
 		let editor, editorElement, contextualBalloon,
-			tablePropertiesUI, tablePropertiesView, tablePropertiesButton;
+			tablePropertiesUI, tablePropertiesView, tablePropertiesButton,
+			clock;
 
 		testUtils.createSinonSandbox();
 
 		beforeEach( () => {
+			clock = sinon.useFakeTimers();
 			editorElement = document.createElement( 'div' );
 			document.body.appendChild( editorElement );
 
@@ -52,6 +55,7 @@ describe( 'table properties', () => {
 		} );
 
 		afterEach( () => {
+			clock.restore();
 			editorElement.remove();
 
 			return editor.destroy();
@@ -65,6 +69,15 @@ describe( 'table properties', () => {
 			expect( editor.plugins.get( ContextualBalloon ) ).to.be.instanceOf( ContextualBalloon );
 		} );
 
+		describe( 'constructor()', () => {
+			it( 'should define table.tableProperties config', () => {
+				expect( editor.config.get( 'table.tableProperties' ) ).to.deep.equal( {
+					borderColors: defaultColors,
+					backgroundColors: defaultColors
+				} );
+			} );
+		} );
+
 		describe( 'init()', () => {
 			it( 'should set a batch', () => {
 				expect( tablePropertiesUI._undoStepBatch ).to.be.null;
@@ -78,6 +91,14 @@ describe( 'table properties', () => {
 				it( 'should be rendered', () => {
 					expect( tablePropertiesUI.view.isRendered ).to.be.true;
 				} );
+
+				it( 'should get the border colors configurations', () => {
+					expect( tablePropertiesView.options.borderColors ).to.have.length( 15 );
+				} );
+
+				it( 'should get the background colors configurations', () => {
+					expect( tablePropertiesView.options.backgroundColors ).to.have.length( 15 );
+				} );
 			} );
 
 			describe( 'toolbar button', () => {
@@ -127,45 +148,64 @@ describe( 'table properties', () => {
 				expect( contextualBalloon.visibleView ).to.be.null;
 			} );
 
-			it( 'should undo the entire batch of changes on #cancel', () => {
-				// Show the view. New batch will be created.
-				tablePropertiesButton.fire( 'execute' );
+			describe( '#cancel event', () => {
+				// https://github.com/ckeditor/ckeditor5/issues/6180
+				it( 'should not undo if it there were no changes made to the property fields', () => {
+					const spy = sinon.spy( editor, 'execute' );
 
-				// Do the changes like a user.
-				tablePropertiesView.borderStyle = 'dotted';
-				tablePropertiesView.backgroundColor = 'red';
-
-				expect( getModelData( editor.model ) ).to.equal(
-					'<table backgroundColor="red" borderStyle="dotted">' +
-						'<tableRow>' +
-							'<tableCell>' +
-								'<paragraph>[]foo</paragraph>' +
-							'</tableCell>' +
-						'</tableRow>' +
-					'</table>' +
-					'<paragraph>bar</paragraph>'
-				);
-
-				tablePropertiesView.fire( 'cancel' );
-
-				expect( getModelData( editor.model ) ).to.equal(
-					'<table>' +
-						'<tableRow>' +
-							'<tableCell>' +
-								'<paragraph>[]foo</paragraph>' +
-							'</tableCell>' +
-						'</tableRow>' +
-					'</table>' +
-					'<paragraph>bar</paragraph>'
-				);
-			} );
+					// Show the view. New batch will be created.
+					tablePropertiesButton.fire( 'execute' );
 
-			it( 'should hide on #cancel', () => {
-				tablePropertiesButton.fire( 'execute' );
-				expect( contextualBalloon.visibleView ).to.equal( tablePropertiesView );
+					// Cancel the view immediately.
+					tablePropertiesView.fire( 'cancel' );
 
-				tablePropertiesView.fire( 'cancel' );
-				expect( contextualBalloon.visibleView ).to.be.null;
+					sinon.assert.notCalled( spy );
+				} );
+
+				it( 'should undo the entire batch of changes if there were some', () => {
+					const spy = sinon.spy( editor, 'execute' );
+
+					// Show the view. New batch will be created.
+					tablePropertiesButton.fire( 'execute' );
+
+					// Do the changes like a user.
+					tablePropertiesView.borderStyle = 'dotted';
+					tablePropertiesView.backgroundColor = 'red';
+
+					expect( getModelData( editor.model ) ).to.equal(
+						'<table backgroundColor="red" borderStyle="dotted">' +
+							'<tableRow>' +
+								'<tableCell>' +
+									'<paragraph>[]foo</paragraph>' +
+								'</tableCell>' +
+							'</tableRow>' +
+						'</table>' +
+						'<paragraph>bar</paragraph>'
+					);
+
+					tablePropertiesView.fire( 'cancel' );
+
+					expect( getModelData( editor.model ) ).to.equal(
+						'<table>' +
+							'<tableRow>' +
+								'<tableCell>' +
+									'<paragraph>[]foo</paragraph>' +
+								'</tableCell>' +
+							'</tableRow>' +
+						'</table>' +
+						'<paragraph>bar</paragraph>'
+					);
+
+					sinon.assert.calledWith( spy, 'undo', tablePropertiesUI._undoStepBatch );
+				} );
+
+				it( 'should hide the view', () => {
+					tablePropertiesButton.fire( 'execute' );
+					expect( contextualBalloon.visibleView ).to.equal( tablePropertiesView );
+
+					tablePropertiesView.fire( 'cancel' );
+					expect( contextualBalloon.visibleView ).to.be.null;
+				} );
 			} );
 
 			it( 'should hide on the Esc key press', () => {
@@ -215,14 +255,200 @@ describe( 'table properties', () => {
 			} );
 
 			describe( 'property changes', () => {
-				it( 'should affect the editor state', () => {
-					const spy = testUtils.sinon.stub( editor, 'execute' );
-
+				beforeEach( () => {
 					tablePropertiesUI._undoStepBatch = 'foo';
-					tablePropertiesView.borderStyle = 'dotted';
+				} );
 
-					sinon.assert.calledOnce( spy );
-					sinon.assert.calledWithExactly( spy, 'tableBorderStyle', { value: 'dotted', batch: 'foo' } );
+				describe( '#borderStyle', () => {
+					it( 'should affect the editor state', () => {
+						const spy = testUtils.sinon.stub( editor, 'execute' );
+
+						tablePropertiesView.borderStyle = 'dotted';
+
+						sinon.assert.calledOnce( spy );
+						sinon.assert.calledWithExactly( spy, 'tableBorderStyle', { value: 'dotted', batch: 'foo' } );
+					} );
+				} );
+
+				describe( '#borderColor', () => {
+					it( 'should affect the editor state', () => {
+						const spy = testUtils.sinon.stub( editor, 'execute' );
+
+						tablePropertiesView.borderColor = '#FFAAFF';
+
+						sinon.assert.calledOnce( spy );
+						sinon.assert.calledWithExactly( spy, 'tableBorderColor', { value: '#FFAAFF', batch: 'foo' } );
+					} );
+
+					it( 'should display an error message if value is invalid', () => {
+						const spy = testUtils.sinon.stub( editor, 'execute' );
+
+						// First, let's pass an invalid value and check what happens.
+						tablePropertiesView.borderColor = '42';
+
+						clock.tick( 500 );
+
+						expect( tablePropertiesView.borderColorInput.errorText ).to.match( /^The color is invalid/ );
+						sinon.assert.notCalled( spy );
+
+						// And now let's pass a valid value and check if the error text will be gone.
+						tablePropertiesView.borderColor = '#AAA';
+
+						clock.tick( 500 );
+
+						expect( tablePropertiesView.borderColorInput.errorText ).to.be.null;
+						sinon.assert.calledWithExactly( spy, 'tableBorderColor', { value: '#AAA', batch: 'foo' } );
+					} );
+				} );
+
+				describe( '#borderWidth', () => {
+					it( 'should affect the editor state', () => {
+						const spy = testUtils.sinon.stub( editor, 'execute' );
+
+						tablePropertiesView.borderWidth = '12px';
+
+						sinon.assert.calledOnce( spy );
+						sinon.assert.calledWithExactly( spy, 'tableBorderWidth', { value: '12px', batch: 'foo' } );
+					} );
+
+					it( 'should display an error message if value is invalid', () => {
+						const spy = testUtils.sinon.stub( editor, 'execute' );
+
+						// First, let's pass an invalid value and check what happens.
+						tablePropertiesView.borderWidth = 'wrong';
+
+						clock.tick( 500 );
+
+						expect( tablePropertiesView.borderWidthInput.errorText ).to.match( /^The value is invalid/ );
+						sinon.assert.notCalled( spy );
+
+						// And now let's pass a valid value and check if the error text will be gone.
+						tablePropertiesView.borderWidth = '3em';
+
+						clock.tick( 500 );
+
+						expect( tablePropertiesView.backgroundInput.errorText ).to.be.null;
+						sinon.assert.calledWithExactly( spy, 'tableBorderWidth', { value: '3em', batch: 'foo' } );
+					} );
+				} );
+
+				describe( '#backgroundColor', () => {
+					it( 'should affect the editor state', () => {
+						const spy = testUtils.sinon.stub( editor, 'execute' );
+
+						tablePropertiesView.backgroundColor = '#FFAAFF';
+
+						sinon.assert.calledOnce( spy );
+						sinon.assert.calledWithExactly( spy, 'tableBackgroundColor', { value: '#FFAAFF', batch: 'foo' } );
+					} );
+
+					it( 'should display an error message if value is invalid', () => {
+						const spy = testUtils.sinon.stub( editor, 'execute' );
+
+						// First, let's pass an invalid value and check what happens.
+						tablePropertiesView.backgroundColor = '42';
+
+						clock.tick( 500 );
+
+						expect( tablePropertiesView.backgroundInput.errorText ).to.match( /^The color is invalid/ );
+						sinon.assert.notCalled( spy );
+
+						// And now let's pass a valid value and check if the error text will be gone.
+						tablePropertiesView.backgroundColor = '#AAA';
+
+						clock.tick( 500 );
+
+						expect( tablePropertiesView.backgroundInput.errorText ).to.be.null;
+						sinon.assert.calledWithExactly( spy, 'tableBackgroundColor', { value: '#AAA', batch: 'foo' } );
+					} );
+				} );
+
+				describe( '#width', () => {
+					it( 'should affect the editor state', () => {
+						const spy = testUtils.sinon.stub( editor, 'execute' );
+
+						tablePropertiesView.width = '12px';
+
+						sinon.assert.calledOnce( spy );
+						sinon.assert.calledWithExactly( spy, 'tableWidth', { value: '12px', batch: 'foo' } );
+					} );
+
+					it( 'should display an error message if value is invalid', () => {
+						const spy = testUtils.sinon.stub( editor, 'execute' );
+
+						// First, let's pass an invalid value and check what happens.
+						tablePropertiesView.width = 'wrong';
+
+						clock.tick( 500 );
+
+						expect( tablePropertiesView.widthInput.errorText ).to.match( /^The value is invalid/ );
+						sinon.assert.notCalled( spy );
+
+						// And now let's pass a valid value and check if the error text will be gone.
+						tablePropertiesView.width = '3em';
+
+						clock.tick( 500 );
+
+						expect( tablePropertiesView.backgroundInput.errorText ).to.be.null;
+						sinon.assert.calledWithExactly( spy, 'tableWidth', { value: '3em', batch: 'foo' } );
+					} );
+				} );
+
+				describe( '#height', () => {
+					it( 'should affect the editor state', () => {
+						const spy = testUtils.sinon.stub( editor, 'execute' );
+
+						tablePropertiesView.height = '12px';
+
+						sinon.assert.calledOnce( spy );
+						sinon.assert.calledWithExactly( spy, 'tableHeight', { value: '12px', batch: 'foo' } );
+					} );
+
+					it( 'should display an error message if value is invalid', () => {
+						const spy = testUtils.sinon.stub( editor, 'execute' );
+
+						// First, let's pass an invalid value and check what happens.
+						tablePropertiesView.height = 'wrong';
+
+						clock.tick( 500 );
+
+						expect( tablePropertiesView.heightInput.errorText ).to.match( /^The value is invalid/ );
+						sinon.assert.notCalled( spy );
+
+						// And now let's pass a valid value and check if the error text will be gone.
+						tablePropertiesView.height = '3em';
+
+						clock.tick( 500 );
+
+						expect( tablePropertiesView.backgroundInput.errorText ).to.be.null;
+						sinon.assert.calledWithExactly( spy, 'tableHeight', { value: '3em', batch: 'foo' } );
+					} );
+				} );
+
+				describe( '#alignment', () => {
+					it( 'should affect the editor state', () => {
+						const spy = testUtils.sinon.stub( editor, 'execute' );
+
+						tablePropertiesView.alignment = 'right';
+
+						sinon.assert.calledOnce( spy );
+						sinon.assert.calledWithExactly( spy, 'tableAlignment', { value: 'right', batch: 'foo' } );
+					} );
+				} );
+
+				it( 'should not display an error text if user managed to fix the value before the UI timeout', () => {
+					// First, let's pass an invalid value.
+					tablePropertiesView.borderColor = '#';
+
+					clock.tick( 100 );
+
+					// Then the user managed to quickly type the correct value.
+					tablePropertiesView.borderColor = '#aaa';
+
+					clock.tick( 400 );
+
+					// Because they were quick, they should see no error
+					expect( tablePropertiesView.borderColorInput.errorText ).to.be.null;
 				} );
 
 				it( 'should not affect the editor state if internal property has changed', () => {
@@ -230,7 +456,6 @@ describe( 'table properties', () => {
 
 					tablePropertiesView.set( 'internalProp', 'foo' );
 
-					tablePropertiesUI._undoStepBatch = 'foo';
 					tablePropertiesView.internalProp = 'bar';
 
 					sinon.assert.notCalled( spy );
@@ -299,13 +524,13 @@ describe( 'table properties', () => {
 
 					expect( contextualBalloon.visibleView ).to.equal( tablePropertiesView );
 					expect( tablePropertiesView ).to.include( {
-						borderStyle: 'none',
+						borderStyle: '',
 						borderColor: '',
 						borderWidth: '',
 						backgroundColor: '',
 						width: '',
 						height: '',
-						alignment: 'center'
+						alignment: ''
 					} );
 				} );
 			} );

+ 145 - 18
packages/ckeditor5-table/tests/tableproperties/ui/tablepropertiesview.js

@@ -16,6 +16,29 @@ import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import ToolbarView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarview';
 import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
 import InputTextView from '@ckeditor/ckeditor5-ui/src/inputtext/inputtextview';
+import ColorInputView from '../../../src/ui/colorinputview';
+
+const VIEW_OPTIONS = {
+	borderColors: [
+		{
+			model: 'rgb(255,0,0)',
+			label: 'Red',
+			hasBorder: false
+		},
+		{
+			model: 'rgb(0,0,255)',
+			label: 'Blue',
+			hasBorder: false
+		}
+	],
+	backgroundColors: [
+		{
+			model: 'rgb(0,255,0)',
+			label: 'Green',
+			hasBorder: false
+		},
+	]
+};
 
 describe( 'table properties', () => {
 	describe( 'TablePropertiesView', () => {
@@ -25,7 +48,7 @@ describe( 'table properties', () => {
 
 		beforeEach( () => {
 			locale = { t: val => val };
-			view = new TablePropertiesView( locale );
+			view = new TablePropertiesView( locale, VIEW_OPTIONS );
 			view.render();
 		} );
 
@@ -34,6 +57,10 @@ describe( 'table properties', () => {
 		} );
 
 		describe( 'constructor()', () => {
+			it( 'should set view#options', () => {
+				expect( view.options ).to.deep.equal( VIEW_OPTIONS );
+			} );
+
 			it( 'should set view#locale', () => {
 				expect( view.locale ).to.equal( locale );
 			} );
@@ -44,13 +71,13 @@ describe( 'table properties', () => {
 
 			it( 'should define the public data interface (observable properties)', () => {
 				expect( view ).to.include( {
-					borderStyle: 'none',
+					borderStyle: '',
 					borderWidth: '',
 					borderColor: '',
 					backgroundColor: '',
 					width: '',
 					height: '',
-					alignment: 'center'
+					alignment: ''
 				} );
 			} );
 
@@ -124,7 +151,7 @@ describe( 'table properties', () => {
 
 						it( 'should change #borderStyle when executed', () => {
 							labeledDropdown.view.listView.items.first.children.first.fire( 'execute' );
-							expect( view.borderStyle ).to.equal( 'none' );
+							expect( view.borderStyle ).to.equal( '' );
 
 							labeledDropdown.view.listView.items.last.children.first.fire( 'execute' );
 							expect( view.borderStyle ).to.equal( 'outset' );
@@ -134,9 +161,20 @@ describe( 'table properties', () => {
 							expect( labeledDropdown.view.listView.items.map( item => {
 								return item.children.first.label;
 							} ) ).to.have.ordered.members( [
-								'None', 'Solid', 'Dotted', 'Dashed', 'Double', 'Groove', 'Ridge', 'Inset', 'Outset',
+								'None', 'Solid', 'Dotted', 'Dashed', 'Double', 'Groove', 'Ridge', 'Inset', 'Outset'
 							] );
 						} );
+
+						it( 'should reset border width and color inputs when setting style to none', () => {
+							view.borderStyle = 'dotted';
+							view.borderWidth = '1px';
+							view.borderColor = 'red';
+
+							view.borderStyle = '';
+
+							expect( view.borderColor ).to.equal( '' );
+							expect( view.borderWidth ).to.equal( '' );
+						} );
 					} );
 
 					describe( 'border width input', () => {
@@ -161,7 +199,7 @@ describe( 'table properties', () => {
 						} );
 
 						it( 'should be enabled only when #borderStyle is different than "none"', () => {
-							view.borderStyle = 'none';
+							view.borderStyle = '';
 							expect( labeledInput.isEnabled ).to.be.false;
 
 							view.borderStyle = 'dotted';
@@ -187,10 +225,33 @@ describe( 'table properties', () => {
 						} );
 
 						it( 'should be created', () => {
-							expect( labeledInput.view ).to.be.instanceOf( InputTextView );
+							expect( labeledInput.view ).to.be.instanceOf( ColorInputView );
 							expect( labeledInput.label ).to.equal( 'Color' );
 						} );
 
+						it( 'should get the color configuration', () => {
+							expect( labeledInput.view.options.colorDefinitions ).to.deep.equal( [
+								{
+									color: 'rgb(255,0,0)',
+									label: 'Red',
+									options: {
+										hasBorder: false
+									}
+								},
+								{
+									color: 'rgb(0,0,255)',
+									label: 'Blue',
+									options: {
+										hasBorder: false
+									}
+								}
+							] );
+						} );
+
+						it( 'should obtain the columns configuration', () => {
+							expect( labeledInput.view.options.columns ).to.equal( 5 );
+						} );
+
 						it( 'should reflect #borderColor property', () => {
 							view.borderColor = 'foo';
 							expect( labeledInput.view.value ).to.equal( 'foo' );
@@ -200,7 +261,7 @@ describe( 'table properties', () => {
 						} );
 
 						it( 'should be enabled only when #borderStyle is different than "none"', () => {
-							view.borderStyle = 'none';
+							view.borderStyle = '';
 							expect( labeledInput.isEnabled ).to.be.false;
 
 							view.borderStyle = 'dotted';
@@ -208,11 +269,11 @@ describe( 'table properties', () => {
 						} );
 
 						it( 'should update #borderColor on DOM "input" event', () => {
-							labeledInput.view.element.value = 'foo';
+							labeledInput.view.value = 'foo';
 							labeledInput.view.fire( 'input' );
 							expect( view.borderColor ).to.equal( 'foo' );
 
-							labeledInput.view.element.value = 'bar';
+							labeledInput.view.value = 'bar';
 							labeledInput.view.fire( 'input' );
 							expect( view.borderColor ).to.equal( 'bar' );
 						} );
@@ -235,11 +296,27 @@ describe( 'table properties', () => {
 						} );
 
 						it( 'should be created', () => {
-							expect( labeledInput.view ).to.be.instanceOf( InputTextView );
+							expect( labeledInput.view ).to.be.instanceOf( ColorInputView );
 							expect( labeledInput.label ).to.equal( 'Background' );
 							expect( labeledInput.class ).to.equal( 'ck-table-properties-form__background' );
 						} );
 
+						it( 'should get the color configuration', () => {
+							expect( labeledInput.view.options.colorDefinitions ).to.deep.equal( [
+								{
+									color: 'rgb(0,255,0)',
+									label: 'Green',
+									options: {
+										hasBorder: false
+									}
+								}
+							] );
+						} );
+
+						it( 'should obtain the columns configuration', () => {
+							expect( labeledInput.view.options.columns ).to.equal( 5 );
+						} );
+
 						it( 'should reflect #backgroundColor property', () => {
 							view.backgroundColor = 'foo';
 							expect( labeledInput.view.value ).to.equal( 'foo' );
@@ -249,11 +326,11 @@ describe( 'table properties', () => {
 						} );
 
 						it( 'should update #backgroundColor on DOM "input" event', () => {
-							labeledInput.view.element.value = 'foo';
+							labeledInput.view.value = 'foo';
 							labeledInput.view.fire( 'input' );
 							expect( view.backgroundColor ).to.equal( 'foo' );
 
-							labeledInput.view.element.value = 'bar';
+							labeledInput.view.value = 'bar';
 							labeledInput.view.fire( 'input' );
 							expect( view.backgroundColor ).to.equal( 'bar' );
 						} );
@@ -265,7 +342,7 @@ describe( 'table properties', () => {
 						const row = view.element.childNodes[ 3 ].childNodes[ 0 ];
 
 						expect( row.classList.contains( 'ck-form__row' ) ).to.be.true;
-						expect( row.classList.contains( 'ck-table-properties-form__dimensions-row' ) ).to.be.true;
+						expect( row.classList.contains( 'ck-table-form__dimensions-row' ) ).to.be.true;
 						expect( row.childNodes[ 0 ].textContent ).to.equal( 'Dimensions' );
 						expect( row.childNodes[ 1 ] ).to.equal( view.widthInput.element );
 						expect( row.childNodes[ 2 ].textContent ).to.equal( '×' );
@@ -282,7 +359,7 @@ describe( 'table properties', () => {
 						it( 'should be created', () => {
 							expect( labeledInput.view ).to.be.instanceOf( InputTextView );
 							expect( labeledInput.label ).to.equal( 'Width' );
-							expect( labeledInput.class ).to.equal( 'ck-table-properties-form__width' );
+							expect( labeledInput.class ).to.equal( 'ck-table-form__dimensions-row__width' );
 						} );
 
 						it( 'should reflect #width property', () => {
@@ -314,7 +391,7 @@ describe( 'table properties', () => {
 						it( 'should be created', () => {
 							expect( labeledInput.view ).to.be.instanceOf( InputTextView );
 							expect( labeledInput.label ).to.equal( 'Height' );
-							expect( labeledInput.class ).to.equal( 'ck-table-properties-form__height' );
+							expect( labeledInput.class ).to.equal( 'ck-table-form__dimensions-row__height' );
 						} );
 
 						it( 'should reflect #height property', () => {
@@ -417,6 +494,56 @@ describe( 'table properties', () => {
 
 						expect( spy.calledOnce ).to.be.true;
 					} );
+
+					it( 'should make sure the #saveButtonView is disabled until text fields are without errors', () => {
+						view.borderWidthInput.errorText = 'foo';
+						view.borderColorInput.errorText = 'foo';
+						view.backgroundInput.errorText = 'foo';
+						view.widthInput.errorText = 'foo';
+						view.heightInput.errorText = 'foo';
+
+						expect( view.saveButtonView.isEnabled ).to.be.false;
+
+						view.borderWidthInput.errorText = 'foo';
+						view.borderColorInput.errorText = 'foo';
+						view.backgroundInput.errorText = 'foo';
+						view.widthInput.errorText = 'foo';
+						view.heightInput.errorText = null;
+
+						expect( view.saveButtonView.isEnabled ).to.be.false;
+
+						view.borderWidthInput.errorText = 'foo';
+						view.borderColorInput.errorText = 'foo';
+						view.backgroundInput.errorText = 'foo';
+						view.widthInput.errorText = null;
+						view.heightInput.errorText = null;
+
+						expect( view.saveButtonView.isEnabled ).to.be.false;
+
+						view.borderWidthInput.errorText = 'foo';
+						view.borderColorInput.errorText = 'foo';
+						view.backgroundInput.errorText = null;
+						view.widthInput.errorText = null;
+						view.heightInput.errorText = null;
+
+						expect( view.saveButtonView.isEnabled ).to.be.false;
+
+						view.borderWidthInput.errorText = 'foo';
+						view.borderColorInput.errorText = null;
+						view.backgroundInput.errorText = null;
+						view.widthInput.errorText = null;
+						view.heightInput.errorText = null;
+
+						expect( view.saveButtonView.isEnabled ).to.be.false;
+
+						view.borderWidthInput.errorText = null;
+						view.borderColorInput.errorText = null;
+						view.backgroundInput.errorText = null;
+						view.widthInput.errorText = null;
+						view.heightInput.errorText = null;
+
+						expect( view.saveButtonView.isEnabled ).to.be.true;
+					} );
 				} );
 			} );
 
@@ -454,7 +581,7 @@ describe( 'table properties', () => {
 
 			it( 'should register child views\' #element in #focusTracker', () => {
 				const spy = testUtils.sinon.spy( FocusTracker.prototype, 'add' );
-				const view = new TablePropertiesView( { t: val => val } );
+				const view = new TablePropertiesView( { t: val => val }, VIEW_OPTIONS );
 				view.render();
 
 				sinon.assert.calledWith( spy, view.borderStyleDropdown.element );
@@ -471,7 +598,7 @@ describe( 'table properties', () => {
 			} );
 
 			it( 'starts listening for #keystrokes coming from #element', () => {
-				const view = new TablePropertiesView( { t: val => val } );
+				const view = new TablePropertiesView( { t: val => val }, VIEW_OPTIONS );
 				const spy = sinon.spy( view.keystrokes, 'listenTo' );
 
 				view.render();

+ 13 - 1
packages/ckeditor5-table/tests/tableui.js

@@ -11,6 +11,7 @@ import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 
 import TableEditing from '../src/tableediting';
 import TableUI from '../src/tableui';
+import InsertTableView from '../src/ui/inserttableview';
 import SwitchButtonView from '@ckeditor/ckeditor5-ui/src/button/switchbuttonview';
 import DropdownView from '@ckeditor/ckeditor5-ui/src/dropdown/dropdownview';
 import ListSeparatorView from '@ckeditor/ckeditor5-ui/src/list/listseparatorview';
@@ -53,6 +54,7 @@ describe( 'TableUI', () => {
 
 		beforeEach( () => {
 			insertTable = editor.ui.componentFactory.create( 'insertTable' );
+			insertTable.isOpen = true; // Dropdown is lazy loaded, so make sure its open (#6193).
 		} );
 
 		it( 'should register insertTable button', () => {
@@ -65,7 +67,7 @@ describe( 'TableUI', () => {
 			const command = editor.commands.get( 'insertTable' );
 
 			command.isEnabled = true;
-			expect( insertTable.buttonView.isOn ).to.be.false;
+			expect( insertTable.buttonView.isOn ).to.be.true;
 			expect( insertTable.buttonView.isEnabled ).to.be.true;
 
 			command.isEnabled = false;
@@ -87,6 +89,8 @@ describe( 'TableUI', () => {
 		} );
 
 		it( 'should reset rows & columns on dropdown open', () => {
+			insertTable.isOpen = true;
+
 			const tableSizeView = insertTable.panelView.children.first;
 
 			expect( tableSizeView.rows ).to.equal( 0 );
@@ -100,6 +104,14 @@ describe( 'TableUI', () => {
 			expect( tableSizeView.rows ).to.equal( 0 );
 			expect( tableSizeView.columns ).to.equal( 0 );
 		} );
+
+		it( 'is not fully initialized when not open', () => {
+			const dropdown = editor.ui.componentFactory.create( 'insertTable' );
+
+			for ( const childView of dropdown.panelView.children ) {
+				expect( childView ).not.to.be.instanceOf( InsertTableView );
+			}
+		} );
 	} );
 
 	describe( 'tableRow dropdown', () => {

+ 301 - 0
packages/ckeditor5-table/tests/ui/colorinputview.js

@@ -0,0 +1,301 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+import ColorInputView from '../../src/ui/colorinputview';
+import InputTextView from '@ckeditor/ckeditor5-ui/src/inputtext/inputtextview';
+import ColorGridView from '@ckeditor/ckeditor5-ui/src/colorgrid/colorgridview';
+import DropdownView from '@ckeditor/ckeditor5-ui/src/dropdown/dropdownview';
+import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
+
+const DEFAULT_COLORS = [
+	{
+		color: 'rgb(255,0,0)',
+		label: 'Red',
+		options: {}
+	},
+	{
+		color: 'rgb(0,255,0)',
+		label: 'Green',
+		options: {
+			hasBorder: true
+		}
+	},
+	{
+		color: 'rgb(0,0,255)',
+		label: 'Blue',
+		options: {}
+	}
+];
+
+describe( 'ColorInputView', () => {
+	let view, locale, colorGridView, removeColorButton, inputView;
+
+	beforeEach( () => {
+		locale = { t: val => val };
+		view = new ColorInputView( locale, {
+			colorDefinitions: DEFAULT_COLORS,
+			columns: 5
+		} );
+		view.render();
+
+		inputView = view._inputView;
+		removeColorButton = view._dropdownView.panelView.children.first;
+		colorGridView = view._dropdownView.panelView.children.last;
+	} );
+
+	afterEach( () => {
+		view.destroy();
+	} );
+
+	describe( 'constructor()', () => {
+		it( 'should set view#options', () => {
+			expect( view.options ).to.deep.equal( {
+				colorDefinitions: DEFAULT_COLORS,
+				columns: 5
+			} );
+		} );
+
+		it( 'should set view#locale', () => {
+			expect( view.locale ).to.equal( locale );
+		} );
+
+		it( 'should set #isReadOnly', () => {
+			expect( view.isReadOnly ).to.be.false;
+		} );
+
+		it( 'should set #hasError', () => {
+			expect( view.hasError ).to.be.false;
+		} );
+
+		describe( 'dropdown', () => {
+			it( 'should be created', () => {
+				expect( view._dropdownView ).to.be.instanceOf( DropdownView );
+				expect( view._dropdownView.buttonView.element.classList.contains( 'ck-input-color__button' ) ).to.be.true;
+				expect( view._dropdownView.panelPosition ).to.equal( 'sw' );
+			} );
+
+			it( 'should bind #isEnabled to the view\'s #isReadOnly', () => {
+				view.isReadOnly = false;
+				expect( view._dropdownView.isEnabled ).to.be.true;
+
+				view.isReadOnly = true;
+				expect( view._dropdownView.isEnabled ).to.be.false;
+			} );
+
+			it( 'should have the color preview', () => {
+				const preview = view._dropdownView.buttonView.children.first;
+
+				expect( preview.element.classList.contains( 'ck' ) ).to.be.true;
+				expect( preview.element.classList.contains( 'ck-input-color__button__preview' ) ).to.be.true;
+			} );
+
+			it( 'should display no-color preview when color is not set', () => {
+				const preview = view._dropdownView.buttonView.children.first;
+				const noColorPreview = preview.element.firstChild;
+
+				view.value = 'hsl(0, 0, 50%)';
+
+				expect( noColorPreview.classList.contains( 'ck-hidden' ) ).to.be.true;
+
+				view.value = '';
+
+				expect( noColorPreview.classList.contains( 'ck-hidden' ) ).to.be.false;
+			} );
+
+			it( 'should have the remove color button', () => {
+				expect( view._dropdownView.panelView.children.first ).to.be.instanceOf( ButtonView );
+			} );
+		} );
+
+		describe( 'color grid', () => {
+			it( 'should be an instance of ColorGridView', () => {
+				expect( colorGridView ).to.be.instanceOf( ColorGridView );
+			} );
+
+			it( 'should set #value upon #execute', () => {
+				expect( view.value ).to.equal( '' );
+
+				colorGridView.items.last.fire( 'execute' );
+
+				expect( view.value ).to.equal( 'rgb(0,0,255)' );
+			} );
+
+			it( 'should close the dropdown upon #execute', () => {
+				view._dropdownView.isOpen = true;
+
+				colorGridView.items.last.fire( 'execute' );
+
+				expect( view._dropdownView.isOpen ).to.be.false;
+			} );
+
+			it( 'should fire #input upon #execute', () => {
+				const spy = sinon.spy( view, 'fire' );
+
+				colorGridView.items.last.fire( 'execute' );
+
+				sinon.assert.calledWithExactly( spy.lastCall, 'input' );
+			} );
+
+			it( 'should have #selectedColor bound to the #value', () => {
+				view.value = 'rgb(0,255,0)';
+				expect( colorGridView.selectedColor ).to.equal( 'rgb(0,255,0)' );
+
+				view.value = 'rgb(0,0,255)';
+				expect( colorGridView.selectedColor ).to.equal( 'rgb(0,0,255)' );
+			} );
+		} );
+
+		describe( 'remove color button', () => {
+			it( 'should be created from the template', () => {
+				expect( removeColorButton.element.classList.contains( 'ck-input-color__remove-color' ) ).to.be.true;
+				expect( removeColorButton.withText ).to.be.true;
+				expect( removeColorButton.label ).to.equal( 'Remove color' );
+			} );
+
+			it( 'should set the empty #value upon #execute', () => {
+				view.value = 'foo';
+
+				removeColorButton.fire( 'execute' );
+
+				expect( view.value ).to.equal( '' );
+			} );
+
+			it( 'should close the #_dropdownView upon #execute', () => {
+				view._dropdownView.isOpen = true;
+
+				removeColorButton.fire( 'execute' );
+
+				expect( view._dropdownView.isOpen ).to.be.false;
+			} );
+		} );
+
+		describe( 'text input', () => {
+			it( 'should be created', () => {
+				expect( inputView ).to.be.instanceOf( InputTextView );
+			} );
+
+			it( 'should have #value bound to the color input', () => {
+				view.value = 'foo';
+				expect( inputView.value ).to.equal( 'foo' );
+
+				view.value = 'bar';
+				expect( inputView.value ).to.equal( 'bar' );
+			} );
+
+			it( 'should have #isReadOnly bound to the color input', () => {
+				view.isReadOnly = true;
+				expect( inputView.isReadOnly ).to.equal( true );
+
+				view.isReadOnly = false;
+				expect( inputView.isReadOnly ).to.equal( false );
+			} );
+
+			it( 'should have #hasError bound to the color input', () => {
+				view.hasError = true;
+				expect( inputView.hasError ).to.equal( true );
+
+				view.hasError = false;
+				expect( inputView.hasError ).to.equal( false );
+			} );
+
+			it( 'should set #value on #input event', () => {
+				inputView.element.value = 'foo';
+				inputView.fire( 'input' );
+
+				expect( view.value ).to.equal( 'foo' );
+
+				inputView.element.value = 'bar';
+				inputView.fire( 'input' );
+
+				expect( view.value ).to.equal( 'bar' );
+			} );
+
+			it( 'should have #input event delegated to the color input', () => {
+				const spy = sinon.spy();
+				view.on( 'input', spy );
+
+				inputView.fire( 'input' );
+				sinon.assert.calledOnce( spy );
+			} );
+		} );
+
+		it( 'should set the template', () => {
+			expect( view.element.classList.contains( 'ck' ) ).to.be.true;
+			expect( view.element.classList.contains( 'ck-input-color' ) ).to.be.true;
+			expect( view.element.firstChild ).to.equal( inputView.element );
+			expect( view.element.lastChild ).to.equal( view._dropdownView.element );
+		} );
+
+		describe( 'options', () => {
+			it( 'should pass the color definitions to the color grid', () => {
+				const colorTiles = colorGridView.items.map( ( { color, hasBorder, label } ) => {
+					return { color, hasBorder, label };
+				} );
+
+				expect( colorTiles ).to.deep.equal( [
+					{
+						color: 'rgb(255,0,0)',
+						label: 'Red',
+						hasBorder: undefined
+					},
+					{
+						color: 'rgb(0,255,0)',
+						label: 'Green',
+						hasBorder: true
+					},
+					{
+						color: 'rgb(0,0,255)',
+						label: 'Blue',
+						hasBorder: undefined
+					}
+				] );
+			} );
+
+			it( 'should pass the number of columns to the color grid', () => {
+				expect( colorGridView.element.getAttribute( 'style' ) ).to.match( /repeat\(5/g );
+			} );
+		} );
+
+		describe( 'template bindings', () => {
+			it( 'should bind the element class to #hasError', () => {
+				expect( view.element.classList.contains( 'ck-error' ) ).to.be.false;
+
+				view.hasError = true;
+				expect( view.element.classList.contains( 'ck-error' ) ).to.be.true;
+			} );
+
+			it( 'should bind element id to #id', () => {
+				expect( view.element.id ).to.equal( '' );
+
+				view.id = 'foo';
+				expect( view.element.id ).to.equal( 'foo' );
+			} );
+
+			it( 'should bind the "aria-invalid" attribute to #hasError', () => {
+				expect( view.element.getAttribute( 'aria-invalid' ) ).to.be.null;
+
+				view.hasError = true;
+				expect( view.element.getAttribute( 'aria-invalid' ) ).to.equal( 'true' );
+			} );
+
+			it( 'should bind the "aria-describedby" attribute to #ariaDescribedById', () => {
+				expect( view.element.getAttribute( 'aria-describedby' ) ).to.be.null;
+
+				view.ariaDescribedById = 'foo';
+				expect( view.element.getAttribute( 'aria-describedby' ) ).to.equal( 'foo' );
+			} );
+		} );
+	} );
+
+	describe( 'focus()', () => {
+		it( 'should focus the input', () => {
+			const spy = sinon.spy( inputView, 'focus' );
+
+			view.focus();
+
+			sinon.assert.calledOnce( spy );
+		} );
+	} );
+} );

+ 140 - 10
packages/ckeditor5-table/tests/ui/utils.js

@@ -19,6 +19,11 @@ import {
 	getBalloonCellPositionData,
 	getBorderStyleDefinitions,
 	getBorderStyleLabels,
+	getLocalizedColorErrorText,
+	getLocalizedLengthErrorText,
+	lengthFieldValidator,
+	lineWidthFieldValidator,
+	colorFieldValidator,
 	fillToolbar
 } from '../../src/ui/utils';
 import Collection from '@ckeditor/ckeditor5-utils/src/collection';
@@ -145,7 +150,7 @@ describe( 'UI Utils', () => {
 		} );
 	} );
 
-	describe( 'getBalloonCellPositionData', () => {
+	describe( 'getBalloonCellPositionData()', () => {
 		it( 'returns the position data', () => {
 			const defaultPositions = BalloonPanelView.defaultPositions;
 
@@ -190,6 +195,112 @@ describe( 'UI Utils', () => {
 		} );
 	} );
 
+	describe( 'getLocalizedColorErrorText()', () => {
+		it( 'should return the error text', () => {
+			const t = string => string;
+
+			expect( getLocalizedColorErrorText( t ) ).to.match( /^The color is invalid/ );
+		} );
+	} );
+
+	describe( 'getLocalizedLengthErrorText()', () => {
+		it( 'should return the error text', () => {
+			const t = string => string;
+
+			expect( getLocalizedLengthErrorText( t ) ).to.match( /^The value is invalid/ );
+		} );
+	} );
+
+	describe( 'colorFieldValidator()', () => {
+		it( 'should pass for an empty value', () => {
+			expect( colorFieldValidator( '' ) ).to.be.true;
+		} );
+
+		it( 'should pass for white spaces', () => {
+			expect( colorFieldValidator( '  ' ) ).to.be.true;
+		} );
+
+		it( 'should pass for colors', () => {
+			expect( colorFieldValidator( '#FFF' ) ).to.be.true;
+			expect( colorFieldValidator( '#FFAA11' ) ).to.be.true;
+			expect( colorFieldValidator( 'rgb(255,123,100)' ) ).to.be.true;
+			expect( colorFieldValidator( 'red' ) ).to.be.true;
+		} );
+
+		it( 'should pass for colors surrounded by white spaces', () => {
+			expect( colorFieldValidator( ' #AAA ' ) ).to.be.true;
+			expect( colorFieldValidator( ' rgb(255,123,100) ' ) ).to.be.true;
+		} );
+	} );
+
+	describe( 'lengthFieldValidator()', () => {
+		it( 'should pass for an empty value', () => {
+			expect( lengthFieldValidator( '' ) ).to.be.true;
+		} );
+
+		it( 'should pass for white spaces', () => {
+			expect( lengthFieldValidator( '  ' ) ).to.be.true;
+		} );
+
+		it( 'should pass for lengths', () => {
+			expect( lengthFieldValidator( '1px' ) ).to.be.true;
+			expect( lengthFieldValidator( '12em' ) ).to.be.true;
+			expect( lengthFieldValidator( ' 12em ' ) ).to.be.true;
+			expect( lengthFieldValidator( '45%' ) ).to.be.true;
+		} );
+
+		it( 'should pass for number without unit', () => {
+			expect( lengthFieldValidator( '1' ) ).to.be.true;
+			expect( lengthFieldValidator( '12.1' ) ).to.be.true;
+			expect( lengthFieldValidator( '0.125 ' ) ).to.be.true;
+		} );
+
+		it( 'should not pass for invalid number values', () => {
+			expect( lengthFieldValidator( '.1 ' ) ).to.be.false;
+			expect( lengthFieldValidator( '45. ' ) ).to.be.false;
+			expect( lengthFieldValidator( '45.1.1 ' ) ).to.be.false;
+		} );
+
+		it( 'should pass for lengths surrounded by white spaces', () => {
+			expect( lengthFieldValidator( '3px ' ) ).to.be.true;
+			expect( lengthFieldValidator( ' 12em ' ) ).to.be.true;
+		} );
+	} );
+
+	describe( 'lineWidthFieldValidator()', () => {
+		it( 'should pass for an empty value', () => {
+			expect( lineWidthFieldValidator( '' ) ).to.be.true;
+		} );
+
+		it( 'should pass for white spaces', () => {
+			expect( lineWidthFieldValidator( '  ' ) ).to.be.true;
+		} );
+
+		it( 'should pass for lengths', () => {
+			expect( lineWidthFieldValidator( '1px' ) ).to.be.true;
+			expect( lineWidthFieldValidator( '12em' ) ).to.be.true;
+			expect( lineWidthFieldValidator( ' 12em ' ) ).to.be.true;
+		} );
+
+		it( 'should pass for number without unit', () => {
+			expect( lineWidthFieldValidator( '1' ) ).to.be.true;
+			expect( lineWidthFieldValidator( '12.1' ) ).to.be.true;
+			expect( lineWidthFieldValidator( '0.125 ' ) ).to.be.true;
+		} );
+
+		it( 'should not pass for invalid number values', () => {
+			expect( lineWidthFieldValidator( '.1 ' ) ).to.be.false;
+			expect( lineWidthFieldValidator( '45. ' ) ).to.be.false;
+			expect( lineWidthFieldValidator( '45.1.1 ' ) ).to.be.false;
+			expect( lineWidthFieldValidator( '45%' ) ).to.be.false;
+		} );
+
+		it( 'should pass for lengths surrounded by white spaces', () => {
+			expect( lineWidthFieldValidator( '3px ' ) ).to.be.true;
+			expect( lineWidthFieldValidator( ' 12em ' ) ).to.be.true;
+		} );
+	} );
+
 	describe( 'getBorderStyleDefinitions()', () => {
 		let view, locale, definitions;
 
@@ -253,7 +364,7 @@ describe( 'UI Utils', () => {
 				false,
 				false,
 				true,
-				false,
+				false
 			] );
 		} );
 	} );
@@ -263,12 +374,14 @@ describe( 'UI Utils', () => {
 
 		const labels = {
 			first: 'Do something',
-			second: 'Do something else'
+			second: 'Do something else',
+			third: 'Be default'
 		};
 
 		const icons = {
-			first: '<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path /></svg>',
-			second: '<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path /></svg>'
+			first: '<svg viewBox="0 0 21 21" xmlns="http://www.w3.org/2000/svg"><path /></svg>',
+			second: '<svg viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg"><path /></svg>',
+			third: '<svg viewBox="0 0 23 23" xmlns="http://www.w3.org/2000/svg"><path /></svg>'
 		};
 
 		beforeEach( () => {
@@ -279,7 +392,8 @@ describe( 'UI Utils', () => {
 
 			fillToolbar( {
 				view, toolbar, icons, labels,
-				propertyName: 'someProperty'
+				propertyName: 'someProperty',
+				nameToValue: name => name === 'third' ? '' : name
 			} );
 		} );
 
@@ -288,33 +402,45 @@ describe( 'UI Utils', () => {
 		} );
 
 		it( 'should create buttons', () => {
-			expect( toolbar.items ).to.have.length( 2 );
+			expect( toolbar.items ).to.have.length( 3 );
 			expect( toolbar.items.first ).to.be.instanceOf( ButtonView );
+			expect( toolbar.items.get( 1 ) ).to.be.instanceOf( ButtonView );
 			expect( toolbar.items.last ).to.be.instanceOf( ButtonView );
 		} );
 
 		it( 'should set button labels', () => {
 			expect( toolbar.items.first.label ).to.equal( 'Do something' );
-			expect( toolbar.items.last.label ).to.equal( 'Do something else' );
+			expect( toolbar.items.get( 1 ).label ).to.equal( 'Do something else' );
+			expect( toolbar.items.last.label ).to.equal( 'Be default' );
 		} );
 
 		it( 'should set button icons', () => {
 			expect( toolbar.items.first.icon ).to.equal( icons.first );
-			expect( toolbar.items.last.icon ).to.equal( icons.second );
+			expect( toolbar.items.get( 1 ).icon ).to.equal( icons.second );
+			expect( toolbar.items.last.icon ).to.equal( icons.third );
 		} );
 
 		it( 'should bind button #isOn to an observable property', () => {
 			expect( toolbar.items.first.isOn ).to.be.false;
+			expect( toolbar.items.get( 1 ).isOn ).to.be.false;
 			expect( toolbar.items.last.isOn ).to.be.false;
 
 			view.someProperty = 'first';
 
 			expect( toolbar.items.first.isOn ).to.be.true;
+			expect( toolbar.items.get( 1 ).isOn ).to.be.false;
 			expect( toolbar.items.last.isOn ).to.be.false;
 
 			view.someProperty = 'second';
 
 			expect( toolbar.items.first.isOn ).to.be.false;
+			expect( toolbar.items.get( 1 ).isOn ).to.be.true;
+			expect( toolbar.items.last.isOn ).to.be.false;
+
+			view.someProperty = '';
+
+			expect( toolbar.items.first.isOn ).to.be.false;
+			expect( toolbar.items.get( 1 ).isOn ).to.be.false;
 			expect( toolbar.items.last.isOn ).to.be.true;
 		} );
 
@@ -323,9 +449,13 @@ describe( 'UI Utils', () => {
 
 			expect( view.someProperty ).to.equal( 'first' );
 
-			toolbar.items.last.fire( 'execute' );
+			toolbar.items.get( 1 ).fire( 'execute' );
 
 			expect( view.someProperty ).to.equal( 'second' );
+
+			toolbar.items.last.fire( 'execute' );
+
+			expect( view.someProperty ).to.equal( '' );
 		} );
 	} );
 } );

+ 40 - 0
packages/ckeditor5-table/theme/colorinput.css

@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+.ck.ck-input-color {
+	width: 100%;
+	display: flex;
+
+	& > input.ck.ck-input-text {
+		min-width: auto;
+		flex-grow: 1;
+
+		&:active,
+		&:focus {
+			z-index: var(--ck-z-default);
+		}
+	}
+
+	& > div.ck.ck-dropdown {
+		min-width: auto;
+
+		/* This dropdown has no arrow but a color preview instead. */
+		& > .ck-input-color__button .ck-dropdown__arrow {
+			display: none;
+		}
+	}
+
+	& .ck.ck-input-color__button {
+		& .ck.ck-input-color__button__preview {
+			position: relative;
+			overflow: hidden;
+
+			& > .ck.ck-input-color__button__preview__no-color-indicator {
+				position: absolute;
+				display: block;
+			}
+		}
+	}
+}

+ 5 - 0
packages/ckeditor5-table/theme/table.css

@@ -13,6 +13,11 @@
 		border-collapse: collapse;
 		border-spacing: 0;
 
+		/* Table width and height are set on the parent <figure>. Make sure the table inside stretches
+		to the full dimensions of the container (https://github.com/ckeditor/ckeditor5/issues/6186). */
+		width: 100%;
+		height: 100%;
+
 		/* The outer border of the table should be slightly darker than the inner lines.
 		Also see https://github.com/ckeditor/ckeditor5-table/issues/50. */
 		border: 1px double hsl(0, 0%, 70%);

+ 53 - 17
packages/ckeditor5-table/theme/tableform.css

@@ -4,33 +4,69 @@
  */
 
 .ck.ck-table-form {
-	& .ck-table-form__border-row {
-		flex-wrap: wrap;
+	& .ck-form__row {
+		&.ck-table-form__border-row {
+			flex-wrap: wrap;
 
-		& .ck-labeled-view {
-			display: flex;
-			flex-direction: column-reverse;
-			align-items: center;
+			& .ck-labeled-view {
+				display: flex;
+				flex-direction: column-reverse;
+
+				& .ck.ck-dropdown {
+					flex-grow: 0;
+				}
+			}
 
-			& > .ck-label {
-				font-size: 10px;
+			& .ck-table-form__border-style {
+				flex-grow: 0;
 			}
 
-			& .ck.ck-dropdown {
+			& .ck-table-form__border-width {
 				flex-grow: 0;
 			}
 		}
 
-		& .ck-table-form__border-style {
-			width: 80px;
-			min-width: 80px;
-			flex-grow: 0;
+		&.ck-table-form__dimensions-row {
+			flex-wrap: wrap;
+			align-items: center;
+
+			& .ck-labeled-view {
+				display: flex;
+				flex-direction: column-reverse;
+				align-items: center;
+
+				& .ck.ck-dropdown {
+					flex-grow: 0;
+				}
+			}
+
+			& .ck-table-form__dimension-operator {
+				flex-grow: 0;
+			}
 		}
+	}
 
-		& .ck-table-form__border-width {
-			width: 80px;
-			min-width: 80px;
-			flex-grow: 0;
+	& .ck.ck-labeled-view {
+		/* Allow absolute positioning of the status (error) balloons. */
+		position: relative;
+
+		& .ck.ck-labeled-view__status {
+			position: absolute;
+			left: 50%;
+			bottom: calc( -1 * var(--ck-table-properties-error-arrow-size) );
+			transform: translate(-50%,100%);
+
+			/* Make sure the balloon status stays on top of other form elements. */
+			z-index: 1;
+
+			/* The arrow pointing towards the field. */
+			&::after {
+				content: "";
+				position: absolute;
+				top: calc( -1 * var(--ck-table-properties-error-arrow-size) );
+				left: 50%;
+				transform: translateX( -50% );
+			}
 		}
 	}
 }

+ 0 - 23
packages/ckeditor5-table/theme/tableproperties.css

@@ -5,29 +5,6 @@
 
 .ck.ck-table-properties-form {
 	& .ck-form__row {
-		&.ck-table-properties-form__dimensions-row,
-		&.ck-table-properties-form__alignment-row {
-			flex-wrap: wrap;
-		}
-
-		&.ck-table-properties-form__dimensions-row {
-			align-items: center;
-
-			& .ck-table-properties-form__dimension-operator {
-				flex-grow: 0;
-			}
-
-			& .ck-labeled-view {
-				display: flex;
-				flex-direction: column-reverse;
-				align-items: center;
-
-				& .ck.ck-dropdown {
-					flex-grow: 0;
-				}
-			}
-		}
-
 		&.ck-table-properties-form__alignment-row {
 			flex-wrap: wrap;
 			flex-basis: 0;