Sfoglia il codice sorgente

Merge pull request #8267 from ckeditor/i/1098-accessible-input-labels

Feature: Made input labels accessible across the editor UI. Closes #1098. Closes #8242.

MAJOR BREAKING CHANGE (theme-lark): The look and behavior of the `LabeledFieldView` UI component (used for displaying fields across the project) have changed. This may require changes in your integration if it customizes the `.ck-labeled-field-view` selector (or its internals).
Piotrek Koszuliński 5 anni fa
parent
commit
407a8a7b1d
53 ha cambiato i file con 989 aggiunte e 211 eliminazioni
  1. 1 2
      packages/ckeditor5-image/src/imageinsert/utils.js
  2. 4 0
      packages/ckeditor5-image/src/imagetextalternative/imagetextalternativeui.js
  3. 3 1
      packages/ckeditor5-image/src/imagetextalternative/ui/textalternativeformview.js
  4. 5 4
      packages/ckeditor5-image/tests/imageinsert/ui/imageinsertpanelview.js
  5. 1 6
      packages/ckeditor5-image/tests/imageinsert/utils.js
  6. 13 0
      packages/ckeditor5-image/tests/imagetextalternative/imagetextalternativeui.js
  7. 5 1
      packages/ckeditor5-image/tests/imagetextalternative/ui/textalternativeformview.js
  8. 1 1
      packages/ckeditor5-image/theme/imageinsert.css
  9. 5 1
      packages/ckeditor5-link/src/linkui.js
  10. 6 5
      packages/ckeditor5-link/src/ui/linkformview.js
  11. 12 0
      packages/ckeditor5-link/tests/linkui.js
  12. 7 8
      packages/ckeditor5-link/tests/ui/linkformview.js
  13. 11 0
      packages/ckeditor5-link/theme/linkform.css
  14. 15 0
      packages/ckeditor5-media-embed/src/mediaembedui.js
  15. 3 1
      packages/ckeditor5-media-embed/src/ui/mediaformview.js
  16. 10 0
      packages/ckeditor5-media-embed/tests/mediaembedui.js
  17. 7 6
      packages/ckeditor5-media-embed/tests/ui/mediaformview.js
  18. 3 3
      packages/ckeditor5-table/src/tablecellproperties/tablecellpropertiesui.js
  19. 24 7
      packages/ckeditor5-table/src/tablecellproperties/ui/tablecellpropertiesview.js
  20. 3 3
      packages/ckeditor5-table/src/tableproperties/tablepropertiesui.js
  21. 26 10
      packages/ckeditor5-table/src/tableproperties/ui/tablepropertiesview.js
  22. 25 4
      packages/ckeditor5-table/src/ui/colorinputview.js
  23. 3 1
      packages/ckeditor5-table/src/utils/ui/table-properties.js
  24. 14 0
      packages/ckeditor5-table/tests/tablecellproperties/tablecellpropertiesui.js
  25. 14 2
      packages/ckeditor5-table/tests/tablecellproperties/ui/tablecellpropertiesview.js
  26. 14 0
      packages/ckeditor5-table/tests/tableproperties/tablepropertiesui.js
  27. 14 2
      packages/ckeditor5-table/tests/tableproperties/ui/tablepropertiesview.js
  28. 26 2
      packages/ckeditor5-table/tests/ui/colorinputview.js
  29. 96 1
      packages/ckeditor5-table/tests/utils/ui/table-properties.js
  30. 1 5
      packages/ckeditor5-table/theme/colorinput.css
  31. 13 1
      packages/ckeditor5-table/theme/tablecellproperties.css
  32. 3 16
      packages/ckeditor5-table/theme/tableform.css
  33. 5 5
      packages/ckeditor5-theme-lark/tests/manual/theme.js
  34. 3 2
      packages/ckeditor5-theme-lark/theme/ckeditor5-link/linkform.css
  35. 3 3
      packages/ckeditor5-theme-lark/theme/ckeditor5-table/colorinput.css
  36. 5 1
      packages/ckeditor5-theme-lark/theme/ckeditor5-table/tablecellproperties.css
  37. 6 12
      packages/ckeditor5-theme-lark/theme/ckeditor5-table/tableform.css
  38. 4 0
      packages/ckeditor5-theme-lark/theme/ckeditor5-table/tableproperties.css
  39. 4 5
      packages/ckeditor5-theme-lark/theme/ckeditor5-ui/components/inputtext/inputtext.css
  40. 109 13
      packages/ckeditor5-theme-lark/theme/ckeditor5-ui/components/labeledfield/labeledfieldview.css
  41. 3 3
      packages/ckeditor5-theme-lark/theme/ckeditor5-ui/components/responsive-form/responsiveform.css
  42. 1 1
      packages/ckeditor5-theme-lark/theme/ckeditor5-ui/globals/_colors.css
  43. 72 0
      packages/ckeditor5-ui/src/bindings/injectcsstransitiondisabler.js
  44. 63 6
      packages/ckeditor5-ui/src/inputtext/inputtextview.js
  45. 48 3
      packages/ckeditor5-ui/src/labeledfield/labeledfieldview.js
  46. 2 0
      packages/ckeditor5-ui/src/labeledfield/utils.js
  47. 66 0
      packages/ckeditor5-ui/tests/bindings/injectcsstransitiondisabler.js
  48. 55 0
      packages/ckeditor5-ui/tests/inputtext/inputtextview.js
  49. 104 59
      packages/ckeditor5-ui/tests/labeledfield/labeledfieldview.js
  50. 24 0
      packages/ckeditor5-ui/tests/labeledfield/utils.js
  51. 11 5
      packages/ckeditor5-ui/theme/components/labeledfield/labeledfieldview.css
  52. 12 0
      packages/ckeditor5-ui/theme/globals/_transition.css
  53. 1 0
      packages/ckeditor5-ui/theme/globals/globals.css

+ 1 - 2
packages/ckeditor5-image/src/imageinsert/utils.js

@@ -70,8 +70,7 @@ export function createLabeledInputView( locale ) {
 	labeledInputView.set( {
 		label: t( 'Insert image via URL' )
 	} );
-	labeledInputView.fieldView.placeholder = 'https://example.com/src/image.png';
-	labeledInputView.infoText = t( 'Paste the image source URL.' );
+	labeledInputView.fieldView.placeholder = 'https://example.com/image.png';
 
 	return labeledInputView;
 }

+ 4 - 0
packages/ckeditor5-image/src/imagetextalternative/imagetextalternativeui.js

@@ -165,6 +165,8 @@ export default class ImageTextAlternativeUI extends Plugin {
 		const command = editor.commands.get( 'imageTextAlternative' );
 		const labeledInput = this._form.labeledInput;
 
+		this._form.disableCssTransitions();
+
 		if ( !this._isInBalloon ) {
 			this._balloon.add( {
 				view: this._form,
@@ -180,6 +182,8 @@ export default class ImageTextAlternativeUI extends Plugin {
 		labeledInput.fieldView.value = labeledInput.fieldView.element.value = command.value || '';
 
 		this._form.labeledInput.fieldView.select();
+
+		this._form.enableCssTransitions();
 	}
 
 	/**

+ 3 - 1
packages/ckeditor5-image/src/imagetextalternative/ui/textalternativeformview.js

@@ -14,6 +14,7 @@ import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
 
 import LabeledFieldView from '@ckeditor/ckeditor5-ui/src/labeledfield/labeledfieldview';
 import { createLabeledInputText } from '@ckeditor/ckeditor5-ui/src/labeledfield/utils';
+import injectCssTransitionDisabler from '@ckeditor/ckeditor5-ui/src/bindings/injectcsstransitiondisabler';
 
 import submitHandler from '@ckeditor/ckeditor5-ui/src/bindings/submithandler';
 import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler';
@@ -126,6 +127,8 @@ export default class TextAlternativeFormView extends View {
 				this.cancelButtonView
 			]
 		} );
+
+		injectCssTransitionDisabler( this );
 	}
 
 	/**
@@ -191,7 +194,6 @@ export default class TextAlternativeFormView extends View {
 		const labeledInput = new LabeledFieldView( this.locale, createLabeledInputText );
 
 		labeledInput.label = t( 'Text alternative' );
-		labeledInput.fieldView.placeholder = t( 'Text alternative' );
 
 		return labeledInput;
 	}

+ 5 - 4
packages/ckeditor5-image/tests/imageinsert/ui/imageinsertpanelview.js

@@ -161,9 +161,9 @@ describe( 'ImageUploadPanelView', () => {
 		} );
 
 		it( 'should register child views\' #element in #focusTracker with no integrations', () => {
-			const spy = testUtils.sinon.spy( FocusTracker.prototype, 'add' );
-
 			view = new ImageUploadPanelView( { t: () => {} } );
+
+			const spy = testUtils.sinon.spy( view.focusTracker, 'add' );
 			view.render();
 
 			sinon.assert.calledWithExactly( spy.getCall( 0 ), view.insertButtonView.element );
@@ -171,11 +171,12 @@ describe( 'ImageUploadPanelView', () => {
 		} );
 
 		it( 'should register child views\' #element in #focusTracker with "insertImageViaUrl" integration', () => {
-			const spy = testUtils.sinon.spy( FocusTracker.prototype, 'add' );
-
 			view = new ImageUploadPanelView( { t: () => {} }, {
 				'insertImageViaUrl': createLabeledInputView( { t: val => val } )
 			} );
+
+			const spy = testUtils.sinon.spy( view.focusTracker, 'add' );
+
 			view.render();
 
 			sinon.assert.calledWithExactly( spy.getCall( 0 ), view.getIntegration( 'insertImageViaUrl' ).element );

+ 1 - 6
packages/ckeditor5-image/tests/imageinsert/utils.js

@@ -130,12 +130,7 @@ describe( 'Upload utils', () => {
 		describe( 'image URL input view', () => {
 			it( 'should have placeholder', () => {
 				const view = createLabeledInputView( { t: val => val } );
-				expect( view.fieldView.placeholder ).to.equal( 'https://example.com/src/image.png' );
-			} );
-
-			it( 'should have info text', () => {
-				const view = createLabeledInputView( { t: val => val } );
-				expect( view.infoText ).to.match( /^Paste the image source URL/ );
+				expect( view.fieldView.placeholder ).to.equal( 'https://example.com/image.png' );
 			} );
 		} );
 	} );

+ 13 - 0
packages/ckeditor5-image/tests/imagetextalternative/imagetextalternativeui.js

@@ -96,6 +96,19 @@ describe( 'ImageTextAlternativeUI', () => {
 			sinon.assert.calledOnce( spy );
 			expect( form.labeledInput.fieldView.value ).equals( '' );
 		} );
+
+		it( 'should disable CSS transitions before showing the form to avoid unnecessary animations (and then enable them again)', () => {
+			const addSpy = sinon.spy( balloon, 'add' );
+			const disableCssTransitionsSpy = sinon.spy( form, 'disableCssTransitions' );
+			const enableCssTransitionsSpy = sinon.spy( form, 'enableCssTransitions' );
+			const selectSpy = sinon.spy( form.labeledInput.fieldView, 'select' );
+
+			setData( model, '[<image src="" alt="foo bar"></image>]' );
+
+			button.fire( 'execute' );
+
+			sinon.assert.callOrder( disableCssTransitionsSpy, addSpy, selectSpy, enableCssTransitionsSpy );
+		} );
 	} );
 
 	describe( 'balloon panel form', () => {

+ 5 - 1
packages/ckeditor5-image/tests/imagetextalternative/ui/textalternativeformview.js

@@ -67,6 +67,10 @@ describe( 'TextAlternativeFormView', () => {
 
 			sinon.assert.calledOnce( spy );
 		} );
+
+		it( 'should implement the CSS transition disabling feature', () => {
+			expect( view.disableCssTransitions ).to.be.a( 'function' );
+		} );
 	} );
 
 	describe( 'render()', () => {
@@ -90,7 +94,7 @@ describe( 'TextAlternativeFormView', () => {
 			} );
 
 			it( 'should register child views\' #element in #focusTracker', () => {
-				const spy = testUtils.sinon.spy( FocusTracker.prototype, 'add' );
+				const spy = testUtils.sinon.spy( view.focusTracker, 'add' );
 
 				view.render();
 

+ 1 - 1
packages/ckeditor5-image/theme/imageinsert.css

@@ -4,7 +4,7 @@
  */
 
 .ck.ck-image-insert__panel {
-	padding: var(--ck-spacing-standard);
+	padding: var(--ck-spacing-large);
 }
 
 .ck.ck-image-insert__ck-finder-button {

+ 5 - 1
packages/ckeditor5-link/src/linkui.js

@@ -163,7 +163,7 @@ export default class LinkUI extends Plugin {
 		const linkCommand = editor.commands.get( 'link' );
 		const defaultProtocol = editor.config.get( 'link.defaultProtocol' );
 
-		const formView = new LinkFormView( editor.locale, linkCommand, defaultProtocol );
+		const formView = new LinkFormView( editor.locale, linkCommand );
 
 		formView.urlInputView.fieldView.bind( 'value' ).to( linkCommand, 'value' );
 
@@ -314,6 +314,8 @@ export default class LinkUI extends Plugin {
 		const editor = this.editor;
 		const linkCommand = editor.commands.get( 'link' );
 
+		this.formView.disableCssTransitions();
+
 		this._balloon.add( {
 			view: this.formView,
 			position: this._getBalloonPositionData()
@@ -324,6 +326,8 @@ export default class LinkUI extends Plugin {
 			this.formView.urlInputView.fieldView.select();
 		}
 
+		this.formView.enableCssTransitions();
+
 		// Make sure that each time the panel shows up, the URL field remains in sync with the value of
 		// the command. If the user typed in the input, then canceled the balloon (`urlInputView.fieldView#value` stays
 		// unaltered) and re-opened it without changing the value of the link command (e.g. because they

+ 6 - 5
packages/ckeditor5-link/src/ui/linkformview.js

@@ -15,6 +15,7 @@ import SwitchButtonView from '@ckeditor/ckeditor5-ui/src/button/switchbuttonview
 
 import LabeledFieldView from '@ckeditor/ckeditor5-ui/src/labeledfield/labeledfieldview';
 import { createLabeledInputText } from '@ckeditor/ckeditor5-ui/src/labeledfield/utils';
+import injectCssTransitionDisabler from '@ckeditor/ckeditor5-ui/src/bindings/injectcsstransitiondisabler';
 
 import submitHandler from '@ckeditor/ckeditor5-ui/src/bindings/submithandler';
 import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker';
@@ -43,7 +44,7 @@ export default class LinkFormView extends View {
 	 * @param {module:link/linkcommand~LinkCommand} linkCommand Reference to {@link module:link/linkcommand~LinkCommand}.
 	 * @param {String} [protocol] A value of a protocol to be displayed in the input's placeholder.
 	 */
-	constructor( locale, linkCommand, protocol ) {
+	constructor( locale, linkCommand ) {
 		super( locale );
 
 		const t = locale.t;
@@ -69,7 +70,7 @@ export default class LinkFormView extends View {
 		 *
 		 * @member {module:ui/labeledfield/labeledfieldview~LabeledFieldView}
 		 */
-		this.urlInputView = this._createUrlInput( protocol );
+		this.urlInputView = this._createUrlInput();
 
 		/**
 		 * The Save button view.
@@ -152,6 +153,8 @@ export default class LinkFormView extends View {
 
 			children: this.children
 		} );
+
+		injectCssTransitionDisabler( this );
 	}
 
 	/**
@@ -209,15 +212,13 @@ export default class LinkFormView extends View {
 	 * Creates a labeled input view.
 	 *
 	 * @private
-	 * @param {String} [protocol=http://] A value of a protocol to be displayed in the input's placeholder.
 	 * @returns {module:ui/labeledfield/labeledfieldview~LabeledFieldView} Labeled field view instance.
 	 */
-	_createUrlInput( protocol = 'https://' ) {
+	_createUrlInput() {
 		const t = this.locale.t;
 		const labeledInput = new LabeledFieldView( this.locale, createLabeledInputText );
 
 		labeledInput.label = t( 'Link URL' );
-		labeledInput.fieldView.placeholder = protocol + 'example.com';
 
 		return labeledInput;
 	}

+ 12 - 0
packages/ckeditor5-link/tests/linkui.js

@@ -1107,6 +1107,18 @@ describe( 'LinkUI', () => {
 				sinon.assert.calledOnce( selectSpy );
 			} );
 
+			it( 'should disable CSS transitions before showing the form to avoid unnecessary animations' +
+				'(and then enable them again)', () => {
+				const addSpy = sinon.spy( balloon, 'add' );
+				const disableCssTransitionsSpy = sinon.spy( formView, 'disableCssTransitions' );
+				const enableCssTransitionsSpy = sinon.spy( formView, 'enableCssTransitions' );
+				const selectSpy = sinon.spy( formView.urlInputView.fieldView, 'select' );
+
+				actionsView.fire( 'edit' );
+
+				sinon.assert.callOrder( disableCssTransitionsSpy, addSpy, selectSpy, enableCssTransitionsSpy );
+			} );
+
 			it( 'should execute unlink command on actionsView#unlink event', () => {
 				const executeSpy = testUtils.sinon.spy( editor, 'execute' );
 

+ 7 - 8
packages/ckeditor5-link/tests/ui/linkformview.js

@@ -82,12 +82,6 @@ describe( 'LinkFormView', () => {
 			expect( spy.calledOnce ).to.true;
 		} );
 
-		describe( 'url input view', () => {
-			it( 'has placeholder', () => {
-				expect( view.urlInputView.fieldView.placeholder ).to.equal( 'https://example.com' );
-			} );
-		} );
-
 		describe( 'template', () => {
 			it( 'has url input view', () => {
 				expect( view.template.children[ 0 ].get( 0 ) ).to.equal( view.urlInputView );
@@ -98,6 +92,10 @@ describe( 'LinkFormView', () => {
 				expect( view.template.children[ 0 ].get( 2 ) ).to.equal( view.cancelButtonView );
 			} );
 		} );
+
+		it( 'should implement the CSS transition disabling feature', () => {
+			expect( view.disableCssTransitions ).to.be.a( 'function' );
+		} );
 	} );
 
 	describe( 'render()', () => {
@@ -110,9 +108,10 @@ describe( 'LinkFormView', () => {
 		} );
 
 		it( 'should register child views\' #element in #focusTracker', () => {
-			const spy = testUtils.sinon.spy( FocusTracker.prototype, 'add' );
-
 			view = new LinkFormView( { t: () => {} }, { manualDecorators: [] } );
+
+			const spy = testUtils.sinon.spy( view.focusTracker, 'add' );
+
 			view.render();
 
 			sinon.assert.calledWithExactly( spy.getCall( 0 ), view.urlInputView.element );

+ 11 - 0
packages/ckeditor5-link/theme/linkform.css

@@ -31,4 +31,15 @@
  */
 .ck.ck-link-form_layout-vertical {
 	display: block;
+
+	/*
+	 * Whether the form is in the responsive mode or not, if there are decorator buttons
+	 * keep the top margin of action buttons medium.
+	 */
+	& .ck-button {
+		&.ck-button-save,
+		&.ck-button-cancel {
+			margin-top: var(--ck-spacing-medium);
+		}
+	}
 }

+ 15 - 0
packages/ckeditor5-media-embed/src/mediaembedui.js

@@ -53,6 +53,12 @@ export default class MediaEmbedUI extends Plugin {
 		} );
 	}
 
+	/**
+	 * @private
+	 * @param {module:ui/dropdown/dropdownview~DropdownView} dropdown
+	 * @param {module:ui/view~View} form
+	 * @param {module:media-embed/mediaembedcommand~MediaEmbedCommand} command
+	 */
 	_setUpDropdown( dropdown, form, command ) {
 		const editor = this.editor;
 		const t = editor.t;
@@ -71,6 +77,8 @@ export default class MediaEmbedUI extends Plugin {
 		// default action of the drop-down is executed (i.e. the panel showed up). Otherwise, the
 		// invisible form/input cannot be focused/selected.
 		button.on( 'open', () => {
+			form.disableCssTransitions();
+
 			// Make sure that each time the panel shows up, the URL field remains in sync with the value of
 			// the command. If the user typed in the input, then canceled (`urlInputView#fieldView#value` stays
 			// unaltered) and re-opened it without changing the value of the media command (e.g. because they
@@ -79,6 +87,7 @@ export default class MediaEmbedUI extends Plugin {
 			form.url = command.value || '';
 			form.urlInputView.fieldView.select();
 			form.focus();
+			form.enableCssTransitions();
 		}, { priority: 'low' } );
 
 		dropdown.on( 'submit', () => {
@@ -97,6 +106,12 @@ export default class MediaEmbedUI extends Plugin {
 		}
 	}
 
+	/**
+	 * @private
+	 * @param {module:ui/dropdown/dropdownview~DropdownView} dropdown
+	 * @param {module:ui/view~View} form
+	 * @param {module:media-embed/mediaembedcommand~MediaEmbedCommand} command
+	 */
 	_setUpForm( dropdown, form, command ) {
 		form.delegate( 'submit', 'cancel' ).to( dropdown );
 		form.urlInputView.bind( 'value' ).to( command, 'value' );

+ 3 - 1
packages/ckeditor5-media-embed/src/ui/mediaformview.js

@@ -19,6 +19,7 @@ import submitHandler from '@ckeditor/ckeditor5-ui/src/bindings/submithandler';
 import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker';
 import FocusCycler from '@ckeditor/ckeditor5-ui/src/focuscycler';
 import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler';
+import injectCssTransitionDisabler from '@ckeditor/ckeditor5-ui/src/bindings/injectcsstransitiondisabler';
 
 import checkIcon from '@ckeditor/ckeditor5-core/theme/icons/check.svg';
 import cancelIcon from '@ckeditor/ckeditor5-core/theme/icons/cancel.svg';
@@ -147,6 +148,8 @@ export default class MediaFormView extends View {
 			]
 		} );
 
+		injectCssTransitionDisabler( this );
+
 		/**
 		 * The default info text for the {@link #urlInputView}.
 		 *
@@ -282,7 +285,6 @@ export default class MediaFormView extends View {
 
 		labeledInput.label = t( 'Media URL' );
 		labeledInput.infoText = this._urlInputViewInfoDefault;
-		inputField.placeholder = 'https://example.com';
 
 		inputField.on( 'input', () => {
 			// Display the tip text only when there's some value. Otherwise fall back to the default info text.

+ 10 - 0
packages/ckeditor5-media-embed/tests/mediaembedui.js

@@ -127,6 +127,16 @@ describe( 'MediaEmbedUI', () => {
 					button.fire( 'open' );
 					sinon.assert.calledOnce( spy );
 				} );
+
+				it( 'should disable CSS transitions to avoid unnecessary animations (and then enable them again)', () => {
+					const disableCssTransitionsSpy = sinon.spy( form, 'disableCssTransitions' );
+					const enableCssTransitionsSpy = sinon.spy( form, 'enableCssTransitions' );
+					const selectSpy = sinon.spy( form.urlInputView.fieldView, 'select' );
+
+					button.fire( 'open' );
+
+					sinon.assert.callOrder( disableCssTransitionsSpy, selectSpy, enableCssTransitionsSpy );
+				} );
 			} );
 		} );
 

+ 7 - 6
packages/ckeditor5-media-embed/tests/ui/mediaformview.js

@@ -78,11 +78,11 @@ describe( 'MediaFormView', () => {
 			expect( spy.calledOnce ).to.true;
 		} );
 
-		describe( 'url input view', () => {
-			it( 'has placeholder', () => {
-				expect( view.urlInputView.fieldView.placeholder ).to.equal( 'https://example.com' );
-			} );
+		it( 'should implement the CSS transition disabling feature', () => {
+			expect( view.disableCssTransitions ).to.be.a( 'function' );
+		} );
 
+		describe( 'url input view', () => {
 			it( 'has info text', () => {
 				expect( view.urlInputView.infoText ).to.match( /^Paste the media URL/ );
 			} );
@@ -122,9 +122,10 @@ describe( 'MediaFormView', () => {
 		} );
 
 		it( 'should register child views\' #element in #focusTracker', () => {
-			const spy = testUtils.sinon.spy( FocusTracker.prototype, 'add' );
-
 			view = new MediaFormView( [], { t: () => {} } );
+
+			const spy = testUtils.sinon.spy( view.focusTracker, 'add' );
+
 			view.render();
 
 			sinon.assert.calledWithExactly( spy.getCall( 0 ), view.urlInputView.element );

+ 3 - 3
packages/ckeditor5-table/src/tablecellproperties/tablecellpropertiesui.js

@@ -292,6 +292,9 @@ export default class TableCellPropertiesUI extends Plugin {
 	_showView() {
 		const editor = this.editor;
 
+		// Update the view with the model values.
+		this._fillViewFormFromCommandValues();
+
 		this._balloon.add( {
 			view: this.view,
 			position: getBalloonCellPositionData( editor )
@@ -300,9 +303,6 @@ export default class TableCellPropertiesUI extends Plugin {
 		// 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();
 	}

+ 24 - 7
packages/ckeditor5-table/src/tablecellproperties/ui/tablecellpropertiesview.js

@@ -166,6 +166,7 @@ export default class TableCellPropertiesView extends View {
 		this.options = options;
 
 		const { borderStyleDropdown, borderWidthInput, borderColorInput, borderRowLabel } = this._createBorderFields();
+		const { backgroundRowLabel, backgroundInput } = this._createBackgroundFields();
 		const { widthInput, operatorLabel, heightInput, dimensionsLabel } = this._createDimensionFields();
 		const { horizontalAlignmentToolbar, verticalAlignmentToolbar, alignmentLabel } = this._createAlignmentFields();
 
@@ -223,7 +224,7 @@ export default class TableCellPropertiesView extends View {
 		 * @readonly
 		 * @member {module:table/ui/colorinputview~ColorInputView}
 		 */
-		this.backgroundInput = this._createBackgroundField();
+		this.backgroundInput = backgroundInput;
 
 		/**
 		 * An input that allows specifying the table cell padding.
@@ -332,9 +333,12 @@ export default class TableCellPropertiesView extends View {
 
 		// Background.
 		this.children.add( new FormRowView( locale, {
+			labelView: backgroundRowLabel,
 			children: [
-				this.backgroundInput
-			]
+				backgroundRowLabel,
+				backgroundInput
+			],
+			class: 'ck-table-form__background-row'
 		} ) );
 
 		// Dimensions row and padding.
@@ -486,6 +490,8 @@ export default class TableCellPropertiesView extends View {
 			this.borderStyle = evt.source._borderStyleValue;
 		} );
 
+		borderStyleDropdown.bind( 'isEmpty' ).to( this, 'borderStyle', value => !value );
+
 		addListToDropdown( borderStyleDropdown.fieldView, getBorderStyleDefinitions( this ) );
 
 		// -- Width ---------------------------------------------------
@@ -542,11 +548,19 @@ export default class TableCellPropertiesView extends View {
 	 * * {@link #backgroundInput}.
 	 *
 	 * @private
-	 * @returns {module:ui/labeledfield/labeledfieldview~LabeledFieldView}
+	 * @returns {Object.<String,module:ui/view~View>}
 	 */
-	_createBackgroundField() {
+	_createBackgroundFields() {
 		const locale = this.locale;
 		const t = this.t;
+
+		// -- Group label ---------------------------------------------
+
+		const backgroundRowLabel = new LabelView( locale );
+		backgroundRowLabel.text = t( 'Background' );
+
+		// -- Background color input -----------------------------------
+
 		const colorInputCreator = getLabeledColorInputCreator( {
 			colorConfig: this.options.backgroundColors,
 			columns: 5
@@ -555,7 +569,7 @@ export default class TableCellPropertiesView extends View {
 		const backgroundInput = new LabeledFieldView( locale, colorInputCreator );
 
 		backgroundInput.set( {
-			label: t( 'Background' ),
+			label: t( 'Color' ),
 			class: 'ck-table-cell-properties-form__background'
 		} );
 
@@ -564,7 +578,10 @@ export default class TableCellPropertiesView extends View {
 			this.backgroundColor = backgroundInput.fieldView.value;
 		} );
 
-		return backgroundInput;
+		return {
+			backgroundRowLabel,
+			backgroundInput
+		};
 	}
 
 	/**

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

@@ -282,6 +282,9 @@ export default class TablePropertiesUI extends Plugin {
 	_showView() {
 		const editor = this.editor;
 
+		// Update the view with the model values.
+		this._fillViewFormFromCommandValues();
+
 		this._balloon.add( {
 			view: this.view,
 			position: getBalloonTablePositionData( editor )
@@ -290,9 +293,6 @@ export default class TablePropertiesUI extends Plugin {
 		// 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();
 	}

+ 26 - 10
packages/ckeditor5-table/src/tableproperties/ui/tablepropertiesview.js

@@ -141,6 +141,7 @@ export default class TablePropertiesView extends View {
 		this.options = options;
 
 		const { borderStyleDropdown, borderWidthInput, borderColorInput, borderRowLabel } = this._createBorderFields();
+		const { backgroundRowLabel, backgroundInput } = this._createBackgroundFields();
 		const { widthInput, operatorLabel, heightInput, dimensionsLabel } = this._createDimensionFields();
 		const { alignmentToolbar, alignmentLabel } = this._createAlignmentFields();
 
@@ -198,7 +199,7 @@ export default class TablePropertiesView extends View {
 		 * @readonly
 		 * @member {module:table/ui/colorinputview~ColorInputView}
 		 */
-		this.backgroundInput = this._createBackgroundField();
+		this.backgroundInput = backgroundInput;
 
 		/**
 		 * An input that allows specifying the table width.
@@ -218,7 +219,6 @@ export default class TablePropertiesView extends View {
 
 		/**
 		 * A toolbar with buttons that allow changing the alignment of an entire table.
-		 *
 		 * @readonly
 		 * @member {module:ui/toolbar/toolbar~ToolbarView}
 		 */
@@ -291,9 +291,12 @@ export default class TablePropertiesView extends View {
 
 		// Background row.
 		this.children.add( new FormRowView( locale, {
+			labelView: backgroundRowLabel,
 			children: [
-				this.backgroundInput
-			]
+				backgroundRowLabel,
+				backgroundInput
+			],
+			class: 'ck-table-form__background-row'
 		} ) );
 
 		this.children.add( new FormRowView( locale, {
@@ -433,6 +436,8 @@ export default class TablePropertiesView extends View {
 			this.borderStyle = evt.source._borderStyleValue;
 		} );
 
+		borderStyleDropdown.bind( 'isEmpty' ).to( this, 'borderStyle', value => !value );
+
 		addListToDropdown( borderStyleDropdown.fieldView, getBorderStyleDefinitions( this ) );
 
 		// -- Width ---------------------------------------------------
@@ -489,20 +494,28 @@ export default class TablePropertiesView extends View {
 	 * * {@link #backgroundInput}.
 	 *
 	 * @private
-	 * @returns {module:ui/labeledfield/labeledfieldview~LabeledFieldView}
+	 * @returns {Object.<String,module:ui/view~View>}
 	 */
-	_createBackgroundField() {
+	_createBackgroundFields() {
+		const locale = this.locale;
+		const t = this.t;
+
+		// -- Group label ---------------------------------------------
+
+		const backgroundRowLabel = new LabelView( locale );
+		backgroundRowLabel.text = t( 'Background' );
+
+		// -- Background color input -----------------------------------
+
 		const backgroundInputCreator = getLabeledColorInputCreator( {
 			colorConfig: this.options.backgroundColors,
 			columns: 5
 		} );
-		const locale = this.locale;
-		const t = this.t;
 
 		const backgroundInput = new LabeledFieldView( locale, backgroundInputCreator );
 
 		backgroundInput.set( {
-			label: t( 'Background' ),
+			label: t( 'Color' ),
 			class: 'ck-table-properties-form__background'
 		} );
 
@@ -511,7 +524,10 @@ export default class TablePropertiesView extends View {
 			this.backgroundColor = backgroundInput.fieldView.value;
 		} );
 
-		return backgroundInput;
+		return {
+			backgroundRowLabel,
+			backgroundInput
+		};
 	}
 
 	/**

+ 25 - 4
packages/ckeditor5-table/src/ui/colorinputview.js

@@ -73,6 +73,27 @@ export default class ColorInputView extends View {
 		 */
 		this.set( 'hasError', false );
 
+		/**
+		 * An observable flag set to `true` when the input is focused by the user.
+		 * `false` otherwise.
+		 *
+		 * @readonly
+		 * @observable
+		 * @member {Boolean} #isFocused
+		 * @default false
+		 */
+		this.set( 'isFocused', false );
+
+		/**
+		 * An observable flag set to `true` when the input contains no text.
+		 *
+		 * @readonly
+		 * @observable
+		 * @member {Boolean} #isEmpty
+		 * @default true
+		 */
+		this.set( 'isEmpty', true );
+
 		/**
 		 * The `id` of the element describing this field. When the field has
 		 * some error, it helps screen readers read the error text.
@@ -128,8 +149,8 @@ export default class ColorInputView extends View {
 				'aria-describedby': bind.to( 'ariaDescribedById' )
 			},
 			children: [
-				this._inputView,
-				this._dropdownView
+				this._dropdownView,
+				this._inputView
 			]
 		} );
 
@@ -214,8 +235,8 @@ export default class ColorInputView extends View {
 		} );
 
 		inputView.value = this.value;
-		inputView.bind( 'isReadOnly' ).to( this );
-		inputView.bind( 'hasError' ).to( this );
+		inputView.bind( 'isReadOnly', 'hasError' ).to( this );
+		this.bind( 'isFocused', 'isEmpty' ).to( inputView );
 
 		inputView.on( 'input', () => {
 			const inputValue = inputView.element.value;

+ 3 - 1
packages/ckeditor5-table/src/utils/ui/table-properties.js

@@ -362,7 +362,7 @@ export function getLabeledColorInputCreator( options ) {
 		} );
 
 		inputView.bind( 'isReadOnly' ).to( labeledFieldView, 'isEnabled', value => !value );
-		inputView.bind( 'errorText' ).to( labeledFieldView );
+		inputView.bind( 'hasError' ).to( labeledFieldView, 'errorText', value => !!value );
 
 		inputView.on( 'input', () => {
 			// UX: Make the error text disappear and disable the error indicator as the user
@@ -370,6 +370,8 @@ export function getLabeledColorInputCreator( options ) {
 			labeledFieldView.errorText = null;
 		} );
 
+		labeledFieldView.bind( 'isEmpty', 'isFocused' ).to( inputView );
+
 		return inputView;
 	};
 }

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

@@ -566,6 +566,20 @@ describe( 'table cell properties', () => {
 			} );
 
 			describe( 'initial data', () => {
+				it( 'should be set before adding the form to the the balloon to avoid unnecessary input animations', () => {
+					const balloonAddSpy = testUtils.sinon.spy( editor.plugins.get( ContextualBalloon ), 'add' );
+					const borderStyleChangeSpy = testUtils.sinon.spy();
+
+					tableCellPropertiesView.on( 'change:borderStyle', borderStyleChangeSpy );
+
+					editor.commands.get( 'tableCellBorderStyle' ).value = 'a';
+					tableCellPropertiesButton.fire( 'execute' );
+
+					sinon.assert.calledOnce( borderStyleChangeSpy );
+					sinon.assert.calledOnce( balloonAddSpy );
+					sinon.assert.callOrder( borderStyleChangeSpy, balloonAddSpy );
+				} );
+
 				it( 'should be set from the command values', () => {
 					editor.commands.get( 'tableCellBorderStyle' ).value = 'a';
 					editor.commands.get( 'tableCellBorderColor' ).value = 'b';

+ 14 - 2
packages/ckeditor5-table/tests/tablecellproperties/ui/tablecellpropertiesview.js

@@ -149,6 +149,14 @@ describe( 'table cell properties', () => {
 							expect( labeledDropdown.fieldView.buttonView.label ).to.equal( 'Dashed' );
 						} );
 
+						it( 'should bind #isEmpty to #borderStyle property', () => {
+							view.borderStyle = 'dotted';
+							expect( labeledDropdown.isEmpty ).to.be.false;
+
+							view.borderStyle = null;
+							expect( labeledDropdown.isEmpty ).to.be.true;
+						} );
+
 						it( 'should change #borderStyle when executed', () => {
 							labeledDropdown.fieldView.listView.items.first.children.first.fire( 'execute' );
 							expect( view.borderStyle ).to.equal( '' );
@@ -281,7 +289,11 @@ describe( 'table cell properties', () => {
 						const row = view.element.childNodes[ 2 ];
 
 						expect( row.classList.contains( 'ck-form__row' ) ).to.be.true;
-						expect( row.childNodes[ 0 ] ).to.equal( view.backgroundInput.element );
+						expect( row.classList.contains( 'ck-table-form__background-row' ) ).to.be.true;
+
+						expect( row.classList.contains( 'ck-form__row' ) ).to.be.true;
+						expect( row.childNodes[ 0 ].textContent ).to.equal( 'Background' );
+						expect( row.childNodes[ 1 ] ).to.equal( view.backgroundInput.element );
 					} );
 
 					describe( 'background color input', () => {
@@ -293,7 +305,7 @@ describe( 'table cell properties', () => {
 
 						it( 'should be created', () => {
 							expect( labeledInput.fieldView ).to.be.instanceOf( ColorInputView );
-							expect( labeledInput.label ).to.equal( 'Background' );
+							expect( labeledInput.label ).to.equal( 'Color' );
 							expect( labeledInput.class ).to.equal( 'ck-table-cell-properties-form__background' );
 						} );
 

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

@@ -508,6 +508,20 @@ describe( 'table properties', () => {
 			} );
 
 			describe( 'initial data', () => {
+				it( 'should be set before adding the form to the the balloon to avoid unnecessary input animations', () => {
+					const balloonAddSpy = testUtils.sinon.spy( editor.plugins.get( ContextualBalloon ), 'add' );
+					const borderStyleChangeSpy = testUtils.sinon.spy();
+
+					tablePropertiesView.on( 'change:borderStyle', borderStyleChangeSpy );
+
+					editor.commands.get( 'tableBorderStyle' ).value = 'a';
+					tablePropertiesButton.fire( 'execute' );
+
+					sinon.assert.calledOnce( borderStyleChangeSpy );
+					sinon.assert.calledOnce( balloonAddSpy );
+					sinon.assert.callOrder( borderStyleChangeSpy, balloonAddSpy );
+				} );
+
 				it( 'should be set from the command values', () => {
 					editor.commands.get( 'tableBorderStyle' ).value = 'a';
 					editor.commands.get( 'tableBorderColor' ).value = 'b';

+ 14 - 2
packages/ckeditor5-table/tests/tableproperties/ui/tablepropertiesview.js

@@ -149,6 +149,14 @@ describe( 'table properties', () => {
 							expect( labeledDropdown.fieldView.buttonView.label ).to.equal( 'Dashed' );
 						} );
 
+						it( 'should bind #isEmpty to #borderStyle property', () => {
+							view.borderStyle = 'dotted';
+							expect( labeledDropdown.isEmpty ).to.be.false;
+
+							view.borderStyle = null;
+							expect( labeledDropdown.isEmpty ).to.be.true;
+						} );
+
 						it( 'should change #borderStyle when executed', () => {
 							labeledDropdown.fieldView.listView.items.first.children.first.fire( 'execute' );
 							expect( view.borderStyle ).to.equal( '' );
@@ -285,7 +293,11 @@ describe( 'table properties', () => {
 						const row = view.element.childNodes[ 2 ];
 
 						expect( row.classList.contains( 'ck-form__row' ) ).to.be.true;
-						expect( row.childNodes[ 0 ] ).to.equal( view.backgroundInput.element );
+						expect( row.classList.contains( 'ck-table-form__background-row' ) ).to.be.true;
+
+						expect( row.classList.contains( 'ck-form__row' ) ).to.be.true;
+						expect( row.childNodes[ 0 ].textContent ).to.equal( 'Background' );
+						expect( row.childNodes[ 1 ] ).to.equal( view.backgroundInput.element );
 					} );
 
 					describe( 'background color input', () => {
@@ -297,7 +309,7 @@ describe( 'table properties', () => {
 
 						it( 'should be created', () => {
 							expect( labeledInput.fieldView ).to.be.instanceOf( ColorInputView );
-							expect( labeledInput.label ).to.equal( 'Background' );
+							expect( labeledInput.label ).to.equal( 'Color' );
 							expect( labeledInput.class ).to.equal( 'ck-table-properties-form__background' );
 						} );
 

+ 26 - 2
packages/ckeditor5-table/tests/ui/colorinputview.js

@@ -71,6 +71,30 @@ describe( 'ColorInputView', () => {
 			expect( view.hasError ).to.be.false;
 		} );
 
+		it( 'should set #isFocused', () => {
+			expect( view.isFocused ).to.be.false;
+		} );
+
+		it( 'should set #isEmpty', () => {
+			expect( view.isEmpty ).to.be.true;
+		} );
+
+		it( 'should have #isEmpty bound to the text input', () => {
+			inputView.isEmpty = true;
+			expect( view.isEmpty ).to.be.true;
+
+			inputView.isEmpty = false;
+			expect( view.isEmpty ).to.be.false;
+		} );
+
+		it( 'should have #isFocused bound to the text input', () => {
+			inputView.isFocused = true;
+			expect( view.isFocused ).to.be.true;
+
+			inputView.isFocused = false;
+			expect( view.isFocused ).to.be.false;
+		} );
+
 		describe( 'dropdown', () => {
 			it( 'should be created', () => {
 				expect( view._dropdownView ).to.be.instanceOf( DropdownView );
@@ -352,8 +376,8 @@ describe( 'ColorInputView', () => {
 		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 );
+			expect( view.element.firstChild ).to.equal( view._dropdownView.element );
+			expect( view.element.lastChild ).to.equal( inputView.element );
 		} );
 
 		describe( 'options', () => {

+ 96 - 1
packages/ckeditor5-table/tests/utils/ui/table-properties.js

@@ -11,6 +11,8 @@ import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import View from '@ckeditor/ckeditor5-ui/src/view';
 import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
 
+import LabeledFieldView from '@ckeditor/ckeditor5-ui/src/labeledfield/labeledfieldview';
+import ColorInputView from '../../../src/ui/colorinputview';
 import ToolbarView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarview';
 
 import {
@@ -21,7 +23,8 @@ import {
 	lengthFieldValidator,
 	lineWidthFieldValidator,
 	colorFieldValidator,
-	fillToolbar
+	fillToolbar,
+	getLabeledColorInputCreator
 } from '../../../src/utils/ui/table-properties';
 import Collection from '@ckeditor/ckeditor5-utils/src/collection';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
@@ -338,5 +341,97 @@ describe( 'table utils', () => {
 				expect( view.someProperty ).to.equal( '' );
 			} );
 		} );
+
+		describe( 'getLabeledColorInputCreator()', () => {
+			let creator, labeledField;
+
+			const colorConfig = [
+				{
+					color: 'hsl(180, 75%, 60%)',
+					label: 'Turquoise'
+				},
+				{
+					color: 'hsl(210, 75%, 60%)',
+					label: 'Light blue'
+				}
+			];
+
+			beforeEach( () => {
+				creator = getLabeledColorInputCreator( {
+					colorConfig,
+					columns: 3
+				} );
+
+				labeledField = new LabeledFieldView( { t: () => {} }, creator );
+			} );
+
+			afterEach( () => {
+				labeledField.destroy();
+			} );
+
+			it( 'should return a function', () => {
+				expect( creator ).to.be.a( 'function' );
+			} );
+
+			it( 'should pass the options.colorConfig on', () => {
+				expect( labeledField.fieldView.options.colorDefinitions ).to.have.length( 2 );
+			} );
+
+			it( 'should pass the options.columns on', () => {
+				expect( labeledField.fieldView.options.columns ).to.equal( 3 );
+			} );
+
+			it( 'should return a ColorInputView instance', () => {
+				expect( labeledField.fieldView ).to.be.instanceOf( ColorInputView );
+			} );
+
+			it( 'should set ColorInputView#id', () => {
+				expect( labeledField.fieldView.id ).to.match( /^ck-labeled-field-view-.+/ );
+			} );
+
+			it( 'should set ColorInputView#ariaDescribedById', () => {
+				expect( labeledField.fieldView.ariaDescribedById ).to.match( /^ck-labeled-field-view-status-.+/ );
+			} );
+
+			it( 'should bind ColorInputView#isReadOnly to LabeledFieldView#isEnabled', () => {
+				labeledField.isEnabled = true;
+				expect( labeledField.fieldView.isReadOnly ).to.be.false;
+
+				labeledField.isEnabled = false;
+				expect( labeledField.fieldView.isReadOnly ).to.be.true;
+			} );
+
+			it( 'should bind ColorInputView#hasError to LabeledFieldView#errorText', () => {
+				labeledField.errorText = 'foo';
+				expect( labeledField.fieldView.hasError ).to.be.true;
+
+				labeledField.errorText = null;
+				expect( labeledField.fieldView.hasError ).to.be.false;
+			} );
+
+			it( 'should clear labeld field view #errorText upon #input event', () => {
+				labeledField.errorText = 'foo';
+
+				labeledField.fieldView.fire( 'input' );
+
+				expect( labeledField.errorText ).to.be.null;
+			} );
+
+			it( 'should bind LabeledFieldView#isEmpty to the ColorInputView instance', () => {
+				labeledField.fieldView.isEmpty = true;
+				expect( labeledField.isEmpty ).to.be.true;
+
+				labeledField.fieldView.isEmpty = false;
+				expect( labeledField.isEmpty ).to.be.false;
+			} );
+
+			it( 'should bind LabeledFieldView#isFocused to the ColorInputView instance', () => {
+				labeledField.fieldView.isFocused = true;
+				expect( labeledField.isFocused ).to.be.true;
+
+				labeledField.fieldView.isFocused = false;
+				expect( labeledField.isFocused ).to.be.false;
+			} );
+		} );
 	} );
 } );

+ 1 - 5
packages/ckeditor5-table/theme/colorinput.css

@@ -6,15 +6,11 @@
 .ck.ck-input-color {
 	width: 100%;
 	display: flex;
+	flex-direction: row-reverse;
 
 	& > input.ck.ck-input-text {
 		min-width: auto;
 		flex-grow: 1;
-
-		&:active,
-		&:focus {
-			z-index: var(--ck-z-default);
-		}
 	}
 
 	& > div.ck.ck-dropdown {

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

@@ -9,7 +9,19 @@
 			flex-wrap: wrap;
 
 			& .ck.ck-toolbar {
-				flex-grow: 0;
+				&:first-of-type {
+					/* 4 buttons out of 7 (h-alignment + v-alignment) = 0.57 */
+					flex-grow: 0.57;
+				}
+
+				&:last-of-type {
+					/* 3 buttons out of 7 (h-alignment + v-alignment) = 0.43 */
+					flex-grow: 0.43;
+				}
+
+				& .ck-button {
+					flex-grow: 1;
+				}
 			}
 		}
 	}

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

@@ -7,23 +7,10 @@
 	& .ck-form__row {
 		&.ck-table-form__border-row {
 			flex-wrap: wrap;
+		}
 
-			& .ck-labeled-field-view {
-				display: flex;
-				flex-direction: column-reverse;
-
-				& .ck.ck-dropdown {
-					flex-grow: 0;
-				}
-			}
-
-			& .ck-table-form__border-style {
-				flex-grow: 0;
-			}
-
-			& .ck-table-form__border-width {
-				flex-grow: 0;
-			}
+		&.ck-table-form__background-row {
+			flex-wrap: wrap;
 		}
 
 		&.ck-table-form__dimensions-row {

+ 5 - 5
packages/ckeditor5-theme-lark/tests/manual/theme.js

@@ -515,7 +515,7 @@ function renderInput() {
 	ui.inputLabeled.add( input() );
 	ui.inputReadOnly.add( input( {
 		label: 'A read–only input',
-		isReadOnly: true,
+		isEnabled: false,
 		value: 'Read–only input value'
 	} ) );
 }
@@ -634,14 +634,14 @@ function toolbarNewLine() {
 
 function input( {
 	label = 'Labeled input',
-	isReadOnly = false,
+	isEnabled = true,
 	value = 'The value of the input'
 } = {} ) {
-	const labeledInput = new LabeledFieldView( {}, createLabeledInputText );
+	const labeledField = new LabeledFieldView( {}, createLabeledInputText );
 
-	labeledInput.fieldView.set( { isReadOnly, label, value } );
+	labeledField.set( { isEnabled, label, value } );
 
-	return labeledInput;
+	return labeledField;
 }
 
 function setManualTestDirection( direction ) {

+ 3 - 2
packages/ckeditor5-theme-lark/theme/ckeditor5-link/linkform.css

@@ -14,7 +14,7 @@
 	min-width: var(--ck-input-text-width);
 
 	& .ck-labeled-field-view {
-		margin: var(--ck-spacing-standard) var(--ck-spacing-standard) var(--ck-spacing-small);
+		margin: var(--ck-spacing-large) var(--ck-spacing-large) var(--ck-spacing-small);
 
 		& .ck-input-text {
 			min-width: 0;
@@ -45,10 +45,11 @@
 
 	/* Using additional `.ck` class for stronger CSS specificity than `.ck.ck-link-form > :not(:first-child)`. */
 	& .ck.ck-list {
-		margin-left: 0;
+		margin: var(--ck-spacing-standard) var(--ck-spacing-large);
 
 		& .ck-button.ck-switchbutton {
 			border: 0;
+			padding: 0;
 			width: 100%;
 
 			&:hover {

+ 3 - 3
packages/ckeditor5-theme-lark/theme/ckeditor5-table/colorinput.css

@@ -20,19 +20,19 @@
 	}
 
 	& > .ck.ck-dropdown {
-		& > .ck.ck-input-color__button {
+		& > .ck.ck-button.ck-input-color__button {
 			padding: 0;
 
 			@mixin ck-dir ltr {
+				border-left-width: 0;
 				border-top-left-radius: 0;
 				border-bottom-left-radius: 0;
-				margin-left: -1px;
 			}
 
 			@mixin ck-dir rtl {
+				border-right-width: 0;
 				border-top-right-radius: 0;
 				border-bottom-right-radius: 0;
-				margin-right: -1px;
 			}
 
 			&.ck-disabled {

+ 5 - 1
packages/ckeditor5-theme-lark/theme/ckeditor5-table/tablecellproperties.css

@@ -8,13 +8,17 @@
 
 	& .ck-form__row {
 		&.ck-table-cell-properties-form__padding-row {
+			align-self: flex-end;
 			padding: 0;
-			width: 35%;
+			width: 25%;
 		}
 
 		&.ck-table-cell-properties-form__alignment-row {
 			& .ck.ck-toolbar {
 				background: none;
+
+				/* Compensate for missing input label that would push the margin (toolbar has no inputs). */
+				margin-top: var(--ck-spacing-standard);
 			}
 		}
 	}

+ 6 - 12
packages/ckeditor5-theme-lark/theme/ckeditor5-table/tableform.css

@@ -20,32 +20,24 @@
 				}
 			}
 
-			& .ck-table-form__border-style {
+			& .ck-table-form__border-style,
+			& .ck-table-form__border-width {
 				width: 80px;
 				min-width: 80px;
-			}
-
-			& .ck-table-form__border-width {
-				width: 50px;
-				min-width: 50px;
+				max-width: 80px;
 			}
 		}
 
 		&.ck-table-form__dimensions-row {
 			padding: 0;
 
-			& .ck-labeled-field-view > .ck-label {
-				font-size: 10px;
-				text-align: center;
-			}
-
 			& .ck-table-form__dimensions-row__width,
 			& .ck-table-form__dimensions-row__height {
 				margin: 0
 			}
 
 			& .ck-table-form__dimension-operator {
-				align-self: start;
+				align-self: flex-end;
 				display: inline-block;
 				height: var(--ck-ui-component-min-height);
 				line-height: var(--ck-ui-component-min-height);
@@ -55,6 +47,8 @@
 	}
 
 	& .ck.ck-labeled-field-view {
+		padding-top: var(--ck-spacing-standard);
+
 		& .ck.ck-labeled-field-view__status {
 			@mixin ck-rounded-corners;
 

+ 4 - 0
packages/ckeditor5-theme-lark/theme/ckeditor5-table/tableproperties.css

@@ -8,11 +8,15 @@
 
 	& .ck-form__row {
 		&.ck-table-properties-form__alignment-row {
+			align-self: flex-end;
 			padding: 0;
 
 			& .ck.ck-toolbar {
 				background: none;
 
+				/* Compensate for missing input label that would push the margin (toolbar has no inputs). */
+				margin-top: var(--ck-spacing-standard);
+
 				& .ck-toolbar__items > * {
 					width: 40px;
 				}

+ 4 - 5
packages/ckeditor5-theme-lark/theme/ckeditor5-ui/components/inputtext/inputtext.css

@@ -13,7 +13,6 @@
 
 .ck.ck-input-text {
 	@mixin ck-rounded-corners;
-	@mixin ck-box-shadow var(--ck-inner-shadow);
 
 	background: var(--ck-color-input-background);
 	border: 1px solid var(--ck-color-input-border);
@@ -24,11 +23,11 @@
 	min-height: var(--ck-ui-component-min-height);
 
 	/* Apply some smooth transition to the box-shadow and border. */
-	transition: box-shadow .2s ease-in-out, border .2s ease-in-out;
+	transition: box-shadow .1s ease-in-out, border .1s ease-in-out;
 
 	&:focus {
 		@mixin ck-focus-ring;
-		@mixin ck-box-shadow var(--ck-focus-outer-shadow), var(--ck-inner-shadow);
+		@mixin ck-box-shadow var(--ck-focus-outer-shadow);
 	}
 
 	&[readonly] {
@@ -38,7 +37,7 @@
 
 		&:focus {
 			/* The read-only input should have a slightly less visible shadow when focused. */
-			@mixin ck-box-shadow var(--ck-focus-disabled-outer-shadow), var(--ck-inner-shadow);
+			@mixin ck-box-shadow var(--ck-focus-disabled-outer-shadow);
 		}
 	}
 
@@ -47,7 +46,7 @@
 		animation: ck-text-input-shake .3s ease both;
 
 		&:focus {
-			@mixin ck-box-shadow var(--ck-focus-error-outer-shadow), var(--ck-inner-shadow);
+			@mixin ck-box-shadow var(--ck-focus-error-outer-shadow);
 		}
 	}
 }

+ 109 - 13
packages/ckeditor5-theme-lark/theme/ckeditor5-ui/components/labeledfield/labeledfieldview.css

@@ -3,21 +3,117 @@
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
-.ck.ck-labeled-field-view .ck-labeled-field-view__status {
-	font-size: var(--ck-font-size-small);
-	margin-top: var(--ck-spacing-small);
+@import "@ckeditor/ckeditor5-ui/theme/mixins/_dir.css";
+@import "../../../mixins/_rounded.css";
 
-	/* Let the info wrap to the next line to avoid stretching the layout horizontally.
-	The status could be very long. */
-	white-space: normal;
+:root {
+	--ck-labeled-field-view-transition: .1s cubic-bezier(0, 0, 0.24, 0.95);
+	--ck-labeled-field-empty-unfocused-max-width: 100% - 2 * var(--ck-spacing-medium);
 }
 
-.ck.ck-labeled-field-view .ck-labeled-field-view__status_error {
-	color: var(--ck-color-base-error);
-}
+.ck.ck-labeled-field-view {
+	@mixin ck-rounded-corners;
+
+	& > .ck.ck-labeled-field-view__input-wrapper {
+		width: 100%;
+
+		& > .ck.ck-label {
+			top: 0px;
+
+			@mixin ck-dir ltr {
+				left: 0px;
+			}
+
+			@mixin ck-dir rtl {
+				right: 0px;
+			}
+
+			pointer-events: none;
+			transform-origin: 0 0;
+
+			/* By default, display the label scaled down above the field. */
+			transform: translate(var(--ck-spacing-medium), -6px) scale(.75);
+
+			background: var(--ck-color-base-background);
+			padding: 0 calc(.5 * var(--ck-font-size-tiny));
+			line-height: initial;
+			font-weight: normal;
+
+			/* Prevent overflow when the label is longer than the input */
+			text-overflow: ellipsis;
+			overflow: hidden;
+
+			max-width: 100%;
+
+			transition:
+				transform var(--ck-labeled-field-view-transition),
+				padding var(--ck-labeled-field-view-transition),
+				background var(--ck-labeled-field-view-transition);
+		}
+	}
+
+	&.ck-error {
+		& > .ck.ck-labeled-field-view__input-wrapper > .ck.ck-label {
+			color: var(--ck-color-base-error);
+		}
+
+		& .ck-input:not([readonly]) + .ck.ck-label {
+			color: var(--ck-color-base-error);
+		}
+	}
+
+	& .ck-labeled-field-view__status {
+		font-size: var(--ck-font-size-small);
+		margin-top: var(--ck-spacing-small);
+
+		/* Let the info wrap to the next line to avoid stretching the layout horizontally.
+		The status could be very long. */
+		white-space: normal;
+
+		&.ck-labeled-field-view__status_error {
+			color: var(--ck-color-base-error);
+		}
+	}
+
+	/* Disabled fields and fields that have no focus should fade out. */
+	&.ck-disabled > .ck.ck-labeled-field-view__input-wrapper > .ck.ck-label,
+	&.ck-labeled-field-view_empty:not(.ck-labeled-field-view_focused) > .ck.ck-labeled-field-view__input-wrapper > .ck.ck-label {
+		color: var(--ck-color-input-disabled-text);
+	}
+
+	/* Fields that are disabled or not focused and without a placeholder should have full-sized labels. */
+	/* stylelint-disable-next-line no-descending-specificity */
+	&.ck-disabled > .ck.ck-labeled-field-view__input-wrapper > .ck.ck-label,
+	&.ck-labeled-field-view_empty:not(.ck-labeled-field-view_focused):not(.ck-labeled-field-view_placeholder) > .ck.ck-labeled-field-view__input-wrapper > .ck.ck-label {
+		@mixin ck-dir ltr {
+			transform: translate(var(--ck-spacing-medium), calc(0.6 * var(--ck-font-size-base))) scale(1);
+		}
+
+		@mixin ck-dir rtl {
+			transform: translate(calc(-1 * var(--ck-spacing-medium)), calc(0.6 * var(--ck-font-size-base))) scale(1);
+		}
+
+		/* Compensate for the default translate position. */
+		max-width: calc(var(--ck-labeled-field-empty-unfocused-max-width));
+
+		background: transparent;
+		padding: 0;
+	}
+
+	/*------ DropdownView integration ----------------------------------------------------------------------------------- */
+
+	/* Make sure dropdown' background color in any of dropdown's state does not collide with labeled field. */
+	& > .ck.ck-labeled-field-view__input-wrapper > .ck-dropdown > .ck.ck-button {
+		background: transparent;
+	}
+
+	/* When the dropdown is "empty", the labeled field label replaces its label. */
+	&.ck-labeled-field-view_empty > .ck.ck-labeled-field-view__input-wrapper > .ck-dropdown > .ck-button > .ck-button__label {
+		opacity: 0;
+	}
 
-.ck.ck-labeled-field-view > .ck.ck-label {
-	width: 100%;
-	text-overflow: ellipsis;
-	overflow: hidden;
+	/* Make sure the label of the empty, unfocused input does not cover the dropdown arrow. */
+	&.ck-labeled-field-view_empty:not(.ck-labeled-field-view_focused):not(.ck-labeled-field-view_placeholder) > .ck.ck-labeled-field-view__input-wrapper > .ck-dropdown + .ck-label {
+		max-width: calc(var(--ck-labeled-field-empty-unfocused-max-width) - var(--ck-dropdown-arrow-size) - var(--ck-spacing-standard));
+	}
 }

+ 3 - 3
packages/ckeditor5-theme-lark/theme/ckeditor5-ui/components/responsive-form/responsiveform.css

@@ -11,7 +11,7 @@
 }
 
 .ck.ck-responsive-form {
-	padding: var(--ck-spacing-standard);
+	padding: var(--ck-spacing-large);
 
 	&:focus {
 		/* See: https://github.com/ckeditor/ckeditor5/issues/4773 */
@@ -35,7 +35,7 @@
 		width: calc(.8 * var(--ck-input-text-width));
 
 		& .ck-labeled-field-view {
-			margin: var(--ck-spacing-standard) var(--ck-spacing-standard) 0;
+			margin: var(--ck-spacing-large) var(--ck-spacing-large) 0;
 
 			& .ck-input-text {
 				min-width: 0;
@@ -52,7 +52,7 @@
 		& > .ck-button:nth-last-child(1),
 		& > .ck-button:nth-last-child(2) {
 			padding: var(--ck-spacing-standard);
-			margin-top: var(--ck-spacing-standard);
+			margin-top: var(--ck-spacing-large);
 
 			border-radius: 0;
 			border: 0;

+ 1 - 1
packages/ckeditor5-theme-lark/theme/ckeditor5-ui/globals/_colors.css

@@ -70,7 +70,7 @@
 	--ck-color-input-text: 										var(--ck-color-base-text);
 	--ck-color-input-disabled-background: 						hsl(0, 0%, 95%);
 	--ck-color-input-disabled-border: 							hsl(0, 0%, 78%);
-	--ck-color-input-disabled-text: 							hsl(0, 0%, 36%);
+	--ck-color-input-disabled-text: 							hsl(0, 0%, 46%);
 
 	/* -- List ---------------------------------------------------------------------------------- */
 

+ 72 - 0
packages/ckeditor5-ui/src/bindings/injectcsstransitiondisabler.js

@@ -0,0 +1,72 @@
+/**
+ * @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 ui/bindings/injectcsstransitiondisabler
+ */
+
+/**
+ * A decorator that brings possibility to temporarily disable CSS transitions using
+ * {@link module:ui/view~View} methods. It is helpful when, for instance, the transitions should not happen
+ * when the view is first displayed but they should work normally in other cases.
+ *
+ * The methods to control the CSS transitions are:
+ * * `disableCssTransitions()` – adds the `.ck-transitions-disabled` class to the
+ * {@link module:ui/view~View#element view element},
+ * * `enableCssTransitions()` – removes the `.ck-transitions-disabled` class from the
+ * {@link module:ui/view~View#element view element}.
+ *
+ * **Note**: This helper extends the {@link module:ui/view~View#template template} and must be used **after**
+ * {@link module:ui/view~View#setTemplate} is called:
+ *
+ *		import injectCssTransitionDisabler from '@ckeditor/ckeditor5-ui/src/bindings/injectcsstransitiondisabler';
+ *
+ *		class MyView extends View {
+ *			constructor() {
+ *				super();
+ *
+ *				// ...
+ *
+ *				this.setTemplate( { ... } );
+ *
+ *				// ...
+ *
+ *				injectCssTransitionDisabler( this );
+ *
+ *				// ...
+ *			}
+ *		}
+ *
+ * The usage comes down to:
+ *
+ *		const view = new MyView();
+ *
+ *		// ...
+ *
+ *		view.disableCssTransitions();
+ *		view.show();
+ *		view.enableCssTransitions();
+ *
+ * @param {module:ui/view~View} view View instance that should get this functionality.
+ */
+export default function injectCssTransitionDisabler( view ) {
+	view.set( '_isCssTransitionsDisabled', false );
+
+	view.disableCssTransitions = () => {
+		view._isCssTransitionsDisabled = true;
+	};
+
+	view.enableCssTransitions = () => {
+		view._isCssTransitionsDisabled = false;
+	};
+
+	view.extendTemplate( {
+		attributes: {
+			class: [
+				view.bindTemplate.if( '_isCssTransitionsDisabled', 'ck-transitions-disabled' )
+			]
+		}
+	} );
+}

+ 63 - 6
packages/ckeditor5-ui/src/inputtext/inputtextview.js

@@ -8,6 +8,7 @@
  */
 
 import View from '../view';
+import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker';
 import '../../theme/components/inputtext/inputtext.css';
 
 /**
@@ -72,6 +73,37 @@ export default class InputTextView extends View {
 		 */
 		this.set( 'ariaDescribedById' );
 
+		/**
+		 * Stores the information about the editor UI focus and propagates it so various plugins and components
+		 * are unified as a focus group.
+		 *
+		 * @readonly
+		 * @member {module:utils/focustracker~FocusTracker} #focusTracker
+		 */
+		this.focusTracker = new FocusTracker();
+
+		/**
+		 * An observable flag set to `true` when the input is currently focused by the user.
+		 * `false` otherwise.
+		 *
+		 * @readonly
+		 * @observable
+		 * @member {Boolean} #isFocused
+		 * @default false
+		 */
+		this.bind( 'isFocused' ).to( this.focusTracker );
+
+		/**
+		 * An observable flag set to `true` when the input contains no text, i.e.
+		 * when {@link #value} is `''`, `null`, or `false`.
+		 *
+		 * @readonly
+		 * @observable
+		 * @member {Boolean} #isEmpty
+		 * @default true
+		 */
+		this.set( 'isEmpty', true );
+
 		const bind = this.bindTemplate;
 
 		this.setTemplate( {
@@ -82,6 +114,8 @@ export default class InputTextView extends View {
 					'ck',
 					'ck-input',
 					'ck-input-text',
+					bind.if( 'isFocused', 'ck-input_focused' ),
+					bind.if( 'isEmpty', 'ck-input-text_empty' ),
 					bind.if( 'hasError', 'ck-error' )
 				],
 				id: bind.to( 'id' ),
@@ -91,7 +125,8 @@ export default class InputTextView extends View {
 				'aria-describedby': bind.to( 'ariaDescribedById' )
 			},
 			on: {
-				input: bind.to( 'input' )
+				input: bind.to( 'input' ),
+				change: bind.to( this._updateIsEmpty.bind( this ) )
 			}
 		} );
 
@@ -109,16 +144,16 @@ export default class InputTextView extends View {
 	render() {
 		super.render();
 
-		const setValue = value => {
-			this.element.value = ( !value && value !== 0 ) ? '' : value;
-		};
+		this.focusTracker.add( this.element );
 
-		setValue( this.value );
+		this._setDomElementValue( this.value );
+		this._updateIsEmpty();
 
 		// Bind `this.value` to the DOM element's value.
 		// We cannot use `value` DOM attribute because removing it on Edge does not clear the DOM element's value property.
 		this.on( 'change:value', ( evt, name, value ) => {
-			setValue( value );
+			this._setDomElementValue( value );
+			this._updateIsEmpty();
 		} );
 	}
 
@@ -135,4 +170,26 @@ export default class InputTextView extends View {
 	focus() {
 		this.element.focus();
 	}
+
+	/**
+	 * Updates the {@link #isEmpty} property value on demand.
+	 *
+	 * @private
+	 */
+	_updateIsEmpty() {
+		this.isEmpty = isInputElementEmpty( this.element );
+	}
+
+	/**
+	 * Sets the `value` property of the {@link #element DOM element} on demand.
+	 *
+	 * @private
+	 */
+	_setDomElementValue( value ) {
+		this.element.value = ( !value && value !== 0 ) ? '' : value;
+	}
+}
+
+function isInputElementEmpty( domElement ) {
+	return !domElement.value;
 }

+ 48 - 3
packages/ckeditor5-ui/src/labeledfield/labeledfieldview.js

@@ -92,6 +92,27 @@ export default class LabeledFieldView extends View {
 		 */
 		this.set( 'isEnabled', true );
 
+		/**
+		 * An observable flag set `true` when {@link #fieldView} is empty (`false` otherwise).
+		 *
+		 * @readonly
+		 * @observable
+		 * @member {Boolean} #isEmpty
+		 * @default true
+		 */
+		this.set( 'isEmpty', true );
+
+		/**
+		 * An observable flag set `true` when {@link #fieldView} is currently focused by
+		 * the user (`false` otherwise).
+		 *
+		 * @readonly
+		 * @observable
+		 * @member {Boolean} #isFocused
+		 * @default false
+		 */
+		this.set( 'isFocused', false );
+
 		/**
 		 * The validation error text. When set, it will be displayed
 		 * next to the {@link #fieldView} as a typical validation error message.
@@ -117,6 +138,7 @@ export default class LabeledFieldView extends View {
 		 *
 		 * @observable
 		 * @member {String|null} #infoText
+		 * @default null
 		 */
 		this.set( 'infoText', null );
 
@@ -128,6 +150,14 @@ export default class LabeledFieldView extends View {
 		 */
 		this.set( 'class' );
 
+		/**
+		 * The content of the `placeholder` attribute of the {@link #fieldView}.
+		 *
+		 * @observable
+		 * @member {String} #placeholder
+		 */
+		this.set( 'placeholder' );
+
 		/**
 		 * The label view instance that describes the entire view.
 		 *
@@ -170,12 +200,27 @@ export default class LabeledFieldView extends View {
 					'ck',
 					'ck-labeled-field-view',
 					bind.to( 'class' ),
-					bind.if( 'isEnabled', 'ck-disabled', value => !value )
+					bind.if( 'isEnabled', 'ck-disabled', value => !value ),
+					bind.if( 'isEmpty', 'ck-labeled-field-view_empty' ),
+					bind.if( 'isFocused', 'ck-labeled-field-view_focused' ),
+					bind.if( 'placeholder', 'ck-labeled-field-view_placeholder' ),
+					bind.if( 'errorText', 'ck-error' )
 				]
 			},
 			children: [
-				this.labelView,
-				this.fieldView,
+				{
+					tag: 'div',
+					attributes: {
+						class: [
+							'ck',
+							'ck-labeled-field-view__input-wrapper'
+						]
+					},
+					children: [
+						this.fieldView,
+						this.labelView
+					]
+				},
 				this.statusView
 			]
 		} );

+ 2 - 0
packages/ckeditor5-ui/src/labeledfield/utils.js

@@ -52,6 +52,8 @@ export function createLabeledInputText( labeledFieldView, viewUid, statusUid ) {
 		labeledFieldView.errorText = null;
 	} );
 
+	labeledFieldView.bind( 'isEmpty', 'isFocused', 'placeholder' ).to( inputView );
+
 	return inputView;
 }
 

+ 66 - 0
packages/ckeditor5-ui/tests/bindings/injectcsstransitiondisabler.js

@@ -0,0 +1,66 @@
+/**
+ * @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 injectCssTransitionDisabler from '../../src/bindings/injectcsstransitiondisabler';
+import View from '../../src/view';
+
+describe( 'injectCssTransitionDisabler()', () => {
+	let view;
+
+	class TestView extends View {
+		constructor( ...args ) {
+			super( ...args );
+
+			this.setTemplate( {
+				tag: 'div'
+			} );
+
+			injectCssTransitionDisabler( this );
+		}
+	}
+
+	beforeEach( () => {
+		view = new TestView();
+		view.render();
+	} );
+
+	afterEach( () => {
+		view.destroy();
+	} );
+
+	it( 'should create a protected observable property for class binding', () => {
+		expect( view._isCssTransitionsDisabled ).to.be.false;
+	} );
+
+	it( 'should not alter the CSS class of the view until blocking is enabled', () => {
+		expect( view.element.classList ).to.be.empty;
+	} );
+
+	describe( 'disableCssTransitions() method', () => {
+		it( 'should belong to the view', () => {
+			expect( view.disableCssTransitions ).to.be.a( 'function' );
+		} );
+
+		it( 'should set the proper CSS class when called', () => {
+			view.disableCssTransitions();
+
+			expect( view.element.classList.contains( 'ck-transitions-disabled' ) ).to.be.true;
+		} );
+	} );
+
+	describe( 'enableCssTransitions() method', () => {
+		it( 'should belong to the view', () => {
+			expect( view.enableCssTransitions ).to.be.a( 'function' );
+		} );
+
+		it( 'should remove the proper CSS class when called', () => {
+			view.disableCssTransitions();
+			expect( view.element.classList.contains( 'ck-transitions-disabled' ) ).to.be.true;
+
+			view.enableCssTransitions();
+			expect( view.element.classList.contains( 'ck-transitions-disabled' ) ).to.be.false;
+		} );
+	} );
+} );

+ 55 - 0
packages/ckeditor5-ui/tests/inputtext/inputtextview.js

@@ -5,6 +5,7 @@
 
 /* global Event */
 
+import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker';
 import InputTextView from '../../src/inputtext/inputtextview';
 
 describe( 'InputTextView', () => {
@@ -29,6 +30,18 @@ describe( 'InputTextView', () => {
 			expect( view.element.classList.contains( 'ck-input' ) ).to.be.true;
 			expect( view.element.classList.contains( 'ck-input-text' ) ).to.be.true;
 		} );
+
+		it( 'should set the #isFocused observable property', () => {
+			expect( view.isFocused ).to.be.false;
+		} );
+
+		it( 'should set the #isEmpty observable property', () => {
+			expect( view.isEmpty ).to.be.true;
+		} );
+
+		it( 'should create an instance of FocusTracker under #focusTracker property', () => {
+			expect( view.focusTracker ).to.be.instanceOf( FocusTracker );
+		} );
 	} );
 
 	describe( 'DOM bindings', () => {
@@ -110,6 +123,24 @@ describe( 'InputTextView', () => {
 
 				expect( view.element.classList.contains( 'ck-error' ) ).to.be.true;
 			} );
+
+			it( 'should react on view#isFocused', () => {
+				expect( view.element.classList.contains( 'ck-input_focused' ) ).to.be.false;
+
+				view.isFocused = true;
+
+				expect( view.element.classList.contains( 'ck-input_focused' ) ).to.be.true;
+			} );
+
+			it( 'should react on view#isEmpty', () => {
+				view.value = '';
+
+				expect( view.element.classList.contains( 'ck-input-text_empty' ) ).to.be.true;
+
+				view.value = 'bar';
+
+				expect( view.element.classList.contains( 'ck-input-text_empty' ) ).to.be.false;
+			} );
 		} );
 
 		describe( 'aria-invalid', () => {
@@ -142,6 +173,30 @@ describe( 'InputTextView', () => {
 				sinon.assert.calledOnce( spy );
 			} );
 		} );
+
+		describe( 'change event', () => {
+			it( 'should trigger update of the #isEmpty property', () => {
+				view.element.value = 'foo';
+				view.element.dispatchEvent( new Event( 'change' ) );
+
+				expect( view.isEmpty ).to.be.false;
+
+				view.element.value = '';
+				view.element.dispatchEvent( new Event( 'change' ) );
+
+				expect( view.isEmpty ).to.be.true;
+			} );
+		} );
+	} );
+
+	describe( 'render()', () => {
+		it( 'registers #element in the #focusTracker', () => {
+			expect( view.isFocused ).to.be.false;
+
+			view.element.dispatchEvent( new Event( 'focus' ) );
+
+			expect( view.isFocused ).to.be.true;
+		} );
 	} );
 
 	describe( 'select()', () => {

+ 104 - 59
packages/ckeditor5-ui/tests/labeledfield/labeledfieldview.js

@@ -10,133 +10,178 @@ import LabelView from '../../src/label/labelview';
 describe( 'LabeledFieldView', () => {
 	const locale = {};
 
-	let labeledInput, view;
+	let labeledField, fieldView;
 
 	beforeEach( () => {
-		labeledInput = new LabeledFieldView( locale, ( labeledInput, viewUid, statusUid ) => {
-			view = new View( locale );
-			view.setTemplate( { tag: 'div' } );
-			view.focus = () => {};
-			view.viewUid = viewUid;
-			view.statusUid = statusUid;
+		labeledField = new LabeledFieldView( locale, ( labeledField, viewUid, statusUid ) => {
+			fieldView = new View( locale );
+			fieldView.setTemplate( { tag: 'div' } );
+			fieldView.focus = () => {};
+			fieldView.viewUid = viewUid;
+			fieldView.statusUid = statusUid;
 
-			return view;
+			return fieldView;
 		} );
 
-		labeledInput.render();
+		labeledField.render();
 	} );
 
 	afterEach( () => {
-		labeledInput.destroy();
+		labeledField.destroy();
 	} );
 
 	describe( 'constructor()', () => {
-		it( 'should set labeledInput#locale', () => {
-			expect( labeledInput.locale ).to.deep.equal( locale );
+		it( 'should set labeledField#locale', () => {
+			expect( labeledField.locale ).to.deep.equal( locale );
 		} );
 
-		it( 'should set labeledInput#fieldView', () => {
-			expect( labeledInput.fieldView ).to.equal( view );
+		it( 'should set labeledField#fieldView', () => {
+			expect( labeledField.fieldView ).to.equal( fieldView );
 		} );
 
-		it( 'should set labeledInput#label', () => {
-			expect( labeledInput.label ).to.be.undefined;
+		it( 'should set labeledField#label', () => {
+			expect( labeledField.label ).to.be.undefined;
 		} );
 
-		it( 'should set labeledInput#isEnabled', () => {
-			expect( labeledInput.isEnabled ).to.be.true;
+		it( 'should set labeledField#isEnabled', () => {
+			expect( labeledField.isEnabled ).to.be.true;
 		} );
 
-		it( 'should set labeledInput#errorText', () => {
-			expect( labeledInput.errorText ).to.be.null;
+		it( 'should set labeledField#errorText', () => {
+			expect( labeledField.errorText ).to.be.null;
 		} );
 
-		it( 'should set labeledInput#infoText', () => {
-			expect( labeledInput.infoText ).to.be.null;
+		it( 'should set labeledField#infoText', () => {
+			expect( labeledField.infoText ).to.be.null;
 		} );
 
-		it( 'should set labeledInput#class', () => {
-			expect( labeledInput.class ).to.be.undefined;
+		it( 'should set labeledField#class', () => {
+			expect( labeledField.class ).to.be.undefined;
 		} );
 
-		it( 'should create labeledInput#labelView', () => {
-			expect( labeledInput.labelView ).to.instanceOf( LabelView );
+		it( 'should set labeledField#isEmpty', () => {
+			expect( labeledField.isEmpty ).to.be.true;
 		} );
 
-		it( 'should create labeledInput#statusView', () => {
-			expect( labeledInput.statusView ).to.instanceOf( View );
+		it( 'should set labeledField#isFocused', () => {
+			expect( labeledField.isFocused ).to.be.false;
+		} );
 
-			expect( labeledInput.statusView.element.tagName ).to.equal( 'DIV' );
-			expect( labeledInput.statusView.element.classList.contains( 'ck' ) ).to.be.true;
-			expect( labeledInput.statusView.element.classList.contains( 'ck-labeled-field-view__status' ) ).to.be.true;
+		it( 'should create labeledField#labelView', () => {
+			expect( labeledField.labelView ).to.instanceOf( LabelView );
+		} );
+
+		it( 'should create labeledField#statusView', () => {
+			expect( labeledField.statusView ).to.instanceOf( View );
+
+			expect( labeledField.statusView.element.tagName ).to.equal( 'DIV' );
+			expect( labeledField.statusView.element.classList.contains( 'ck' ) ).to.be.true;
+			expect( labeledField.statusView.element.classList.contains( 'ck-labeled-field-view__status' ) ).to.be.true;
 		} );
 
 		it( 'should allow pairing #view and #labelView by unique id', () => {
-			expect( labeledInput.labelView.for ).to.equal( view.viewUid );
+			expect( labeledField.labelView.for ).to.equal( fieldView.viewUid );
 		} );
 
 		it( 'should allow pairing #view and #statusView by unique id', () => {
-			expect( view.statusUid ).to.equal( labeledInput.statusView.element.id );
+			expect( fieldView.statusUid ).to.equal( labeledField.statusView.element.id );
 		} );
 	} );
 
 	describe( 'template', () => {
 		it( 'should have the CSS class', () => {
-			expect( labeledInput.element.classList.contains( 'ck' ) ).to.be.true;
-			expect( labeledInput.element.classList.contains( 'ck-labeled-field-view' ) ).to.be.true;
+			expect( labeledField.element.classList.contains( 'ck' ) ).to.be.true;
+			expect( labeledField.element.classList.contains( 'ck-labeled-field-view' ) ).to.be.true;
 		} );
 
-		it( 'should have #labeledInput', () => {
-			expect( labeledInput.template.children[ 0 ] ).to.equal( labeledInput.labelView );
+		it( 'should have a wrapper for internals', () => {
+			expect( labeledField.element.firstChild.classList.contains( 'ck' ) ).to.be.true;
+			expect( labeledField.element.firstChild.classList.contains( 'ck-labeled-field-view__input-wrapper' ) ).to.be.true;
 		} );
 
-		it( 'should have #view', () => {
-			expect( labeledInput.template.children[ 1 ] ).to.equal( view );
+		it( 'should have #fieldView', () => {
+			expect( labeledField.template.children[ 0 ].children[ 0 ] ).to.equal( fieldView );
+		} );
+
+		it( 'should have #labelView', () => {
+			expect( labeledField.template.children[ 0 ].children[ 1 ] ).to.equal( labeledField.labelView );
 		} );
 
 		it( 'should have the #statusView container', () => {
-			expect( labeledInput.template.children[ 2 ] ).to.equal( labeledInput.statusView );
+			expect( labeledField.template.children[ 1 ] ).to.equal( labeledField.statusView );
 		} );
 
 		describe( 'DOM bindings', () => {
 			describe( 'class', () => {
-				it( 'should react on labeledInput#class', () => {
-					labeledInput.class = 'foo';
-					expect( labeledInput.element.classList.contains( 'foo' ) ).to.be.true;
+				it( 'should react on labeledField#class', () => {
+					labeledField.class = 'foo';
+					expect( labeledField.element.classList.contains( 'foo' ) ).to.be.true;
+
+					labeledField.class = 'bar';
+					expect( labeledField.element.classList.contains( 'foo' ) ).to.be.false;
+					expect( labeledField.element.classList.contains( 'bar' ) ).to.be.true;
+				} );
+
+				it( 'should react on labeledField#isEnabled', () => {
+					labeledField.isEnabled = true;
+					expect( labeledField.element.classList.contains( 'ck-disabled' ) ).to.be.false;
+
+					labeledField.isEnabled = false;
+					expect( labeledField.element.classList.contains( 'ck-disabled' ) ).to.be.true;
+				} );
+
+				it( 'should react on labeledField#isEmpty', () => {
+					labeledField.isEmpty = true;
+					expect( labeledField.element.classList.contains( 'ck-labeled-field-view_empty' ) ).to.be.true;
+
+					labeledField.isEmpty = false;
+					expect( labeledField.element.classList.contains( 'ck-labeled-field-view_empty' ) ).to.be.false;
+				} );
+
+				it( 'should react on labeledField#isFocused', () => {
+					labeledField.isFocused = true;
+					expect( labeledField.element.classList.contains( 'ck-labeled-field-view_focused' ) ).to.be.true;
+
+					labeledField.isFocused = false;
+					expect( labeledField.element.classList.contains( 'ck-labeled-field-view_focused' ) ).to.be.false;
+				} );
+
+				it( 'should react on labeledField#placeholder', () => {
+					labeledField.placeholder = 'asd';
+					expect( labeledField.element.classList.contains( 'ck-labeled-field-view_placeholder' ) ).to.be.true;
 
-					labeledInput.class = 'bar';
-					expect( labeledInput.element.classList.contains( 'foo' ) ).to.be.false;
-					expect( labeledInput.element.classList.contains( 'bar' ) ).to.be.true;
+					labeledField.placeholder = null;
+					expect( labeledField.element.classList.contains( 'ck-labeled-field-view_placeholder' ) ).to.be.false;
 				} );
 			} );
 
 			describe( 'status container', () => {
-				it( 'should react on labeledInput#errorText', () => {
-					const statusElement = labeledInput.statusView.element;
+				it( 'should react on labeledField#errorText', () => {
+					const statusElement = labeledField.statusView.element;
 
-					labeledInput.errorText = '';
+					labeledField.errorText = '';
 					expect( statusElement.classList.contains( 'ck-hidden' ) ).to.be.true;
 					expect( statusElement.classList.contains( 'ck-labeled-field-view__status_error' ) ).to.be.false;
 					expect( statusElement.hasAttribute( 'role' ) ).to.be.false;
 					expect( statusElement.innerHTML ).to.equal( '' );
 
-					labeledInput.errorText = 'foo';
+					labeledField.errorText = 'foo';
 					expect( statusElement.classList.contains( 'ck-hidden' ) ).to.be.false;
 					expect( statusElement.classList.contains( 'ck-labeled-field-view__status_error' ) ).to.be.true;
 					expect( statusElement.getAttribute( 'role' ) ).to.equal( 'alert' );
 					expect( statusElement.innerHTML ).to.equal( 'foo' );
 				} );
 
-				it( 'should react on labeledInput#infoText', () => {
-					const statusElement = labeledInput.statusView.element;
+				it( 'should react on labeledField#infoText', () => {
+					const statusElement = labeledField.statusView.element;
 
-					labeledInput.infoText = '';
+					labeledField.infoText = '';
 					expect( statusElement.classList.contains( 'ck-hidden' ) ).to.be.true;
 					expect( statusElement.classList.contains( 'ck-labeled-field-view__status_error' ) ).to.be.false;
 					expect( statusElement.hasAttribute( 'role' ) ).to.be.false;
 					expect( statusElement.innerHTML ).to.equal( '' );
 
-					labeledInput.infoText = 'foo';
+					labeledField.infoText = 'foo';
 					expect( statusElement.classList.contains( 'ck-hidden' ) ).to.be.false;
 					expect( statusElement.classList.contains( 'ck-labeled-field-view__status_error' ) ).to.be.false;
 					expect( statusElement.hasAttribute( 'role' ) ).to.be.false;
@@ -147,18 +192,18 @@ describe( 'LabeledFieldView', () => {
 	} );
 
 	describe( 'binding', () => {
-		it( 'should bind labeledInput#label to labeledInput.labelView#label', () => {
-			labeledInput.label = 'Foo bar';
+		it( 'should bind labeledField#label to labeledField.labelView#label', () => {
+			labeledField.label = 'Foo bar';
 
-			expect( labeledInput.labelView.text ).to.equal( 'Foo bar' );
+			expect( labeledField.labelView.text ).to.equal( 'Foo bar' );
 		} );
 	} );
 
 	describe( 'focus()', () => {
 		it( 'should focus the #view in DOM', () => {
-			const spy = sinon.spy( view, 'focus' );
+			const spy = sinon.spy( fieldView, 'focus' );
 
-			labeledInput.focus();
+			labeledField.focus();
 
 			sinon.assert.calledOnce( spy );
 		} );

+ 24 - 0
packages/ckeditor5-ui/tests/labeledfield/utils.js

@@ -64,6 +64,30 @@ describe( 'LabeledFieldView utils', () => {
 			expect( labeledInput.fieldView.hasError ).to.be.false;
 		} );
 
+		it( 'should bind labeledInput#isEmpty to input\'s #isEmpty', () => {
+			labeledInput.fieldView.isEmpty = true;
+			expect( labeledInput.isEmpty ).to.be.true;
+
+			labeledInput.fieldView.isEmpty = false;
+			expect( labeledInput.isEmpty ).to.be.false;
+		} );
+
+		it( 'should bind labeledInput#isFocused to input\'s #isFocused', () => {
+			labeledInput.fieldView.isFocused = true;
+			expect( labeledInput.isFocused ).to.be.true;
+
+			labeledInput.fieldView.isFocused = false;
+			expect( labeledInput.isFocused ).to.be.false;
+		} );
+
+		it( 'should bind labeledInput#placeholder to input\'s #placeholder', () => {
+			labeledInput.fieldView.placeholder = null;
+			expect( labeledInput.placeholder ).to.be.null;
+
+			labeledInput.fieldView.placeholder = 'foo';
+			expect( labeledInput.placeholder ).to.equal( 'foo' );
+		} );
+
 		it( 'should clean labeledInput#errorText upon input\'s DOM "update" event', () => {
 			labeledInput.errorText = 'some error';
 			labeledInput.fieldView.fire( 'input' );

+ 11 - 5
packages/ckeditor5-ui/theme/components/labeledfield/labeledfieldview.css

@@ -3,8 +3,14 @@
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
-/*
- * Note: This file should contain the wireframe styles only. But since there are no such styles,
- * it acts as a message to the builder telling that it should look for the corresponding styles
- * **in the theme** when compiling the editor.
- */
+.ck.ck-labeled-field-view {
+	& > .ck.ck-labeled-field-view__input-wrapper {
+		display: flex;
+		position: relative;
+	}
+
+	& .ck.ck-label {
+		display: block;
+		position: absolute;
+	}
+}

+ 12 - 0
packages/ckeditor5-ui/theme/globals/_transition.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
+ */
+
+/**
+ * A class that disables all transitions of the element and its children.
+ */
+.ck-transitions-disabled,
+.ck-transitions-disabled * {
+	transition: none !important;
+}

+ 1 - 0
packages/ckeditor5-ui/theme/globals/globals.css

@@ -6,3 +6,4 @@
 @import "./_hidden.css";
 @import "./_reset.css";
 @import "./_zindex.css";
+@import "./_transition.css";