Kaynağa Gözat

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 yıl önce
ebeveyn
işleme
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.",
 	"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.",
@@ -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.",
 	"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.",
+	"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/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
+ */

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

@@ -19,10 +19,15 @@ import {
 	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;
@@ -54,6 +59,18 @@ export default class TableCellPropertiesUI extends Plugin {
 	/**
 	 * @inheritDoc
 	 */
+	constructor( editor ) {
+		super( editor );
+
+		editor.config.define( 'table.tableCellProperties', {
+			borderColors: defaultColors,
+			backgroundColors: defaultColors
+		} );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
 	init() {
 		const editor = this.editor;
 		const t = editor.t;
@@ -118,7 +135,15 @@ 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.

+ 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 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,9 +62,16 @@ 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 );
 
 		this.set( {
@@ -145,6 +157,13 @@ export default class TableCellPropertiesView extends View {
 			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();
@@ -193,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;
 
@@ -201,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();
 
@@ -329,7 +348,7 @@ export default class TableCellPropertiesView extends View {
 						operatorLabel,
 						heightInput
 					],
-					class: 'ck-table-cell-properties-form__dimensions-row'
+					class: 'ck-table-form__dimensions-row'
 				} ),
 				// Padding row.
 				new FormRowView( locale, {
@@ -431,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;
 
@@ -481,13 +504,18 @@ export default class TableCellPropertiesView extends View {
 
 		// -- 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.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".
@@ -518,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' ),
@@ -528,7 +560,7 @@ 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;
@@ -558,7 +590,7 @@ export default class TableCellPropertiesView extends View {
 
 		widthInput.set( {
 			label: t( 'Width' ),
-			class: 'ck-table-cell-properties-form__width',
+			class: 'ck-table-form__dimensions-row__width',
 		} );
 
 		widthInput.view.bind( 'value' ).to( this, 'width' );
@@ -587,7 +619,7 @@ export default class TableCellPropertiesView extends View {
 
 		heightInput.set( {
 			label: t( 'Height' ),
-			class: 'ck-table-cell-properties-form__height',
+			class: 'ck-table-form__dimensions-row__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 ];
 	}
 }
+
+/**
+ * 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,
 	lengthFieldValidator,
 	lineWidthFieldValidator,
-	repositionContextualBalloon
+	repositionContextualBalloon,
+	defaultColors,
 } from '../ui/utils';
+import {
+	getLocalizedColorOptions,
+	normalizeColorOptions
+} from '@ckeditor/ckeditor5-ui/src/colorgrid/utils';
 import { debounce } from 'lodash-es';
 
 const ERROR_TEXT_TIMEOUT = 500;
@@ -54,6 +59,18 @@ export default class TablePropertiesUI extends Plugin {
 	/**
 	 * @inheritDoc
 	 */
+	constructor( editor ) {
+		super( editor );
+
+		editor.config.define( 'table.tableProperties', {
+			borderColors: defaultColors,
+			backgroundColors: defaultColors
+		} );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
 	init() {
 		const editor = this.editor;
 		const t = editor.t;
@@ -118,7 +135,15 @@ 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.

+ 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 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,9 +54,16 @@ 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 );
 
 		this.set( {
@@ -119,6 +131,14 @@ export default class TablePropertiesView extends View {
 			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();
@@ -167,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;
 
@@ -175,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();
 
@@ -286,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, {
@@ -377,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;
 
@@ -427,13 +451,18 @@ export default class TablePropertiesView extends View {
 
 		// -- 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.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".
@@ -462,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' ),
@@ -474,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;
@@ -504,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' );
@@ -533,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' );

+ 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 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';
@@ -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.
 // I allows full number notation, so omitting 0 is not allowed:
 function isNumberString( value ) {
@@ -262,3 +456,15 @@ function isNumberString( 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
+		}
+	} ) );
+}

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

@@ -36,4 +36,3 @@ ClassicEditor
 	.catch( err => {
 		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 TableCellPropertiesUI from '../../src/tablecellproperties/tablecellpropertiesui';
 import TableCellPropertiesUIView from '../../src/tablecellproperties/ui/tablecellpropertiesview';
+import { defaultColors } from '../../src/ui/utils';
 
 describe( 'table cell properties', () => {
 	describe( 'TableCellPropertiesUI', () => {
@@ -68,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;
@@ -81,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', () => {

+ 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 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 );
 			} );
@@ -198,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' );
@@ -219,11 +265,11 @@ 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' );
 						} );
@@ -246,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' );
@@ -260,11 +318,11 @@ 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' );
 						} );
@@ -276,7 +334,7 @@ describe( 'table cell 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-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[ 1 ] ).to.equal( view.widthInput.element );
 						expect( row.childNodes[ 2 ].textContent ).to.equal( '×' );
@@ -293,7 +351,7 @@ describe( 'table cell 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-cell-properties-form__width' );
+							expect( labeledInput.class ).to.equal( 'ck-table-form__dimensions-row__width' );
 						} );
 
 						it( 'should reflect #width property', () => {
@@ -325,7 +383,7 @@ describe( 'table cell 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-cell-properties-form__height' );
+							expect( labeledInput.class ).to.equal( 'ck-table-form__dimensions-row__height' );
 						} );
 
 						it( 'should reflect #height property', () => {
@@ -587,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 );
@@ -604,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();

+ 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 TablePropertiesUI from '../../src/tableproperties/tablepropertiesui';
 import TablePropertiesUIView from '../../src/tableproperties/ui/tablepropertiesview';
+import { defaultColors } from '../../src/ui/utils';
 
 describe( 'table properties', () => {
 	describe( 'TablePropertiesUI', () => {
@@ -68,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;
@@ -81,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', () => {

+ 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 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 );
 			} );
@@ -198,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' );
@@ -219,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' );
 						} );
@@ -246,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' );
@@ -260,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' );
 						} );
@@ -276,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( '×' );
@@ -293,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', () => {
@@ -325,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', () => {
@@ -515,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 );
@@ -532,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();

+ 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-form__row {
-		&.ck-table-cell-properties-form__dimensions-row,
 		&.ck-table-cell-properties-form__alignment-row {
 			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 {
 				flex-grow: 0;
 			}

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

@@ -4,30 +4,46 @@
  */
 
 .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;
 			}
-		}
 
-		& .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 {

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

@@ -5,25 +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-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;