浏览代码

Merge pull request #244 from ckeditor/i/6106

Internal: Introduced color pickers in the table and table cell property forms. Introduced config.table.tableProperties and config.table.tableCellProperties configurations. Closes ckeditor/ckeditor5/issues/6106.
Piotrek Koszuliński 5 年之前
父节点
当前提交
2dc8c6125b

+ 2 - 1
packages/ckeditor5-table/lang/contexts.json

@@ -22,7 +22,7 @@
 	"Cell properties": "The label describing the form allowing to specify the properties of a selected table cell.",
 	"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.).",
 	"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.",
 	"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.",
 	"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.",
 	"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.",
 	"Background": "The label for the input that allows configuring the background color of a table or a table cell.",
@@ -52,6 +52,7 @@
 	"Align table to the left": "The label used by assistive technologies describing a button that aligns the table to the left.",
 	"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.",
 	"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 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."
 	"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/table.js

@@ -63,3 +63,31 @@ export default class Table extends Plugin {
  *
  *
  * @member {module:table/table~TableConfig} module:core/editor/editorconfig~EditorConfig#table
  * @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 ];
 		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
+ */

+ 26 - 1
packages/ckeditor5-table/src/tablecellproperties/tablecellpropertiesui.js

@@ -19,10 +19,15 @@ import {
 	getBalloonCellPositionData,
 	getBalloonCellPositionData,
 	getLocalizedColorErrorText,
 	getLocalizedColorErrorText,
 	getLocalizedLengthErrorText,
 	getLocalizedLengthErrorText,
+	defaultColors,
 	lengthFieldValidator,
 	lengthFieldValidator,
 	lineWidthFieldValidator,
 	lineWidthFieldValidator,
 	repositionContextualBalloon
 	repositionContextualBalloon
 } from '../ui/utils';
 } from '../ui/utils';
+import {
+	getLocalizedColorOptions,
+	normalizeColorOptions
+} from '@ckeditor/ckeditor5-ui/src/colorgrid/utils';
 import { debounce } from 'lodash-es';
 import { debounce } from 'lodash-es';
 
 
 const ERROR_TEXT_TIMEOUT = 500;
 const ERROR_TEXT_TIMEOUT = 500;
@@ -54,6 +59,18 @@ export default class TableCellPropertiesUI extends Plugin {
 	/**
 	/**
 	 * @inheritDoc
 	 * @inheritDoc
 	 */
 	 */
+	constructor( editor ) {
+		super( editor );
+
+		editor.config.define( 'table.tableCellProperties', {
+			borderColors: defaultColors,
+			backgroundColors: defaultColors
+		} );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
 	init() {
 	init() {
 		const editor = this.editor;
 		const editor = this.editor;
 		const t = editor.t;
 		const t = editor.t;
@@ -118,7 +135,15 @@ export default class TableCellPropertiesUI extends Plugin {
 	_createPropertiesView() {
 	_createPropertiesView() {
 		const editor = this.editor;
 		const editor = this.editor;
 		const viewDocument = editor.editing.view.document;
 		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;
 		const t = editor.t;
 
 
 		// Render the view so its #element is available for the clickOutsideHandler.
 		// Render the view so its #element is available for the clickOutsideHandler.

+ 45 - 13
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 { addListToDropdown } from '@ckeditor/ckeditor5-ui/src/dropdown/utils';
 import ToolbarView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarview';
 import ToolbarView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarview';
 import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
 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 FormRowView from '../../ui/formrowview';
 import FormHeaderView from '../../ui/formheaderview';
 import FormHeaderView from '../../ui/formheaderview';
 
 
@@ -57,9 +62,16 @@ const ALIGNMENT_ICONS = {
  */
  */
 export default class TableCellPropertiesView extends View {
 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 );
 		super( locale );
 
 
 		this.set( {
 		this.set( {
@@ -145,6 +157,13 @@ export default class TableCellPropertiesView extends View {
 			verticalAlignment: ''
 			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 { borderStyleDropdown, borderWidthInput, borderColorInput, borderRowLabel } = this._createBorderFields();
 		const { widthInput, operatorLabel, heightInput, dimensionsLabel } = this._createDimensionFields();
 		const { widthInput, operatorLabel, heightInput, dimensionsLabel } = this._createDimensionFields();
 		const { horizontalAlignmentToolbar, verticalAlignmentToolbar, alignmentLabel } = this._createAlignmentFields();
 		const { horizontalAlignmentToolbar, verticalAlignmentToolbar, alignmentLabel } = this._createAlignmentFields();
@@ -193,7 +212,7 @@ export default class TableCellPropertiesView extends View {
 		 * An input that allows specifying the color of the table cell border.
 		 * An input that allows specifying the color of the table cell border.
 		 *
 		 *
 		 * @readonly
 		 * @readonly
-		 * @member {module:ui/inputtext/inputtextview~InputTextView}
+		 * @member {module:table/ui/colorinputview~ColorInputView}
 		 */
 		 */
 		this.borderColorInput = borderColorInput;
 		this.borderColorInput = borderColorInput;
 
 
@@ -201,7 +220,7 @@ export default class TableCellPropertiesView extends View {
 		 * An input that allows specifying the table cell background color.
 		 * An input that allows specifying the table cell background color.
 		 *
 		 *
 		 * @readonly
 		 * @readonly
-		 * @member {module:ui/inputtext/inputtextview~InputTextView}
+		 * @member {module:table/ui/colorinputview~ColorInputView}
 		 */
 		 */
 		this.backgroundInput = this._createBackgroundField();
 		this.backgroundInput = this._createBackgroundField();
 
 
@@ -329,7 +348,7 @@ export default class TableCellPropertiesView extends View {
 						operatorLabel,
 						operatorLabel,
 						heightInput
 						heightInput
 					],
 					],
-					class: 'ck-table-cell-properties-form__dimensions-row'
+					class: 'ck-table-form__dimensions-row'
 				} ),
 				} ),
 				// Padding row.
 				// Padding row.
 				new FormRowView( locale, {
 				new FormRowView( locale, {
@@ -431,6 +450,10 @@ export default class TableCellPropertiesView extends View {
 	 * @returns {Object.<String,module:ui/view~View>}
 	 * @returns {Object.<String,module:ui/view~View>}
 	 */
 	 */
 	_createBorderFields() {
 	_createBorderFields() {
+		const colorInputCreator = getLabeledColorInputCreator( {
+			colorConfig: this.options.borderColors,
+			columns: 5
+		} );
 		const locale = this.locale;
 		const locale = this.locale;
 		const t = this.t;
 		const t = this.t;
 
 
@@ -481,13 +504,18 @@ export default class TableCellPropertiesView extends View {
 
 
 		// -- Color ---------------------------------------------------
 		// -- Color ---------------------------------------------------
 
 
-		const borderColorInput = new LabeledView( locale, createLabeledInputText );
-		borderColorInput.label = t( 'Color' );
+		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.view.bind( 'value' ).to( this, 'borderColor' );
 		borderColorInput.bind( 'isEnabled' ).to( this, 'borderStyle', isBorderStyleSet );
 		borderColorInput.bind( 'isEnabled' ).to( this, 'borderStyle', isBorderStyleSet );
 
 
 		borderColorInput.view.on( 'input', () => {
 		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".
 		// Reset the border color and width fields when style is "none".
@@ -518,8 +546,12 @@ export default class TableCellPropertiesView extends View {
 	_createBackgroundField() {
 	_createBackgroundField() {
 		const locale = this.locale;
 		const locale = this.locale;
 		const t = this.t;
 		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( {
 		backgroundInput.set( {
 			label: t( 'Background' ),
 			label: t( 'Background' ),
@@ -528,7 +560,7 @@ export default class TableCellPropertiesView extends View {
 
 
 		backgroundInput.view.bind( 'value' ).to( this, 'backgroundColor' );
 		backgroundInput.view.bind( 'value' ).to( this, 'backgroundColor' );
 		backgroundInput.view.on( 'input', () => {
 		backgroundInput.view.on( 'input', () => {
-			this.backgroundColor = backgroundInput.view.element.value;
+			this.backgroundColor = backgroundInput.view.value;
 		} );
 		} );
 
 
 		return backgroundInput;
 		return backgroundInput;
@@ -558,7 +590,7 @@ export default class TableCellPropertiesView extends View {
 
 
 		widthInput.set( {
 		widthInput.set( {
 			label: t( 'Width' ),
 			label: t( 'Width' ),
-			class: 'ck-table-cell-properties-form__width',
+			class: 'ck-table-form__dimensions-row__width',
 		} );
 		} );
 
 
 		widthInput.view.bind( 'value' ).to( this, 'width' );
 		widthInput.view.bind( 'value' ).to( this, 'width' );
@@ -587,7 +619,7 @@ export default class TableCellPropertiesView extends View {
 
 
 		heightInput.set( {
 		heightInput.set( {
 			label: t( 'Height' ),
 			label: t( 'Height' ),
-			class: 'ck-table-cell-properties-form__height',
+			class: 'ck-table-form__dimensions-row__height',
 		} );
 		} );
 
 
 		heightInput.view.bind( 'value' ).to( this, 'height' );
 		heightInput.view.bind( 'value' ).to( this, 'height' );

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

@@ -36,3 +36,43 @@ export default class TableProperties extends Plugin {
 		return [ TablePropertiesEditing, TablePropertiesUI ];
 		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
+ */

+ 27 - 2
packages/ckeditor5-table/src/tableproperties/tablepropertiesui.js

@@ -21,8 +21,13 @@ import {
 	getLocalizedLengthErrorText,
 	getLocalizedLengthErrorText,
 	lengthFieldValidator,
 	lengthFieldValidator,
 	lineWidthFieldValidator,
 	lineWidthFieldValidator,
-	repositionContextualBalloon
+	repositionContextualBalloon,
+	defaultColors,
 } from '../ui/utils';
 } from '../ui/utils';
+import {
+	getLocalizedColorOptions,
+	normalizeColorOptions
+} from '@ckeditor/ckeditor5-ui/src/colorgrid/utils';
 import { debounce } from 'lodash-es';
 import { debounce } from 'lodash-es';
 
 
 const ERROR_TEXT_TIMEOUT = 500;
 const ERROR_TEXT_TIMEOUT = 500;
@@ -54,6 +59,18 @@ export default class TablePropertiesUI extends Plugin {
 	/**
 	/**
 	 * @inheritDoc
 	 * @inheritDoc
 	 */
 	 */
+	constructor( editor ) {
+		super( editor );
+
+		editor.config.define( 'table.tableProperties', {
+			borderColors: defaultColors,
+			backgroundColors: defaultColors
+		} );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
 	init() {
 	init() {
 		const editor = this.editor;
 		const editor = this.editor;
 		const t = editor.t;
 		const t = editor.t;
@@ -118,7 +135,15 @@ export default class TablePropertiesUI extends Plugin {
 	_createPropertiesView() {
 	_createPropertiesView() {
 		const editor = this.editor;
 		const editor = this.editor;
 		const viewDocument = editor.editing.view.document;
 		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;
 		const t = editor.t;
 
 
 		// Render the view so its #element is available for the clickOutsideHandler.
 		// Render the view so its #element is available for the clickOutsideHandler.

+ 46 - 13
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 { addListToDropdown } from '@ckeditor/ckeditor5-ui/src/dropdown/utils';
 import ToolbarView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarview';
 import ToolbarView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarview';
 import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
 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 FormRowView from '../../ui/formrowview';
 import FormHeaderView from '../../ui/formheaderview';
 import FormHeaderView from '../../ui/formheaderview';
 
 
@@ -49,9 +54,16 @@ const ALIGNMENT_ICONS = {
  */
  */
 export default class TablePropertiesView extends View {
 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 );
 		super( locale );
 
 
 		this.set( {
 		this.set( {
@@ -119,6 +131,14 @@ export default class TablePropertiesView extends View {
 			alignment: ''
 			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 { borderStyleDropdown, borderWidthInput, borderColorInput, borderRowLabel } = this._createBorderFields();
 		const { widthInput, operatorLabel, heightInput, dimensionsLabel } = this._createDimensionFields();
 		const { widthInput, operatorLabel, heightInput, dimensionsLabel } = this._createDimensionFields();
 		const { alignmentToolbar, alignmentLabel } = this._createAlignmentFields();
 		const { alignmentToolbar, alignmentLabel } = this._createAlignmentFields();
@@ -167,7 +187,7 @@ export default class TablePropertiesView extends View {
 		 * An input that allows specifying the color of the table border.
 		 * An input that allows specifying the color of the table border.
 		 *
 		 *
 		 * @readonly
 		 * @readonly
-		 * @member {module:ui/inputtext/inputtextview~InputTextView}
+		 * @member {module:table/ui/colorinputview~ColorInputView}
 		 */
 		 */
 		this.borderColorInput = borderColorInput;
 		this.borderColorInput = borderColorInput;
 
 
@@ -175,7 +195,7 @@ export default class TablePropertiesView extends View {
 		 * An input that allows specifying the table background color.
 		 * An input that allows specifying the table background color.
 		 *
 		 *
 		 * @readonly
 		 * @readonly
-		 * @member {module:ui/inputtext/inputtextview~InputTextView}
+		 * @member {module:table/ui/colorinputview~ColorInputView}
 		 */
 		 */
 		this.backgroundInput = this._createBackgroundField();
 		this.backgroundInput = this._createBackgroundField();
 
 
@@ -286,7 +306,7 @@ export default class TablePropertiesView extends View {
 						operatorLabel,
 						operatorLabel,
 						heightInput
 						heightInput
 					],
 					],
-					class: 'ck-table-properties-form__dimensions-row'
+					class: 'ck-table-form__dimensions-row'
 				} ),
 				} ),
 				// Alignment row.
 				// Alignment row.
 				new FormRowView( locale, {
 				new FormRowView( locale, {
@@ -377,6 +397,10 @@ export default class TablePropertiesView extends View {
 	 * @returns {Object.<String,module:ui/view~View>}
 	 * @returns {Object.<String,module:ui/view~View>}
 	 */
 	 */
 	_createBorderFields() {
 	_createBorderFields() {
+		const colorInputCreator = getLabeledColorInputCreator( {
+			colorConfig: this.options.borderColors,
+			columns: 5
+		} );
 		const locale = this.locale;
 		const locale = this.locale;
 		const t = this.t;
 		const t = this.t;
 
 
@@ -427,13 +451,18 @@ export default class TablePropertiesView extends View {
 
 
 		// -- Color ---------------------------------------------------
 		// -- Color ---------------------------------------------------
 
 
-		const borderColorInput = new LabeledView( locale, createLabeledInputText );
-		borderColorInput.label = t( 'Color' );
+		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.view.bind( 'value' ).to( this, 'borderColor' );
 		borderColorInput.bind( 'isEnabled' ).to( this, 'borderStyle', isBorderStyleSet );
 		borderColorInput.bind( 'isEnabled' ).to( this, 'borderStyle', isBorderStyleSet );
 
 
 		borderColorInput.view.on( 'input', () => {
 		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".
 		// Reset the border color and width fields when style is "none".
@@ -462,10 +491,14 @@ export default class TablePropertiesView extends View {
 	 * @returns {module:ui/labeledview/labeledview~LabeledView}
 	 * @returns {module:ui/labeledview/labeledview~LabeledView}
 	 */
 	 */
 	_createBackgroundField() {
 	_createBackgroundField() {
+		const backgroundInputCreator = getLabeledColorInputCreator( {
+			colorConfig: this.options.backgroundColors,
+			columns: 5
+		} );
 		const locale = this.locale;
 		const locale = this.locale;
 		const t = this.t;
 		const t = this.t;
 
 
-		const backgroundInput = new LabeledView( locale, createLabeledInputText );
+		const backgroundInput = new LabeledView( locale, backgroundInputCreator );
 
 
 		backgroundInput.set( {
 		backgroundInput.set( {
 			label: t( 'Background' ),
 			label: t( 'Background' ),
@@ -474,7 +507,7 @@ export default class TablePropertiesView extends View {
 
 
 		backgroundInput.view.bind( 'value' ).to( this, 'backgroundColor' );
 		backgroundInput.view.bind( 'value' ).to( this, 'backgroundColor' );
 		backgroundInput.view.on( 'input', () => {
 		backgroundInput.view.on( 'input', () => {
-			this.backgroundColor = backgroundInput.view.element.value;
+			this.backgroundColor = backgroundInput.view.value;
 		} );
 		} );
 
 
 		return backgroundInput;
 		return backgroundInput;
@@ -504,7 +537,7 @@ export default class TablePropertiesView extends View {
 
 
 		widthInput.set( {
 		widthInput.set( {
 			label: t( 'Width' ),
 			label: t( 'Width' ),
-			class: 'ck-table-properties-form__width',
+			class: 'ck-table-form__dimensions-row__width',
 		} );
 		} );
 
 
 		widthInput.view.bind( 'value' ).to( this, 'width' );
 		widthInput.view.bind( 'value' ).to( this, 'width' );
@@ -533,7 +566,7 @@ export default class TablePropertiesView extends View {
 
 
 		heightInput.set( {
 		heightInput.set( {
 			label: t( 'Height' ),
 			label: t( 'Height' ),
-			class: 'ck-table-properties-form__height',
+			class: 'ck-table-form__dimensions-row__height',
 		} );
 		} );
 
 
 		heightInput.view.bind( 'value' ).to( this, 'height' );
 		heightInput.view.bind( 'value' ).to( this, 'height' );

+ 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;
+	}
+}

+ 206 - 0
packages/ckeditor5-table/src/ui/utils.js

@@ -11,6 +11,7 @@ import BalloonPanelView from '@ckeditor/ckeditor5-ui/src/panel/balloon/balloonpa
 import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
 import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
 import Collection from '@ckeditor/ckeditor5-utils/src/collection';
 import Collection from '@ckeditor/ckeditor5-utils/src/collection';
 import Model from '@ckeditor/ckeditor5-ui/src/model';
 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 { isColor, isLength, isPercentage } from '@ckeditor/ckeditor5-engine/src/view/styles/utils';
 import { getTableWidgetAncestor } from '../utils';
 import { getTableWidgetAncestor } from '../utils';
 import { findAncestor } from '../commands/utils';
 import { findAncestor } from '../commands/utils';
@@ -255,6 +256,199 @@ export function fillToolbar( { view, icons, toolbar, labels, propertyName, nameT
 	}
 	}
 }
 }
 
 
+/**
+ * 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.
 // A simple helper method to detect number strings.
 // I allows full number notation, so omitting 0 is not allowed:
 // I allows full number notation, so omitting 0 is not allowed:
 function isNumberString( value ) {
 function isNumberString( value ) {
@@ -262,3 +456,15 @@ function isNumberString( value ) {
 
 
 	return !Number.isNaN( parsedValue ) && value === String( parsedValue );
 	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
+		}
+	} ) );
+}

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

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

+ 18 - 0
packages/ckeditor5-table/tests/tablecellproperties/tablecellpropertiesui.js

@@ -20,6 +20,7 @@ import Table from '../../src/table';
 import TableCellPropertiesEditing from '../../src/tablecellproperties/tablecellpropertiesediting';
 import TableCellPropertiesEditing from '../../src/tablecellproperties/tablecellpropertiesediting';
 import TableCellPropertiesUI from '../../src/tablecellproperties/tablecellpropertiesui';
 import TableCellPropertiesUI from '../../src/tablecellproperties/tablecellpropertiesui';
 import TableCellPropertiesUIView from '../../src/tablecellproperties/ui/tablecellpropertiesview';
 import TableCellPropertiesUIView from '../../src/tablecellproperties/ui/tablecellpropertiesview';
+import { defaultColors } from '../../src/ui/utils';
 
 
 describe( 'table cell properties', () => {
 describe( 'table cell properties', () => {
 	describe( 'TableCellPropertiesUI', () => {
 	describe( 'TableCellPropertiesUI', () => {
@@ -68,6 +69,15 @@ describe( 'table cell properties', () => {
 			expect( editor.plugins.get( ContextualBalloon ) ).to.be.instanceOf( ContextualBalloon );
 			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()', () => {
 		describe( 'init()', () => {
 			it( 'should set a batch', () => {
 			it( 'should set a batch', () => {
 				expect( tableCellPropertiesUI._undoStepBatch ).to.be.null;
 				expect( tableCellPropertiesUI._undoStepBatch ).to.be.null;
@@ -81,6 +91,14 @@ describe( 'table cell properties', () => {
 				it( 'should be rendered', () => {
 				it( 'should be rendered', () => {
 					expect( tableCellPropertiesUI.view.isRendered ).to.be.true;
 					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', () => {
 			describe( 'toolbar button', () => {

+ 70 - 12
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 ToolbarView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarview';
 import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
 import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
 import InputTextView from '@ckeditor/ckeditor5-ui/src/inputtext/inputtextview';
 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( 'table cell properties', () => {
 	describe( 'TableCellPropertiesView', () => {
 	describe( 'TableCellPropertiesView', () => {
@@ -25,7 +48,7 @@ describe( 'table cell properties', () => {
 
 
 		beforeEach( () => {
 		beforeEach( () => {
 			locale = { t: val => val };
 			locale = { t: val => val };
-			view = new TableCellPropertiesView( locale );
+			view = new TableCellPropertiesView( locale, VIEW_OPTIONS );
 			view.render();
 			view.render();
 		} );
 		} );
 
 
@@ -34,6 +57,10 @@ describe( 'table cell properties', () => {
 		} );
 		} );
 
 
 		describe( 'constructor()', () => {
 		describe( 'constructor()', () => {
+			it( 'should set view#options', () => {
+				expect( view.options ).to.deep.equal( VIEW_OPTIONS );
+			} );
+
 			it( 'should set view#locale', () => {
 			it( 'should set view#locale', () => {
 				expect( view.locale ).to.equal( locale );
 				expect( view.locale ).to.equal( locale );
 			} );
 			} );
@@ -198,10 +225,29 @@ describe( 'table cell properties', () => {
 						} );
 						} );
 
 
 						it( 'should be created', () => {
 						it( 'should be created', () => {
-							expect( labeledInput.view ).to.be.instanceOf( InputTextView );
+							expect( labeledInput.view ).to.be.instanceOf( ColorInputView );
 							expect( labeledInput.label ).to.equal( 'Color' );
 							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', () => {
 						it( 'should reflect #borderColor property', () => {
 							view.borderColor = 'foo';
 							view.borderColor = 'foo';
 							expect( labeledInput.view.value ).to.equal( 'foo' );
 							expect( labeledInput.view.value ).to.equal( 'foo' );
@@ -219,11 +265,11 @@ describe( 'table cell properties', () => {
 						} );
 						} );
 
 
 						it( 'should update #borderColor on DOM "input" event', () => {
 						it( 'should update #borderColor on DOM "input" event', () => {
-							labeledInput.view.element.value = 'foo';
+							labeledInput.view.value = 'foo';
 							labeledInput.view.fire( 'input' );
 							labeledInput.view.fire( 'input' );
 							expect( view.borderColor ).to.equal( 'foo' );
 							expect( view.borderColor ).to.equal( 'foo' );
 
 
-							labeledInput.view.element.value = 'bar';
+							labeledInput.view.value = 'bar';
 							labeledInput.view.fire( 'input' );
 							labeledInput.view.fire( 'input' );
 							expect( view.borderColor ).to.equal( 'bar' );
 							expect( view.borderColor ).to.equal( 'bar' );
 						} );
 						} );
@@ -246,11 +292,23 @@ describe( 'table cell properties', () => {
 						} );
 						} );
 
 
 						it( 'should be created', () => {
 						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.label ).to.equal( 'Background' );
 							expect( labeledInput.class ).to.equal( 'ck-table-cell-properties-form__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', () => {
 						it( 'should reflect #backgroundColor property', () => {
 							view.backgroundColor = 'foo';
 							view.backgroundColor = 'foo';
 							expect( labeledInput.view.value ).to.equal( 'foo' );
 							expect( labeledInput.view.value ).to.equal( 'foo' );
@@ -260,11 +318,11 @@ describe( 'table cell properties', () => {
 						} );
 						} );
 
 
 						it( 'should update #backgroundColor on DOM "input" event', () => {
 						it( 'should update #backgroundColor on DOM "input" event', () => {
-							labeledInput.view.element.value = 'foo';
+							labeledInput.view.value = 'foo';
 							labeledInput.view.fire( 'input' );
 							labeledInput.view.fire( 'input' );
 							expect( view.backgroundColor ).to.equal( 'foo' );
 							expect( view.backgroundColor ).to.equal( 'foo' );
 
 
-							labeledInput.view.element.value = 'bar';
+							labeledInput.view.value = 'bar';
 							labeledInput.view.fire( 'input' );
 							labeledInput.view.fire( 'input' );
 							expect( view.backgroundColor ).to.equal( 'bar' );
 							expect( view.backgroundColor ).to.equal( 'bar' );
 						} );
 						} );
@@ -276,7 +334,7 @@ describe( 'table cell properties', () => {
 						const row = view.element.childNodes[ 3 ].childNodes[ 0 ];
 						const row = view.element.childNodes[ 3 ].childNodes[ 0 ];
 
 
 						expect( row.classList.contains( 'ck-form__row' ) ).to.be.true;
 						expect( row.classList.contains( 'ck-form__row' ) ).to.be.true;
-						expect( row.classList.contains( 'ck-table-cell-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[ 0 ].textContent ).to.equal( 'Dimensions' );
 						expect( row.childNodes[ 1 ] ).to.equal( view.widthInput.element );
 						expect( row.childNodes[ 1 ] ).to.equal( view.widthInput.element );
 						expect( row.childNodes[ 2 ].textContent ).to.equal( '×' );
 						expect( row.childNodes[ 2 ].textContent ).to.equal( '×' );
@@ -293,7 +351,7 @@ describe( 'table cell properties', () => {
 						it( 'should be created', () => {
 						it( 'should be created', () => {
 							expect( labeledInput.view ).to.be.instanceOf( InputTextView );
 							expect( labeledInput.view ).to.be.instanceOf( InputTextView );
 							expect( labeledInput.label ).to.equal( 'Width' );
 							expect( labeledInput.label ).to.equal( 'Width' );
-							expect( labeledInput.class ).to.equal( 'ck-table-cell-properties-form__width' );
+							expect( labeledInput.class ).to.equal( 'ck-table-form__dimensions-row__width' );
 						} );
 						} );
 
 
 						it( 'should reflect #width property', () => {
 						it( 'should reflect #width property', () => {
@@ -325,7 +383,7 @@ describe( 'table cell properties', () => {
 						it( 'should be created', () => {
 						it( 'should be created', () => {
 							expect( labeledInput.view ).to.be.instanceOf( InputTextView );
 							expect( labeledInput.view ).to.be.instanceOf( InputTextView );
 							expect( labeledInput.label ).to.equal( 'Height' );
 							expect( labeledInput.label ).to.equal( 'Height' );
-							expect( labeledInput.class ).to.equal( 'ck-table-cell-properties-form__height' );
+							expect( labeledInput.class ).to.equal( 'ck-table-form__dimensions-row__height' );
 						} );
 						} );
 
 
 						it( 'should reflect #height property', () => {
 						it( 'should reflect #height property', () => {
@@ -587,7 +645,7 @@ describe( 'table cell properties', () => {
 
 
 			it( 'should register child views\' #element in #focusTracker', () => {
 			it( 'should register child views\' #element in #focusTracker', () => {
 				const spy = testUtils.sinon.spy( FocusTracker.prototype, 'add' );
 				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();
 				view.render();
 
 
 				sinon.assert.calledWith( spy, view.borderStyleDropdown.element );
 				sinon.assert.calledWith( spy, view.borderStyleDropdown.element );
@@ -604,7 +662,7 @@ describe( 'table cell properties', () => {
 			} );
 			} );
 
 
 			it( 'starts listening for #keystrokes coming from #element', () => {
 			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' );
 				const spy = sinon.spy( view.keystrokes, 'listenTo' );
 
 
 				view.render();
 				view.render();

+ 18 - 0
packages/ckeditor5-table/tests/tableproperties/tablepropertiesui.js

@@ -20,6 +20,7 @@ import Table from '../../src/table';
 import TablePropertiesEditing from '../../src/tableproperties/tablepropertiesediting';
 import TablePropertiesEditing from '../../src/tableproperties/tablepropertiesediting';
 import TablePropertiesUI from '../../src/tableproperties/tablepropertiesui';
 import TablePropertiesUI from '../../src/tableproperties/tablepropertiesui';
 import TablePropertiesUIView from '../../src/tableproperties/ui/tablepropertiesview';
 import TablePropertiesUIView from '../../src/tableproperties/ui/tablepropertiesview';
+import { defaultColors } from '../../src/ui/utils';
 
 
 describe( 'table properties', () => {
 describe( 'table properties', () => {
 	describe( 'TablePropertiesUI', () => {
 	describe( 'TablePropertiesUI', () => {
@@ -68,6 +69,15 @@ describe( 'table properties', () => {
 			expect( editor.plugins.get( ContextualBalloon ) ).to.be.instanceOf( ContextualBalloon );
 			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()', () => {
 		describe( 'init()', () => {
 			it( 'should set a batch', () => {
 			it( 'should set a batch', () => {
 				expect( tablePropertiesUI._undoStepBatch ).to.be.null;
 				expect( tablePropertiesUI._undoStepBatch ).to.be.null;
@@ -81,6 +91,14 @@ describe( 'table properties', () => {
 				it( 'should be rendered', () => {
 				it( 'should be rendered', () => {
 					expect( tablePropertiesUI.view.isRendered ).to.be.true;
 					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', () => {
 			describe( 'toolbar button', () => {

+ 78 - 12
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 ToolbarView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarview';
 import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
 import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
 import InputTextView from '@ckeditor/ckeditor5-ui/src/inputtext/inputtextview';
 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( 'table properties', () => {
 	describe( 'TablePropertiesView', () => {
 	describe( 'TablePropertiesView', () => {
@@ -25,7 +48,7 @@ describe( 'table properties', () => {
 
 
 		beforeEach( () => {
 		beforeEach( () => {
 			locale = { t: val => val };
 			locale = { t: val => val };
-			view = new TablePropertiesView( locale );
+			view = new TablePropertiesView( locale, VIEW_OPTIONS );
 			view.render();
 			view.render();
 		} );
 		} );
 
 
@@ -34,6 +57,10 @@ describe( 'table properties', () => {
 		} );
 		} );
 
 
 		describe( 'constructor()', () => {
 		describe( 'constructor()', () => {
+			it( 'should set view#options', () => {
+				expect( view.options ).to.deep.equal( VIEW_OPTIONS );
+			} );
+
 			it( 'should set view#locale', () => {
 			it( 'should set view#locale', () => {
 				expect( view.locale ).to.equal( locale );
 				expect( view.locale ).to.equal( locale );
 			} );
 			} );
@@ -198,10 +225,33 @@ describe( 'table properties', () => {
 						} );
 						} );
 
 
 						it( 'should be created', () => {
 						it( 'should be created', () => {
-							expect( labeledInput.view ).to.be.instanceOf( InputTextView );
+							expect( labeledInput.view ).to.be.instanceOf( ColorInputView );
 							expect( labeledInput.label ).to.equal( 'Color' );
 							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', () => {
 						it( 'should reflect #borderColor property', () => {
 							view.borderColor = 'foo';
 							view.borderColor = 'foo';
 							expect( labeledInput.view.value ).to.equal( 'foo' );
 							expect( labeledInput.view.value ).to.equal( 'foo' );
@@ -219,11 +269,11 @@ describe( 'table properties', () => {
 						} );
 						} );
 
 
 						it( 'should update #borderColor on DOM "input" event', () => {
 						it( 'should update #borderColor on DOM "input" event', () => {
-							labeledInput.view.element.value = 'foo';
+							labeledInput.view.value = 'foo';
 							labeledInput.view.fire( 'input' );
 							labeledInput.view.fire( 'input' );
 							expect( view.borderColor ).to.equal( 'foo' );
 							expect( view.borderColor ).to.equal( 'foo' );
 
 
-							labeledInput.view.element.value = 'bar';
+							labeledInput.view.value = 'bar';
 							labeledInput.view.fire( 'input' );
 							labeledInput.view.fire( 'input' );
 							expect( view.borderColor ).to.equal( 'bar' );
 							expect( view.borderColor ).to.equal( 'bar' );
 						} );
 						} );
@@ -246,11 +296,27 @@ describe( 'table properties', () => {
 						} );
 						} );
 
 
 						it( 'should be created', () => {
 						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.label ).to.equal( 'Background' );
 							expect( labeledInput.class ).to.equal( 'ck-table-properties-form__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', () => {
 						it( 'should reflect #backgroundColor property', () => {
 							view.backgroundColor = 'foo';
 							view.backgroundColor = 'foo';
 							expect( labeledInput.view.value ).to.equal( 'foo' );
 							expect( labeledInput.view.value ).to.equal( 'foo' );
@@ -260,11 +326,11 @@ describe( 'table properties', () => {
 						} );
 						} );
 
 
 						it( 'should update #backgroundColor on DOM "input" event', () => {
 						it( 'should update #backgroundColor on DOM "input" event', () => {
-							labeledInput.view.element.value = 'foo';
+							labeledInput.view.value = 'foo';
 							labeledInput.view.fire( 'input' );
 							labeledInput.view.fire( 'input' );
 							expect( view.backgroundColor ).to.equal( 'foo' );
 							expect( view.backgroundColor ).to.equal( 'foo' );
 
 
-							labeledInput.view.element.value = 'bar';
+							labeledInput.view.value = 'bar';
 							labeledInput.view.fire( 'input' );
 							labeledInput.view.fire( 'input' );
 							expect( view.backgroundColor ).to.equal( 'bar' );
 							expect( view.backgroundColor ).to.equal( 'bar' );
 						} );
 						} );
@@ -276,7 +342,7 @@ describe( 'table properties', () => {
 						const row = view.element.childNodes[ 3 ].childNodes[ 0 ];
 						const row = view.element.childNodes[ 3 ].childNodes[ 0 ];
 
 
 						expect( row.classList.contains( 'ck-form__row' ) ).to.be.true;
 						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[ 0 ].textContent ).to.equal( 'Dimensions' );
 						expect( row.childNodes[ 1 ] ).to.equal( view.widthInput.element );
 						expect( row.childNodes[ 1 ] ).to.equal( view.widthInput.element );
 						expect( row.childNodes[ 2 ].textContent ).to.equal( '×' );
 						expect( row.childNodes[ 2 ].textContent ).to.equal( '×' );
@@ -293,7 +359,7 @@ describe( 'table properties', () => {
 						it( 'should be created', () => {
 						it( 'should be created', () => {
 							expect( labeledInput.view ).to.be.instanceOf( InputTextView );
 							expect( labeledInput.view ).to.be.instanceOf( InputTextView );
 							expect( labeledInput.label ).to.equal( 'Width' );
 							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', () => {
 						it( 'should reflect #width property', () => {
@@ -325,7 +391,7 @@ describe( 'table properties', () => {
 						it( 'should be created', () => {
 						it( 'should be created', () => {
 							expect( labeledInput.view ).to.be.instanceOf( InputTextView );
 							expect( labeledInput.view ).to.be.instanceOf( InputTextView );
 							expect( labeledInput.label ).to.equal( 'Height' );
 							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', () => {
 						it( 'should reflect #height property', () => {
@@ -515,7 +581,7 @@ describe( 'table properties', () => {
 
 
 			it( 'should register child views\' #element in #focusTracker', () => {
 			it( 'should register child views\' #element in #focusTracker', () => {
 				const spy = testUtils.sinon.spy( FocusTracker.prototype, 'add' );
 				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();
 				view.render();
 
 
 				sinon.assert.calledWith( spy, view.borderStyleDropdown.element );
 				sinon.assert.calledWith( spy, view.borderStyleDropdown.element );
@@ -532,7 +598,7 @@ describe( 'table properties', () => {
 			} );
 			} );
 
 
 			it( 'starts listening for #keystrokes coming from #element', () => {
 			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' );
 				const spy = sinon.spy( view.keystrokes, 'listenTo' );
 
 
 				view.render();
 				view.render();

+ 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 );
+		} );
+	} );
+} );

+ 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;
+			}
+		}
+	}
+}

+ 0 - 17
packages/ckeditor5-table/theme/tablecellproperties.css

@@ -5,26 +5,9 @@
 
 
 .ck.ck-table-cell-properties-form {
 .ck.ck-table-cell-properties-form {
 	& .ck-form__row {
 	& .ck-form__row {
-		&.ck-table-cell-properties-form__dimensions-row,
 		&.ck-table-cell-properties-form__alignment-row {
 		&.ck-table-cell-properties-form__alignment-row {
 			flex-wrap: wrap;
 			flex-wrap: wrap;
-		}
-
-		&.ck-table-cell-properties-form__dimensions-row {
-			align-items: center;
 
 
-			& .ck-labeled-view {
-				display: flex;
-				flex-direction: column-reverse;
-				align-items: center;
-
-				& .ck.ck-dropdown {
-					flex-grow: 0;
-				}
-			}
-		}
-
-		&.ck-table-cell-properties-form__alignment-row {
 			& .ck.ck-toolbar {
 			& .ck.ck-toolbar {
 				flex-grow: 0;
 				flex-grow: 0;
 			}
 			}

+ 32 - 16
packages/ckeditor5-table/theme/tableform.css

@@ -4,30 +4,46 @@
  */
  */
 
 
 .ck.ck-table-form {
 .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.ck-dropdown {
+			& .ck-table-form__border-style {
 				flex-grow: 0;
 				flex-grow: 0;
 			}
 			}
-		}
 
 
-		& .ck-table-form__border-style {
-			flex-grow: 0;
+			& .ck-table-form__border-width {
+				flex-grow: 0;
+			}
 		}
 		}
 
 
-		& .ck-table-form__border-width {
-			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__dimension-operator {
+				flex-grow: 0;
+			}
+		}
 	}
 	}
 
 
 	& .ck.ck-labeled-view {
 	& .ck.ck-labeled-view {

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

@@ -5,25 +5,6 @@
 
 
 .ck.ck-table-properties-form {
 .ck.ck-table-properties-form {
 	& .ck-form__row {
 	& .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-labeled-view {
-				display: flex;
-				flex-direction: column-reverse;
-				align-items: center;
-
-				& .ck.ck-dropdown {
-					flex-grow: 0;
-				}
-			}
-		}
-
 		&.ck-table-properties-form__alignment-row {
 		&.ck-table-properties-form__alignment-row {
 			flex-wrap: wrap;
 			flex-wrap: wrap;
 			flex-basis: 0;
 			flex-basis: 0;