Browse Source

A PoC of the Table styles UI.

Aleksander Nowodzinski 5 years ago
parent
commit
00285c1ca2

+ 43 - 109
packages/ckeditor5-table/src/tablecellproperties/ui/tablecellpropertiesview.js

@@ -8,8 +8,6 @@
  */
 
 import View from '@ckeditor/ckeditor5-ui/src/view';
-import Model from '@ckeditor/ckeditor5-ui/src/model';
-import Collection from '@ckeditor/ckeditor5-utils/src/collection';
 import ViewCollection from '@ckeditor/ckeditor5-ui/src/viewcollection';
 import submitHandler from '@ckeditor/ckeditor5-ui/src/bindings/submithandler';
 
@@ -23,6 +21,11 @@ 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 {
+	getBorderStyleLabels,
+	fillAlignmentToolbar,
+	getBorderStyleDefinitions
+} from '../../ui/utils';
 
 import checkIcon from '@ckeditor/ckeditor5-core/theme/icons/check.svg';
 import cancelIcon from '@ckeditor/ckeditor5-core/theme/icons/cancel.svg';
@@ -35,8 +38,10 @@ import alignMiddleIcon from '@ckeditor/ckeditor5-core/theme/icons/align-middle.s
 import alignBottomIcon from '@ckeditor/ckeditor5-core/theme/icons/align-bottom.svg';
 
 import '../../../theme/form.css';
+import '../../../theme/tableform.css';
 import '../../../theme/tablecellproperties.css';
 import FormRowView from '../../ui/formrowview';
+import FormHeaderView from '../../ui/formheaderview';
 
 const ALIGNMENT_ICONS = {
 	left: alignLeftIcon,
@@ -265,7 +270,7 @@ export default class TableCellPropertiesView extends View {
 				borderColorInput,
 				borderWidthInput
 			],
-			class: 'ck-table-cell-properties-form__border-row'
+			class: 'ck-table-form__border-row'
 		} ) );
 
 		// Background and padding row.
@@ -302,6 +307,7 @@ export default class TableCellPropertiesView extends View {
 				class: [
 					'ck',
 					'ck-form',
+					'ck-table-form',
 					'ck-table-cell-properties-form'
 				],
 				// https://github.com/ckeditor/ckeditor5-link/issues/90
@@ -356,28 +362,15 @@ export default class TableCellPropertiesView extends View {
 	 * Creates the header of the form with a localized label.
 	 *
 	 * @private
-	 * @returns {module:ui/view~View}
+	 * @returns {module:table/ui/formheaderview~FormHeaderView}
 	 */
 	_createHeaderView() {
 		const locale = this.locale;
 		const t = this.t;
 
-		const headerView = new View( locale );
-
-		headerView.setTemplate( {
-			tag: 'div',
-			attributes: {
-				class: [
-					'ck',
-					'ck-form__header'
-				]
-			},
-			children: [
-				t( 'Cell properties' )
-			]
+		return new FormHeaderView( locale, {
+			label: t( 'Cell properties' )
 		} );
-
-		return headerView;
 	}
 
 	/**
@@ -401,10 +394,11 @@ export default class TableCellPropertiesView extends View {
 
 		// -- Style ---------------------------------------------------
 
+		const styleLabels = getBorderStyleLabels( t );
 		const borderStyleDropdown = new LabeledView( locale, createLabeledDropdown );
 		borderStyleDropdown.set( {
 			label: t( 'Style' ),
-			class: 'ck-table-cell-properties-form__border-style'
+			class: 'ck-table-form__border-style'
 		} );
 
 		borderStyleDropdown.view.buttonView.set( {
@@ -414,14 +408,14 @@ export default class TableCellPropertiesView extends View {
 		} );
 
 		borderStyleDropdown.view.buttonView.bind( 'label' ).to( this, 'borderStyle', value => {
-			return this._borderStyleLabels[ value ];
+			return styleLabels[ value ];
 		} );
 
 		borderStyleDropdown.view.on( 'execute', evt => {
 			this.borderStyle = evt.source._borderStyleValue;
 		} );
 
-		addListToDropdown( borderStyleDropdown.view, this._getBorderStyleDefinitions() );
+		addListToDropdown( borderStyleDropdown.view, getBorderStyleDefinitions( this ) );
 
 		// -- Width ---------------------------------------------------
 
@@ -429,7 +423,7 @@ export default class TableCellPropertiesView extends View {
 
 		borderWidthInput.set( {
 			label: t( 'Width' ),
-			class: 'ck-table-cell-properties-form__border-width',
+			class: 'ck-table-form__border-width',
 		} );
 
 		borderWidthInput.view.bind( 'value' ).to( this, 'borderWidth' );
@@ -535,14 +529,36 @@ export default class TableCellPropertiesView extends View {
 		// -- Horizontal ---------------------------------------------------
 
 		const horizontalAlignmentToolbar = new ToolbarView( locale );
-		horizontalAlignmentToolbar.ariaLabel = t( 'Horizontal text alignment toolbar' );
-		this._fillAlignmentToolbar( horizontalAlignmentToolbar, this._horizontalAlignmentLabels, 'horizontalAlignment' );
+
+		horizontalAlignmentToolbar.set( {
+			isCompact: true,
+			ariaLabel: t( 'Horizontal text alignment toolbar' )
+		} );
+
+		fillAlignmentToolbar( {
+			view: this,
+			icons: ALIGNMENT_ICONS,
+			toolbar: horizontalAlignmentToolbar,
+			labels: this._horizontalAlignmentLabels,
+			propertyName: 'horizontalAlignment'
+		} );
 
 		// -- Vertical -----------------------------------------------------
 
 		const verticalAlignmentToolbar = new ToolbarView( locale );
-		verticalAlignmentToolbar.ariaLabel = t( 'Vertical text alignment toolbar' );
-		this._fillAlignmentToolbar( verticalAlignmentToolbar, this._verticalAlignmentLabels, 'verticalAlignment' );
+
+		verticalAlignmentToolbar.set( {
+			isCompact: true,
+			ariaLabel: t( 'Vertical text alignment toolbar' )
+		} );
+
+		fillAlignmentToolbar( {
+			view: this,
+			icons: ALIGNMENT_ICONS,
+			toolbar: verticalAlignmentToolbar,
+			labels: this._verticalAlignmentLabels,
+			propertyName: 'verticalAlignment'
+		} );
 
 		return {
 			horizontalAlignmentToolbar,
@@ -591,88 +607,6 @@ export default class TableCellPropertiesView extends View {
 	}
 
 	/**
-	 * Provides a set of {@link #borderStyleDropdown} item definitions.
-	 *
-	 * @private
-	 * @returns {Iterable.<module:ui/dropdown/utils~ListDropdownItemDefinition>}
-	 */
-	_getBorderStyleDefinitions() {
-		const itemDefinitions = new Collection();
-
-		for ( const style in this._borderStyleLabels ) {
-			const definition = {
-				type: 'button',
-				model: new Model( {
-					_borderStyleValue: style,
-					label: this._borderStyleLabels[ style ],
-					withText: true,
-				} )
-			};
-
-			definition.model.bind( 'isOn' ).to( this, 'borderStyle', value => {
-				return value === style;
-			} );
-
-			itemDefinitions.add( definition );
-		}
-
-		return itemDefinitions;
-	}
-
-	/**
-	 * Fills an alignment (either horizontal or vertical) with buttons
-	 * that have certain labels and interact with a certain view property
-	 * upon execution.
-	 *
-	 * @private
-	 * @param {module:ui/toolbar/toolbarview~ToolbarView} toolbar
-	 * @param {Array.<String>} labels
-	 * @param {String} propertyName
-	 */
-	_fillAlignmentToolbar( toolbar, labels, propertyName ) {
-		for ( const alignment in labels ) {
-			const button = new ButtonView( this.locale );
-
-			button.set( {
-				label: labels[ alignment ],
-				icon: ALIGNMENT_ICONS[ alignment ],
-			} );
-
-			button.bind( 'isOn' ).to( this, propertyName, value => {
-				return value === alignment;
-			} );
-
-			button.on( 'execute', () => {
-				this[ propertyName ] = alignment;
-			} );
-
-			toolbar.items.add( button );
-		}
-	}
-
-	/**
-	 * Provides localized labels for {@link #borderStyleDropdown} items.
-	 *
-	 * @private
-	 * @type {Object.<String,String>}
-	 */
-	get _borderStyleLabels() {
-		const t = this.t;
-
-		return {
-			none: t( 'None' ),
-			solid: t( 'Solid' ),
-			dotted: t( 'Dotted' ),
-			dashed: t( 'Dashed' ),
-			double: t( 'Double' ),
-			groove: t( 'Groove' ),
-			ridge: t( 'Ridge' ),
-			inset: t( 'Inset' ),
-			outset: t( 'Outset' ),
-		};
-	}
-
-	/**
 	 * Provides localized labels for {@link #horizontalAlignmentToolbar} buttons.
 	 *
 	 * @private

+ 3 - 2
packages/ckeditor5-table/src/tableproperties.js

@@ -10,13 +10,14 @@
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 
 import TablePropertiesEditing from './tableproperties/tablepropertiesediting';
+import TablePropertiesUI from './tableproperties/tablepropertiesui';
 
 /**
  * The table properties feature.
  *
  * This is a "glue" plugin which loads the
  * {@link module:table/tableproperties/tablepropertiesediting~TablePropertiesEditing table editing feature} and
- * table UI feature.
+ * the {@link module:table/tableproperties/tablepropertiesui~TablePropertiesUI table properties UI feature}.
  *
  * @extends module:core/plugin~Plugin
  */
@@ -32,6 +33,6 @@ export default class TableProperties extends Plugin {
 	 * @inheritDoc
 	 */
 	static get requires() {
-		return [ TablePropertiesEditing ];
+		return [ TablePropertiesEditing, TablePropertiesUI ];
 	}
 }

+ 280 - 0
packages/ckeditor5-table/src/tableproperties/tablepropertiesui.js

@@ -0,0 +1,280 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module table/tableproperties/tablepropertiesui
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
+import { getTableWidgetAncestor } from '../utils';
+import clickOutsideHandler from '@ckeditor/ckeditor5-ui/src/bindings/clickoutsidehandler';
+import ContextualBalloon from '@ckeditor/ckeditor5-ui/src/panel/balloon/contextualballoon';
+import TablePropertiesView from './ui/tablepropertiesview';
+import tableProperties from './../../theme/icons/table-properties.svg';
+import {
+	repositionContextualBalloon,
+	getBalloonTablePositionData
+} from '../ui/utils';
+
+const DEFAULT_BORDER_STYLE = 'none';
+const DEFAULT_ALIGNMENT = 'center';
+
+/**
+ * The table properties UI plugin. It introduces the `'tableProperties'` button
+ * that opens a form allowing to specify visual styling of an entire table.
+ *
+ * It uses the
+ * {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon contextual balloon plugin}.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class TablePropertiesUI extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get requires() {
+		return [ ContextualBalloon ];
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'TablePropertiesUI';
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		const editor = this.editor;
+		const t = editor.t;
+
+		/**
+		 * The contextual balloon plugin instance.
+		 *
+		 * @private
+		 * @member {module:ui/panel/balloon/contextualballoon~ContextualBalloon}
+		 */
+		this._balloon = editor.plugins.get( ContextualBalloon );
+
+		/**
+		 * The properties form view displayed inside the balloon.
+		 *
+		 * @member {module:table/tableproperties/ui/tablepropertiesview~TablePropertiesView}
+		 */
+		this.view = this._createPropertiesView();
+
+		/**
+		 * The batch used to undo all changes made by the form (which are live, as the user types)
+		 * when "Cancel" was pressed. Each time the view is shown, a new batch is created.
+		 *
+		 * @protected
+		 * @member {module:engine/model/batch~Batch}
+		 */
+		this._undoStepBatch = null;
+
+		editor.ui.componentFactory.add( 'tableProperties', locale => {
+			const view = new ButtonView( locale );
+
+			view.set( {
+				label: t( 'Table properties' ),
+				icon: tableProperties,
+				tooltip: true
+			} );
+
+			this.listenTo( view, 'execute', () => this._showView() );
+
+			return view;
+		} );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	destroy() {
+		super.destroy();
+
+		// Destroy created UI components as they are not automatically destroyed.
+		// See https://github.com/ckeditor/ckeditor5/issues/1341.
+		this.view.destroy();
+	}
+
+	/**
+	 * Creates the {@link module:table/tableproperties/ui/tablepropertiesview~TablePropertiesView} instance.
+	 *
+	 * @private
+	 * @returns {module:table/tableproperties/ui/tablepropertiesview~TablePropertiesView} The table
+	 * properties form view instance.
+	 */
+	_createPropertiesView() {
+		const editor = this.editor;
+		const viewDocument = editor.editing.view.document;
+		const view = new TablePropertiesView( editor.locale );
+
+		// Render the view so its #element is available for the clickOutsideHandler.
+		view.render();
+
+		this.listenTo( view, 'submit', () => {
+			this._hideView();
+		} );
+
+		this.listenTo( view, 'cancel', () => {
+			editor.execute( 'undo', this._undoStepBatch );
+			this._hideView();
+		} );
+
+		// Close the balloon on Esc key press.
+		view.keystrokes.set( 'Esc', ( data, cancel ) => {
+			this._hideView();
+			cancel();
+		} );
+
+		// Reposition the balloon or hide the form if a table is no longer selected.
+		this.listenTo( editor.ui, 'update', () => {
+			if ( !getTableWidgetAncestor( viewDocument.selection ) ) {
+				this._hideView();
+			} else if ( this._isViewVisible ) {
+				repositionContextualBalloon( editor );
+			}
+		} );
+
+		// Close on click outside of balloon panel element.
+		clickOutsideHandler( {
+			emitter: view,
+			activator: () => this._isViewInBalloon,
+			contextElements: [ this._balloon.view.element ],
+			callback: () => this._hideView()
+		} );
+
+		// Create the "UI -> editor data" binding.
+		// These listeners update the editor data (via table commands) when any observable
+		// property of the view has changed. This makes the view live, which means the changes are
+		// visible in the editing as soon as the user types or changes fields' values.
+		view.on( 'change:borderStyle', this._getPropertyChangeCallback( 'tableBorderStyle' ) );
+		view.on( 'change:borderColor', this._getPropertyChangeCallback( 'tableBorderColor' ) );
+		view.on( 'change:borderWidth', this._getPropertyChangeCallback( 'tableBorderWidth' ) );
+		view.on( 'change:backgroundColor', this._getPropertyChangeCallback( 'tableBackgroundColor' ) );
+		view.on( 'change:width', this._getPropertyChangeCallback( 'tableWidth' ) );
+		view.on( 'change:height', this._getPropertyChangeCallback( 'tableHeight' ) );
+		view.on( 'change:alignment', this._getPropertyChangeCallback( 'tableAlignment' ) );
+
+		return view;
+	}
+
+	/**
+	 * In this method the "editor data -> UI" binding is happening.
+	 *
+	 * When executed, this method obtains selected table property values from various table commands
+	 * and passes them to the {@link #view}.
+	 *
+	 * This way, the UI stays up–to–date with the editor data.
+	 *
+	 * @private
+	 */
+	_fillViewFormFromCommandValues() {
+		const commands = this.editor.commands;
+
+		this.view.set( {
+			borderStyle: commands.get( 'tableBorderStyle' ).value || DEFAULT_BORDER_STYLE,
+			borderColor: commands.get( 'tableBorderColor' ).value || '',
+			borderWidth: commands.get( 'tableBorderWidth' ).value || '',
+			backgroundColor: commands.get( 'tableBackgroundColor' ).value || '',
+			width: commands.get( 'tableWidth' ).value || '',
+			height: commands.get( 'tableHeight' ).value || '',
+			alignment: commands.get( 'tableAlignment' ).value || DEFAULT_ALIGNMENT,
+		} );
+	}
+
+	/**
+	 * Shows the {@link #view} in the {@link #_balloon}.
+	 *
+	 * **Note**: Each time a view is shown, the new {@link #_undoStepBatch} is created that contains
+	 * all changes made to the document when the view is visible, allowing a single undo step
+	 * for all of them.
+	 *
+	 * @protected
+	 */
+	_showView() {
+		const editor = this.editor;
+
+		this._balloon.add( {
+			view: this.view,
+			position: getBalloonTablePositionData( editor )
+		} );
+
+		// Create a new batch. Clicking "Cancel" will undo this batch.
+		this._undoStepBatch = editor.model.createBatch();
+
+		// Update the view with the model values.
+		this._fillViewFormFromCommandValues();
+
+		// Basic a11y.
+		this.view.focus();
+	}
+
+	/**
+	 * Removes the {@link #view} from the {@link #_balloon}.
+	 *
+	 * @protected
+	 */
+	_hideView() {
+		if ( !this._isViewInBalloon ) {
+			return;
+		}
+
+		const editor = this.editor;
+
+		this.stopListening( editor.ui, 'update' );
+
+		// Blur any input element before removing it from DOM to prevent issues in some browsers.
+		// See https://github.com/ckeditor/ckeditor5/issues/1501.
+		this.view.saveButtonView.focus();
+
+		this._balloon.remove( this.view );
+
+		// Make sure the focus is not lost in the process by putting it directly
+		// into the editing view.
+		this.editor.editing.view.focus();
+	}
+
+	/**
+	 * Returns `true` when the {@link #view} is the visible in the {@link #_balloon}.
+	 *
+	 * @private
+	 * @type {Boolean}
+	 */
+	get _isViewVisible() {
+		return this._balloon.visibleView === this.view;
+	}
+
+	/**
+	 * Returns `true` when the {@link #view} is in the {@link #_balloon}.
+	 *
+	 * @private
+	 * @type {Boolean}
+	 */
+	get _isViewInBalloon() {
+		return this._balloon.hasView( this.view );
+	}
+
+	/**
+	 * Creates a callback that when executed upon {@link #view view's} property change
+	 * executes a related editor command with the new property value.
+	 *
+	 * @private
+	 * @param {String} commandName
+	 * @returns {Function}
+	 */
+	_getPropertyChangeCallback( commandName ) {
+		return ( evt, propertyName, newValue ) => {
+			this.editor.execute( commandName, {
+				value: newValue,
+				batch: this._undoStepBatch
+			} );
+		};
+	}
+}

+ 643 - 0
packages/ckeditor5-table/src/tableproperties/ui/tablepropertiesview.js

@@ -0,0 +1,643 @@
+/**
+ * @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/tableproperties/ui/tablepropertiesview
+ */
+
+import View from '@ckeditor/ckeditor5-ui/src/view';
+import ViewCollection from '@ckeditor/ckeditor5-ui/src/viewcollection';
+import submitHandler from '@ckeditor/ckeditor5-ui/src/bindings/submithandler';
+
+import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler';
+import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker';
+import FocusCycler from '@ckeditor/ckeditor5-ui/src/focuscycler';
+
+import LabeledView from '@ckeditor/ckeditor5-ui/src/labeledview/labeledview';
+import { createLabeledInputText, createLabeledDropdown } from '@ckeditor/ckeditor5-ui/src/labeledview/utils';
+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 {
+	getBorderStyleLabels,
+	fillAlignmentToolbar,
+	getBorderStyleDefinitions
+} from '../../ui/utils';
+
+import checkIcon from '@ckeditor/ckeditor5-core/theme/icons/check.svg';
+import cancelIcon from '@ckeditor/ckeditor5-core/theme/icons/cancel.svg';
+import objectLeftIcon from '@ckeditor/ckeditor5-core/theme/icons/object-left.svg';
+import objectRightIcon from '@ckeditor/ckeditor5-core/theme/icons/object-right.svg';
+import objectCenterIcon from '@ckeditor/ckeditor5-core/theme/icons/object-center.svg';
+
+import '../../../theme/form.css';
+import '../../../theme/tableform.css';
+import '../../../theme/tableproperties.css';
+import FormRowView from '../../ui/formrowview';
+import FormHeaderView from '../../ui/formheaderview';
+
+const ALIGNMENT_ICONS = {
+	left: objectLeftIcon,
+	center: objectCenterIcon,
+	right: objectRightIcon
+};
+
+/**
+ * The class representing a table properties form, allowing users to customize
+ * certain style aspects of a table, for instance, border, padding, text alignment, etc..
+ *
+ * @extends module:ui/view~View
+ */
+export default class TablePropertiesView extends View {
+	/**
+	 * @inheritDoc
+	 */
+	constructor( locale ) {
+		super( locale );
+
+		const { borderStyleDropdown, borderWidthInput, borderColorInput, borderRowLabel } = this._createBorderFields();
+		const { widthInput, operatorLabel, heightInput, dimensionsLabel } = this._createDimensionFields();
+		const { alignmentToolbar, alignmentLabel } = this._createAlignmentFields();
+		const { saveButtonView, cancelButtonView } = this._createActionButtons();
+
+		/**
+		 * Tracks information about the DOM focus in the form.
+		 *
+		 * @readonly
+		 * @member {module:utils/focustracker~FocusTracker}
+		 */
+		this.focusTracker = new FocusTracker();
+
+		/**
+		 * An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
+		 *
+		 * @readonly
+		 * @member {module:utils/keystrokehandler~KeystrokeHandler}
+		 */
+		this.keystrokes = new KeystrokeHandler();
+
+		/**
+		 * A collection of child views in the form.
+		 *
+		 * @readonly
+		 * @type {module:ui/viewcollection~ViewCollection}
+		 */
+		this.children = this.createCollection();
+
+		this.set( {
+			/**
+			 * The value of the border style.
+			 *
+			 * @observable
+			 * @default 'none'
+			 * @member #borderStyle
+			 */
+			borderStyle: 'none',
+
+			/**
+			 * The value of the border width style.
+			 *
+			 * @observable
+			 * @default ''
+			 * @member #borderWidth
+			 */
+			borderWidth: '',
+
+			/**
+			 * The value of the border color style.
+			 *
+			 * @observable
+			 * @default ''
+			 * @member #borderColor
+			 */
+			borderColor: '',
+
+			/**
+			 * The value of the background color style.
+			 *
+			 * @observable
+			 * @default ''
+			 * @member #backgroundColor
+			 */
+			backgroundColor: '',
+
+			/**
+			 * The value of the table width style.
+			 *
+			 * @observable
+			 * @default ''
+			 * @member #width
+			 */
+			width: '',
+
+			/**
+			 * The value of the table height style.
+			 *
+			 * @observable
+			 * @default ''
+			 * @member #height
+			 */
+			height: '',
+
+			/**
+			 * The value of the table alignment style.
+			 *
+			 * @observable
+			 * @default 'center'
+			 * @member #alignment
+			 */
+			alignment: 'center',
+		} );
+
+		/**
+		 * A dropdown that allows selecting the style of the table border.
+		 *
+		 * @readonly
+		 * @member {module:ui/dropdown/dropdownview~DropdownView}
+		 */
+		this.borderStyleDropdown = borderStyleDropdown;
+
+		/**
+		 * An input that allows specifying the width of the table border.
+		 *
+		 * @readonly
+		 * @member {module:ui/inputtext/inputtextview~InputTextView}
+		 */
+		this.borderWidthInput = borderWidthInput;
+
+		/**
+		 * An input that allows specifying the color of the table border.
+		 *
+		 * @readonly
+		 * @member {module:ui/inputtext/inputtextview~InputTextView}
+		 */
+		this.borderColorInput = borderColorInput;
+
+		/**
+		 * An input that allows specifying the table background color.
+		 *
+		 * @readonly
+		 * @member {module:ui/inputtext/inputtextview~InputTextView}
+		 */
+		this.backgroundInput = this._createBackgroundField();
+
+		/**
+		 * An input that allows specifying the table width.
+		 *
+		 * @readonly
+		 * @member {module:ui/inputtext/inputtextview~InputTextView}
+		 */
+		this.widthInput = widthInput;
+
+		/**
+		 * An input that allows specifying the table height.
+		 *
+		 * @readonly
+		 * @member {module:ui/inputtext/inputtextview~InputTextView}
+		 */
+		this.heightInput = heightInput;
+
+		/**
+		 * A toolbar with buttons that allow changing the alignment of an entire table.
+		 *
+		 * @readonly
+		 * @member {module:ui/toolbar/toolbar~ToolbarView}
+		 */
+		this.alignmentToolbar = alignmentToolbar;
+
+		/**
+		 * The "Save" button view.
+		 *
+		 * @member {module:ui/button/buttonview~ButtonView}
+		 */
+		this.saveButtonView = saveButtonView;
+
+		/**
+		 * The "Cancel" button view.
+		 *
+		 * @member {module:ui/button/buttonview~ButtonView}
+		 */
+		this.cancelButtonView = cancelButtonView;
+
+		/**
+		 * A collection of views that can be focused in the form.
+		 *
+		 * @readonly
+		 * @protected
+		 * @member {module:ui/viewcollection~ViewCollection}
+		 */
+		this._focusables = new ViewCollection();
+
+		/**
+		 * Helps cycling over {@link #_focusables} in the form.
+		 *
+		 * @readonly
+		 * @protected
+		 * @member {module:ui/focuscycler~FocusCycler}
+		 */
+		this._focusCycler = new FocusCycler( {
+			focusables: this._focusables,
+			focusTracker: this.focusTracker,
+			keystrokeHandler: this.keystrokes,
+			actions: {
+				// Navigate form fields backwards using the Shift + Tab keystroke.
+				focusPrevious: 'shift + tab',
+
+				// Navigate form fields forwards using the Tab key.
+				focusNext: 'tab'
+			}
+		} );
+
+		// Form header.
+		this.children.add( this._createHeaderView() );
+
+		// Border row.
+		this.children.add( new FormRowView( locale, {
+			labelView: borderRowLabel,
+			children: [
+				borderRowLabel,
+				borderStyleDropdown,
+				borderColorInput,
+				borderWidthInput
+			],
+			class: 'ck-table-form__border-row'
+		} ) );
+
+		// Background row.
+		this.children.add( new FormRowView( locale, {
+			children: [
+				this.backgroundInput
+			]
+		} ) );
+
+		this.children.add( new FormRowView( locale, {
+			children: [
+				// Dimensions row.
+				new FormRowView( locale, {
+					labelView: dimensionsLabel,
+					children: [
+						dimensionsLabel,
+						widthInput,
+						operatorLabel,
+						heightInput
+					],
+					class: 'ck-table-properties-form__dimensions-row'
+				} ),
+				// Alignment row.
+				new FormRowView( locale, {
+					labelView: alignmentLabel,
+					children: [
+						alignmentLabel,
+						alignmentToolbar
+					],
+					class: 'ck-table-properties-form__alignment-row'
+				} )
+			]
+		} ) );
+
+		// Action row.
+		this.children.add( new FormRowView( locale, {
+			children: [
+				this.saveButtonView,
+				this.cancelButtonView,
+			],
+			class: 'ck-table-form__action-row'
+		} ) );
+
+		this.setTemplate( {
+			tag: 'form',
+			attributes: {
+				class: [
+					'ck',
+					'ck-form',
+					'ck-table-form',
+					'ck-table-properties-form'
+				],
+				// https://github.com/ckeditor/ckeditor5-link/issues/90
+				tabindex: '-1'
+			},
+			children: this.children
+		} );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	render() {
+		super.render();
+
+		// Enable the "submit" event for this view. It can be triggered by the #saveButtonView
+		// which is of the "submit" DOM "type".
+		submitHandler( {
+			view: this
+		} );
+
+		[
+			this.borderStyleDropdown,
+			this.borderWidthInput,
+			this.borderColorInput,
+			this.backgroundInput,
+			this.widthInput,
+			this.heightInput,
+			this.alignmentToolbar,
+			this.saveButtonView,
+			this.cancelButtonView,
+		].forEach( view => {
+			// Register the view as focusable.
+			this._focusables.add( view );
+
+			// Register the view in the focus tracker.
+			this.focusTracker.add( view.element );
+		} );
+
+		// Mainly for closing using "Esc" and navigation using "Tab".
+		this.keystrokes.listenTo( this.element );
+	}
+
+	/**
+	 * Focuses the fist focusable field in the form.
+	 */
+	focus() {
+		this._focusCycler.focusFirst();
+	}
+
+	/**
+	 * Creates the header of the form with a localized label.
+	 *
+	 * @private
+	 * @returns {module:table/ui/formheaderview~FormHeaderView}
+	 */
+	_createHeaderView() {
+		const locale = this.locale;
+		const t = this.t;
+
+		return new FormHeaderView( locale, {
+			label: t( 'Table properties' )
+		} );
+	}
+
+	/**
+	 * Creates the following form fields:
+	 *
+	 * * {@link #borderStyleDropdown},
+	 * * {@link #borderWidthInput},
+	 * * {@link #borderColorInput}.
+	 *
+	 * @private
+	 * @returns {Object.<String,module:ui/view~View>}
+	 */
+	_createBorderFields() {
+		const locale = this.locale;
+		const t = this.t;
+
+		// -- Group label ---------------------------------------------
+
+		const borderRowLabel = new LabelView( locale );
+		borderRowLabel.text = t( 'Border' );
+
+		// -- Style ---------------------------------------------------
+
+		const styleLabels = getBorderStyleLabels( this.t );
+		const borderStyleDropdown = new LabeledView( locale, createLabeledDropdown );
+		borderStyleDropdown.set( {
+			label: t( 'Style' ),
+			class: 'ck-table-form__border-style'
+		} );
+
+		borderStyleDropdown.view.buttonView.set( {
+			isOn: false,
+			withText: true,
+			tooltip: t( 'Style' )
+		} );
+
+		borderStyleDropdown.view.buttonView.bind( 'label' ).to( this, 'borderStyle', value => {
+			return styleLabels[ value ];
+		} );
+
+		borderStyleDropdown.view.on( 'execute', evt => {
+			this.borderStyle = evt.source._borderStyleValue;
+		} );
+
+		addListToDropdown( borderStyleDropdown.view, getBorderStyleDefinitions( this ) );
+
+		// -- Width ---------------------------------------------------
+
+		const borderWidthInput = new LabeledView( locale, createLabeledInputText );
+
+		borderWidthInput.set( {
+			label: t( 'Width' ),
+			class: 'ck-table-form__border-width',
+		} );
+
+		borderWidthInput.view.bind( 'value' ).to( this, 'borderWidth' );
+		borderWidthInput.bind( 'isEnabled' ).to( this, 'borderStyle', value => {
+			return value !== 'none';
+		} );
+		borderWidthInput.view.on( 'input', () => {
+			this.borderWidth = borderWidthInput.view.element.value;
+		} );
+
+		// -- Color ---------------------------------------------------
+
+		const borderColorInput = new LabeledView( locale, createLabeledInputText );
+		borderColorInput.label = t( 'Color' );
+		borderColorInput.view.bind( 'value' ).to( this, 'borderColor' );
+		borderColorInput.bind( 'isEnabled' ).to( this, 'borderStyle', value => {
+			return value !== 'none';
+		} );
+
+		borderColorInput.view.on( 'input', () => {
+			this.borderColor = borderColorInput.view.element.value;
+		} );
+
+		return {
+			borderRowLabel,
+			borderStyleDropdown,
+			borderColorInput,
+			borderWidthInput,
+		};
+	}
+
+	/**
+	 * Creates the following form fields:
+	 *
+	 * * {@link #backgroundInput}.
+	 *
+	 * @private
+	 * @returns {module:ui/labeledview/labeledview~LabeledView}
+	 */
+	_createBackgroundField() {
+		const locale = this.locale;
+		const t = this.t;
+
+		const backgroundInput = new LabeledView( locale, createLabeledInputText );
+
+		backgroundInput.set( {
+			label: t( 'Background' ),
+			class: 'ck-table-properties-form__background',
+		} );
+
+		backgroundInput.view.bind( 'value' ).to( this, 'backgroundColor' );
+		backgroundInput.view.on( 'input', () => {
+			this.backgroundColor = backgroundInput.view.element.value;
+		} );
+
+		return backgroundInput;
+	}
+
+	/**
+	 * Creates the following form fields:
+	 *
+	 * * {@link #widthInput}.
+	 * * {@link #heightInput}.
+	 *
+	 * @private
+	 * @returns {module:ui/labeledview/labeledview~LabeledView}
+	 */
+	_createDimensionFields() {
+		const locale = this.locale;
+		const t = this.t;
+
+		// -- Label ---------------------------------------------------
+
+		const dimensionsLabel = new LabelView( locale );
+		dimensionsLabel.text = t( 'Dimensions' );
+
+		// -- Width ---------------------------------------------------
+
+		const widthInput = new LabeledView( locale, createLabeledInputText );
+
+		widthInput.set( {
+			label: t( 'Width' ),
+			class: 'ck-table-properties-form__width',
+		} );
+
+		widthInput.view.bind( 'value' ).to( this, 'width' );
+		widthInput.view.on( 'input', () => {
+			this.width = widthInput.view.element.value;
+		} );
+
+		// -- Operator ---------------------------------------------------
+
+		const operatorLabel = new View( locale );
+		operatorLabel.setTemplate( { text: ' × ' } );
+
+		// -- Height ---------------------------------------------------
+
+		const heightInput = new LabeledView( locale, createLabeledInputText );
+
+		heightInput.set( {
+			label: t( 'Height' ),
+			class: 'ck-table-properties-form__height',
+		} );
+
+		heightInput.view.bind( 'value' ).to( this, 'height' );
+		heightInput.view.on( 'input', () => {
+			this.height = heightInput.view.element.value;
+		} );
+
+		return {
+			dimensionsLabel,
+			widthInput,
+			operatorLabel,
+			heightInput
+		};
+	}
+
+	/**
+	 * Creates the following form fields:
+	 *
+	 * * {@link #alignmentToolbar},
+	 *
+	 * @private
+	 * @returns {Object.<String,module:ui/view~View>}
+	 */
+	_createAlignmentFields() {
+		const locale = this.locale;
+		const t = this.t;
+
+		// -- Label ---------------------------------------------------
+
+		const alignmentLabel = new LabelView( locale );
+		alignmentLabel.text = t( 'Alignment' );
+
+		// -- Toolbar ---------------------------------------------------
+
+		const alignmentToolbar = new ToolbarView( locale );
+		alignmentToolbar.set( {
+			isCompact: true,
+			ariaLabel: t( 'Table alignment toolbar' )
+		} );
+
+		fillAlignmentToolbar( {
+			view: this,
+			icons: ALIGNMENT_ICONS,
+			toolbar: alignmentToolbar,
+			labels: this._alignmentLabels,
+			propertyName: 'alignment'
+		} );
+
+		return {
+			alignmentLabel,
+			alignmentToolbar
+		};
+	}
+
+	/**
+	 * Creates the following form controls:
+	 *
+	 * * {@link #saveButtonView},
+	 * * {@link #cancelButtonView}.
+	 *
+	 * @private
+	 * @returns {Object.<String,module:ui/view~View>}
+	 */
+	_createActionButtons() {
+		const locale = this.locale;
+		const t = this.t;
+
+		const saveButtonView = new ButtonView( locale );
+		const cancelButtonView = new ButtonView( locale );
+
+		saveButtonView.set( {
+			label: t( 'Save' ),
+			icon: checkIcon,
+			class: 'ck-button-save',
+			type: 'submit',
+			withText: true,
+		} );
+
+		cancelButtonView.set( {
+			label: t( 'Cancel' ),
+			icon: cancelIcon,
+			class: 'ck-button-cancel',
+			type: 'cancel',
+			withText: true,
+		} );
+
+		cancelButtonView.delegate( 'execute' ).to( this, 'cancel' );
+
+		return {
+			saveButtonView, cancelButtonView
+		};
+	}
+
+	/**
+	 * Provides localized labels for {@link #alignmentToolbar} buttons.
+	 *
+	 * @private
+	 * @type {Object.<String,String>}
+	 */
+	get _alignmentLabels() {
+		const t = this.t;
+
+		return {
+			left: t( 'Align table to the left' ),
+			center: t( 'Center the table' ),
+			right: t( 'Align table to the right' )
+		};
+	}
+}

+ 79 - 0
packages/ckeditor5-table/src/ui/formheaderview.js

@@ -0,0 +1,79 @@
+/**
+ * @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
+ */
+
+import View from '@ckeditor/ckeditor5-ui/src/view';
+
+import '../../theme/formheader.css';
+
+/**
+ * The class representing a header in a complex form.
+ *
+ * **Note**: For now this class is private. When more use cases arrive (beyond ckeditor5-table),
+ * it will become a component in ckeditor5-ui.
+ *
+ * @private
+ * @extends module:ui/view~View
+ */
+export default class FormHeaderView extends View {
+	/**
+	 * Creates an instance of the form header view class.
+	 *
+	 * @param {module:utils/locale~Locale} locale The locale instance.
+	 * @param {Object} options
+	 * @param {String} [options.class] When passed, the class will be set on the header element.
+	 * @param {String} [options.label] When passed, the label will be used for the header.
+	 */
+	constructor( locale, options ) {
+		super( locale );
+
+		const bind = this.bindTemplate;
+
+		/**
+		 * A collection of header items.
+		 *
+		 * @readonly
+		 * @member {module:ui/viewcollection~ViewCollection}
+		 */
+		this.children = this.createCollection();
+
+		/**
+		 * An additional CSS class added to the {@link #element}.
+		 *
+		 * @observable
+		 * @member {String} #class
+		 */
+		this.set( 'class', options.class || null );
+
+		/**
+		 * Label of the header.
+		 *
+		 * @readonly
+		 * @member {String} #label
+		 */
+		this.set( 'label', options.label || '' );
+
+		this.setTemplate( {
+			tag: 'div',
+			attributes: {
+				class: [
+					'ck',
+					'ck-form__header',
+					bind.to( 'class' )
+				]
+			},
+			children: [
+				{ text: bind.to( 'label' ) },
+				{
+					tag: 'div',
+					children: this.children
+				}
+			]
+		} );
+	}
+}

+ 2 - 0
packages/ckeditor5-table/src/ui/formrowview.js

@@ -9,6 +9,8 @@
 
 import View from '@ckeditor/ckeditor5-ui/src/view';
 
+import '../../theme/formrow.css';
+
 /**
  * The class representing a single row in the complex form,
  * used by {@link module:table/tablecellproperties/ui/tablecellpropertiesview~TableCellPropertiesView}.

+ 117 - 9
packages/ckeditor5-table/src/ui/utils.js

@@ -8,9 +8,22 @@
  */
 
 import BalloonPanelView from '@ckeditor/ckeditor5-ui/src/panel/balloon/balloonpanelview';
+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 { getTableWidgetAncestor } from '../utils';
 import { findAncestor } from '../commands/utils';
 
+const DEFAULT_BALLOON_POSITIONS = BalloonPanelView.defaultPositions;
+const BALLOON_POSITIONS = [
+	DEFAULT_BALLOON_POSITIONS.northArrowSouth,
+	DEFAULT_BALLOON_POSITIONS.northArrowSouthWest,
+	DEFAULT_BALLOON_POSITIONS.northArrowSouthEast,
+	DEFAULT_BALLOON_POSITIONS.southArrowNorth,
+	DEFAULT_BALLOON_POSITIONS.southArrowNorthWest,
+	DEFAULT_BALLOON_POSITIONS.southArrowNorthEast
+];
+
 /**
  * A helper utility that positions the
  * {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon contextual balloon} instance
@@ -31,6 +44,25 @@ export function repositionContextualBalloon( editor ) {
 /**
  * Returns the positioning options that control the geometry of the
  * {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon contextual balloon} with respect
+ * to the selected table in the editor content.
+ *
+ * @param {module:core/editor/editor~Editor} editor The editor instance.
+ * @returns {module:utils/dom/position~Options}
+ */
+export function getBalloonTablePositionData( editor ) {
+	const firstPosition = editor.model.document.selection.getFirstPosition();
+	const modelTable = findAncestor( 'table', firstPosition );
+	const viewTable = editor.editing.mapper.toViewElement( modelTable );
+
+	return {
+		target: editor.editing.view.domConverter.viewToDom( viewTable ),
+		positions: BALLOON_POSITIONS
+	};
+}
+
+/**
+ * Returns the positioning options that control the geometry of the
+ * {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon contextual balloon} with respect
  * to the selected table cell in the editor content.
  *
  * @param {module:core/editor/editor~Editor} editor The editor instance.
@@ -40,17 +72,93 @@ export function getBalloonCellPositionData( editor ) {
 	const firstPosition = editor.model.document.selection.getFirstPosition();
 	const modelTableCell = findAncestor( 'tableCell', firstPosition );
 	const viewTableCell = editor.editing.mapper.toViewElement( modelTableCell );
-	const defaultPositions = BalloonPanelView.defaultPositions;
 
 	return {
 		target: editor.editing.view.domConverter.viewToDom( viewTableCell ),
-		positions: [
-			defaultPositions.northArrowSouth,
-			defaultPositions.northArrowSouthWest,
-			defaultPositions.northArrowSouthEast,
-			defaultPositions.southArrowNorth,
-			defaultPositions.southArrowNorthWest,
-			defaultPositions.southArrowNorthEast
-		]
+		positions: BALLOON_POSITIONS
+	};
+}
+
+/**
+ * TODO
+ *
+ * @param {module:utils/locale~Locale#t} t
+ * @returns {Object.<String,String>}
+ */
+export function getBorderStyleLabels( t ) {
+	return {
+		none: t( 'None' ),
+		solid: t( 'Solid' ),
+		dotted: t( 'Dotted' ),
+		dashed: t( 'Dashed' ),
+		double: t( 'Double' ),
+		groove: t( 'Groove' ),
+		ridge: t( 'Ridge' ),
+		inset: t( 'Inset' ),
+		outset: t( 'Outset' ),
 	};
 }
+
+/**
+ * Generates border style item definitions for a list dropdown.
+ *
+ * @param {module:TODO}
+ * @returns {Iterable.<module:ui/dropdown/utils~ListDropdownItemDefinition>}
+ */
+export function getBorderStyleDefinitions( view ) {
+	const itemDefinitions = new Collection();
+	const styleLabels = getBorderStyleLabels( view.t );
+
+	for ( const style in styleLabels ) {
+		const definition = {
+			type: 'button',
+			model: new Model( {
+				_borderStyleValue: style,
+				label: styleLabels[ style ],
+				withText: true,
+			} )
+		};
+
+		definition.model.bind( 'isOn' ).to( view, 'borderStyle', value => {
+			return value === style;
+		} );
+
+		itemDefinitions.add( definition );
+	}
+
+	return itemDefinitions;
+}
+
+/**
+ * Fills an alignment toolbar with buttons that have certain labels and interact with a certain view
+ * property upon execution.
+ *
+ * TODO
+ *
+ * @param {Object} options
+ * @param {TODO} options.view
+ * @param {Array.<String>} options.icons
+ * @param {module:ui/toolbar/toolbarview~ToolbarView} options.toolbar
+ * @param {Array.<String>} labels
+ * @param {String} propertyName
+ */
+export function fillAlignmentToolbar( { view, icons, toolbar, labels, propertyName } ) {
+	for ( const alignment in labels ) {
+		const button = new ButtonView( view.locale );
+
+		button.set( {
+			label: labels[ alignment ],
+			icon: icons[ alignment ],
+		} );
+
+		button.bind( 'isOn' ).to( view, propertyName, value => {
+			return value === alignment;
+		} );
+
+		button.on( 'execute', () => {
+			view[ propertyName ] = alignment;
+		} );
+
+		toolbar.items.add( button );
+	}
+}

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

@@ -26,7 +26,7 @@ ClassicEditor
 			'heading', '|', 'insertTable', '|', 'bold', 'italic', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo'
 		],
 		table: {
-			contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells', 'tableCellProperties' ],
+			contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells', 'tableProperties', 'tableCellProperties' ],
 			tableToolbar: [ 'bold', 'italic' ]
 		}
 	} )

+ 0 - 39
packages/ckeditor5-table/theme/form.css

@@ -11,14 +11,6 @@
 		outline: none;
 	}
 
-	& .ck.ck-form__header {
-		font-weight: bold;
-		padding: 0 var(--ck-spacing-large);
-		height: 38px;
-		line-height: 38px;
-		border-bottom: 1px solid var(--ck-color-base-border);
-	}
-
 	& .ck.ck-input-text {
 		min-width: 100%;
 		width: 0;
@@ -37,35 +29,4 @@
 			}
 		}
 	}
-
-	& .ck-form__row {
-		display: flex;
-		flex-direction: row;
-		flex-wrap: nowrap;
-		justify-content: space-between;
-
-		padding: var(--ck-spacing-standard) var(--ck-spacing-large) 0;
-
-		/* Ignore labels that work as fieldset legends */
-		& > *:not(.ck-label) {
-			flex-grow: 1;
-
-			& + * {
-				padding-left: var(--ck-spacing-large);
-			}
-		}
-
-		&.ck-table-form__action-row {
-			margin-top: var(--ck-spacing-large);
-
-			& .ck-button-save,
-			& .ck-button-cancel {
-				justify-content: center;
-			}
-
-			& .ck-button .ck-button__label {
-				color: var(--ck-color-text);
-			}
-		}
-	}
 }

+ 12 - 0
packages/ckeditor5-table/theme/formheader.css

@@ -0,0 +1,12 @@
+/*
+ * 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-form__header {
+	font-weight: bold;
+	padding: 0 var(--ck-spacing-large);
+	height: 38px;
+	line-height: 38px;
+	border-bottom: 1px solid var(--ck-color-base-border);
+}

+ 40 - 0
packages/ckeditor5-table/theme/formrow.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-form__row {
+	display: flex;
+	flex-direction: row;
+	flex-wrap: nowrap;
+	justify-content: space-between;
+
+	padding: var(--ck-spacing-standard) var(--ck-spacing-large) 0;
+
+	/* Ignore labels that work as fieldset legends */
+	& > *:not(.ck-label) {
+		flex-grow: 1;
+
+		& + * {
+			margin-left: var(--ck-spacing-large);
+		}
+	}
+
+	& > .ck-label {
+		width: 100%;
+		min-width: 100%;
+	}
+
+	&.ck-table-form__action-row {
+		margin-top: var(--ck-spacing-large);
+
+		& .ck-button-save,
+		& .ck-button-cancel {
+			justify-content: center;
+		}
+
+		& .ck-button .ck-button__label {
+			color: var(--ck-color-text);
+		}
+	}
+}

+ 1 - 60
packages/ckeditor5-table/theme/tablecellproperties.css

@@ -7,41 +7,6 @@
 	width: 320px;
 
 	& .ck-form__row {
-		&.ck-table-cell-properties-form__border-row {
-			flex-wrap: wrap;
-
-			& > .ck.ck-label {
-				width: 100%;
-				min-width: 100%;
-			}
-
-			& .ck-labeled-view {
-				display: flex;
-				flex-direction: column-reverse;
-				align-items: center;
-
-				& > .ck-label {
-					font-size: 10px;
-				}
-
-				& .ck.ck-dropdown {
-					flex-grow: 0;
-				}
-			}
-
-			& .ck-table-cell-properties-form__border-style {
-				width: 80px;
-				min-width: 80px;
-				flex-grow: 0;
-			}
-
-			& .ck-table-cell-properties-form__border-width {
-				width: 80px;
-				min-width: 80px;
-				flex-grow: 0;
-			}
-		}
-
 		& .ck-table-cell-properties-form__background,
 		& .ck-table-cell-properties-form__padding {
 			width: 50%;
@@ -50,33 +15,9 @@
 		&.ck-table-cell-properties-form__alignment-row {
 			flex-wrap: wrap;
 
-			& > .ck.ck-label {
-				width: 100%;
-				min-width: 100%;
-			}
-
 			& .ck.ck-toolbar {
-				padding: 0;
-				background: 0;
 				flex-grow: 0;
-
-				& .ck-toolbar__items > * {
-					margin: 0;
-
-					&:first-child {
-						border-top-right-radius: 0;
-						border-bottom-right-radius: 0;
-					}
-
-					&:last-child {
-						border-top-left-radius: 0;
-						border-bottom-left-radius: 0;
-					}
-
-					&:not(:first-child):not(:last-child) {
-						border-radius: 0;
-					}
-				}
+				background: none;
 			}
 		}
 	}

+ 36 - 0
packages/ckeditor5-table/theme/tableform.css

@@ -0,0 +1,36 @@
+/*
+ * 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-table-form {
+	& .ck-table-form__border-row {
+		flex-wrap: wrap;
+
+		& .ck-labeled-view {
+			display: flex;
+			flex-direction: column-reverse;
+			align-items: center;
+
+			& > .ck-label {
+				font-size: 10px;
+			}
+
+			& .ck.ck-dropdown {
+				flex-grow: 0;
+			}
+		}
+
+		& .ck-table-form__border-style {
+			width: 80px;
+			min-width: 80px;
+			flex-grow: 0;
+		}
+
+		& .ck-table-form__border-width {
+			width: 80px;
+			min-width: 80px;
+			flex-grow: 0;
+		}
+	}
+}

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

@@ -0,0 +1,50 @@
+/*
+ * 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-table-properties-form {
+	width: 320px;
+
+	& .ck-form__row {
+		&.ck-table-properties-form__dimensions-row,
+		&.ck-table-properties-form__alignment-row {
+			flex-wrap: wrap;
+			padding-left: 0;
+			padding-right: 0;
+		}
+
+		&.ck-table-properties-form__dimensions-row {
+			align-items: center;
+
+			& .ck-labeled-view .ck-label {
+				display: none;
+			}
+
+			& .ck-table-properties-form__width {
+				margin-right: var(--ck-spacing-small);
+			}
+
+			& .ck-table-properties-form__height {
+				margin-left: var(--ck-spacing-small);
+			}
+		}
+
+		&.ck-table-properties-form__alignment-row {
+			flex-wrap: wrap;
+			flex-basis: 0;
+
+			& .ck.ck-toolbar {
+				background: none;
+
+				& .ck-toolbar__items {
+					flex-wrap: nowrap;
+
+					& > * {
+						width: 40px;
+					}
+				}
+			}
+		}
+	}
+}