Browse Source

Change folder name by removing `view` from the end

panr 5 years ago
parent
commit
702960fd26

+ 241 - 0
packages/ckeditor5-ui/src/labeledfield/labeledfieldview.js

@@ -0,0 +1,241 @@
+/**
+ * @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/labeledfield/labeledfieldview
+ */
+
+import View from '../view';
+import uid from '@ckeditor/ckeditor5-utils/src/uid';
+import LabelView from '../label/labelview';
+import '../../theme/components/labeledfield/labeledfieldview.css';
+
+/**
+ * The labeled field view class. It can be used to enhance any view with the following features:
+ *
+ * * a label,
+ * * (optional) an error message,
+ * * (optional) an info (status) text,
+ *
+ * all bound logically by proper DOM attributes for UX and accessibility.  It also provides an interface
+ * (e.g. observable properties) that allows controlling those additional features.
+ *
+ * The constructor of this class requires a callback that returns a view to be labeled. The callback
+ * is called with unique ids that allow binding of DOM properties:
+ *
+ *		const labeledInputView = new LabeledFieldView( locale, ( labeledFieldView, viewUid, statusUid ) => {
+ *			const inputView = new InputTextView( labeledFieldView.locale );
+ *
+ *			inputView.set( {
+ *				id: viewUid,
+ *				ariaDescribedById: statusUid
+ *			} );
+ *
+ *			inputView.bind( 'isReadOnly' ).to( labeledFieldView, 'isEnabled', value => !value );
+ *			inputView.bind( 'hasError' ).to( labeledFieldView, 'errorText', value => !!value );
+ *
+ *			return inputView;
+ *		} );
+ *
+ *		labeledInputView.label = 'User name';
+ *		labeledInputView.infoText = 'Full name like for instance, John Doe.';
+ *		labeledInputView.render();
+ *
+ *		document.body.append( labeledInputView.element );
+ *
+ * See {@link module:ui/labeledfield/utils} to discover ready–to–use labeled input helpers for common
+ * UI components.
+ *
+ * @extends module:ui/view~View
+ */
+export default class LabeledFieldView extends View {
+	/**
+	 * Creates an instance of the labeled field view class using a provided creator function
+	 * that provides the view to be labeled.
+	 *
+	 * @param {module:utils/locale~Locale} locale The locale instance.
+	 * @param {Function} viewCreator A function that returns a {@link module:ui/view~View}
+	 * that will be labeled. The following arguments are passed to the creator function:
+	 *
+	 * * an instance of the `LabeledFieldView` to allow binding observable properties,
+	 * * an UID string that connects the {@link #labelView label} and the labeled field view in DOM,
+	 * * an UID string that connects the {@link #statusView status} and the labeled field view in DOM.
+	 */
+	constructor( locale, viewCreator ) {
+		super( locale );
+
+		const viewUid = `ck-labeled-view-${ uid() }`;
+		const statusUid = `ck-labeled-view-status-${ uid() }`;
+
+		/**
+		 * The field that gets labeled.
+		 *
+		 * @member {module:ui/view~View} #field
+		 */
+		this.field = viewCreator( this, viewUid, statusUid );
+
+		/**
+		 * The text of the label.
+		 *
+		 * @observable
+		 * @member {String} #label
+		 */
+		this.set( 'label' );
+
+		/**
+		 * Controls whether the component is in read-only mode.
+		 *
+		 * @observable
+		 * @member {Boolean} #isEnabled
+		 */
+		this.set( 'isEnabled', true );
+
+		/**
+		 * The validation error text. When set, it will be displayed
+		 * next to the {@link #field} as a typical validation error message.
+		 * Set it to `null` to hide the message.
+		 *
+		 * **Note:** Setting this property to anything but `null` will automatically
+		 * make the `hasError` of the {@link #field} `true`.
+		 *
+		 * @observable
+		 * @member {String|null} #errorText
+		 */
+		this.set( 'errorText', null );
+
+		/**
+		 * The additional information text displayed next to the {@link #field} which can
+		 * be used to inform the user about its purpose, provide help or hints.
+		 *
+		 * Set it to `null` to hide the message.
+		 *
+		 * **Note:** This text will be displayed in the same place as {@link #errorText} but the
+		 * latter always takes precedence: if the {@link #errorText} is set, it replaces
+		 * {@link #infoText}.
+		 *
+		 * @observable
+		 * @member {String|null} #infoText
+		 */
+		this.set( 'infoText', null );
+
+		/**
+		 * (Optional) The additional CSS class set on the dropdown {@link #element}.
+		 *
+		 * @observable
+		 * @member {String} #class
+		 */
+		this.set( 'class' );
+
+		/**
+		 * The label view instance that describes the entire view.
+		 *
+		 * @member {module:ui/label/labelview~LabelView} #labelView
+		 */
+		this.labelView = this._createLabelView( viewUid );
+
+		/**
+		 * The status view for the {@link #field}. It displays {@link #errorText} and
+		 * {@link #infoText}.
+		 *
+		 * @member {module:ui/view~View} #statusView
+		 */
+		this.statusView = this._createStatusView( statusUid );
+
+		/**
+		 * The combined status text made of {@link #errorText} and {@link #infoText}.
+		 * Note that when present, {@link #errorText} always takes precedence in the
+		 * status.
+		 *
+		 * @see #errorText
+		 * @see #infoText
+		 * @see #statusView
+		 * @private
+		 * @observable
+		 * @member {String|null} #_statusText
+		 */
+		this.bind( '_statusText' ).to(
+			this, 'errorText',
+			this, 'infoText',
+			( errorText, infoText ) => errorText || infoText
+		);
+
+		const bind = this.bindTemplate;
+
+		this.setTemplate( {
+			tag: 'div',
+			attributes: {
+				class: [
+					'ck',
+					'ck-labeled-view',
+					bind.to( 'class' ),
+					bind.if( 'isEnabled', 'ck-disabled', value => !value )
+				]
+			},
+			children: [
+				this.labelView,
+				this.field,
+				this.statusView
+			]
+		} );
+	}
+
+	/**
+	 * Creates label view class instance and bind with view.
+	 *
+	 * @private
+	 * @param {String} id Unique id to set as labelView#for attribute.
+	 * @returns {module:ui/label/labelview~LabelView}
+	 */
+	_createLabelView( id ) {
+		const labelView = new LabelView( this.locale );
+
+		labelView.for = id;
+		labelView.bind( 'text' ).to( this, 'label' );
+
+		return labelView;
+	}
+
+	/**
+	 * Creates the status view instance. It displays {@link #errorText} and {@link #infoText}
+	 * next to the {@link #field}. See {@link #_statusText}.
+	 *
+	 * @private
+	 * @param {String} statusUid Unique id of the status, shared with the {@link #field view's}
+	 * `aria-describedby` attribute.
+	 * @returns {module:ui/view~View}
+	 */
+	_createStatusView( statusUid ) {
+		const statusView = new View( this.locale );
+		const bind = this.bindTemplate;
+
+		statusView.setTemplate( {
+			tag: 'div',
+			attributes: {
+				class: [
+					'ck',
+					'ck-labeled-view__status',
+					bind.if( 'errorText', 'ck-labeled-view__status_error' ),
+					bind.if( '_statusText', 'ck-hidden', value => !value )
+				],
+				id: statusUid,
+				role: bind.if( 'errorText', 'alert' )
+			},
+			children: [
+				{
+					text: bind.to( '_statusText' )
+				}
+			]
+		} );
+
+		return statusView;
+	}
+
+	/**
+	 * Focuses the {@link #field}.
+	 */
+	focus() {
+		this.field.focus();
+	}
+}

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

@@ -0,0 +1,92 @@
+/**
+ * @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/labeledfield/utils
+ */
+
+import InputTextView from '../inputtext/inputtextview';
+import { createDropdown } from '../dropdown/utils';
+
+/**
+ * A helper for creating labeled inputs.
+ *
+ * It creates an instance of a {@link module:ui/inputtext/inputtextview~InputTextView input text} that is
+ * logically related to a {@link module:ui/labeledfield/labeledfieldview~LabeledFieldView labeled view} in DOM.
+ *
+ * The helper does the following:
+ *
+ * * It sets input's `id` and `ariaDescribedById` attributes.
+ * * It binds input's `isReadOnly` to the labeled view.
+ * * It binds input's `hasError` to the labeled view.
+ * * It enables a logic that cleans up the error when user starts typing in the input..
+ *
+ * Usage:
+ *
+ *		const labeledInputView = new LabeledFieldView( locale, createLabeledDropdown );
+ *		console.log( labeledInputView.view ); // An input instance.
+ *
+ * @param {module:ui/labeledfield/labeledfieldview~LabeledFieldView} labeledFieldView The instance of the labeled view.
+ * @param {String} viewUid An UID string that allows DOM logical connection between the
+ * {@link module:ui/labeledfield/labeledfieldview~LabeledFieldView#labelView labeled view's label} and the input.
+ * @param {String} statusUid An UID string that allows DOM logical connection between the
+ * {@link module:ui/labeledfield/labeledfieldview~LabeledFieldView#statusView labeled view's status} and the input.
+ * @returns {module:ui/inputtext/inputtextview~InputTextView} The input text view instance.
+ */
+export function createLabeledInputText( labeledFieldView, viewUid, statusUid ) {
+	const inputView = new InputTextView( labeledFieldView.locale );
+
+	inputView.set( {
+		id: viewUid,
+		ariaDescribedById: statusUid
+	} );
+
+	inputView.bind( 'isReadOnly' ).to( labeledFieldView, 'isEnabled', value => !value );
+	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
+		// starts fixing the errors.
+		labeledFieldView.errorText = null;
+	} );
+
+	return inputView;
+}
+
+/**
+ * A helper for creating labeled dropdowns.
+ *
+ * It creates an instance of a {@link module:ui/dropdown/dropdownview~DropdownView dropdown} that is
+ * logically related to a {@link module:ui/labeledfield/labeledfieldview~LabeledFieldView labeled view}.
+ *
+ * The helper does the following:
+ *
+ * * It sets dropdown's `id` and `ariaDescribedById` attributes.
+ * * It binds input's `isEnabled` to the labeled view.
+ *
+ * Usage:
+ *
+ *		const labeledInputView = new LabeledFieldView( locale, createLabeledDropdown );
+ *		console.log( labeledInputView.view ); // A dropdown instance.
+ *
+ * @param {module:ui/labeledfield/labeledfieldview~LabeledFieldView} labeledFieldView The instance of the labeled view.
+ * @param {String} viewUid An UID string that allows DOM logical connection between the
+ * {@link module:ui/labeledfield/labeledfieldview~LabeledFieldView#labelView labeled view label} and the dropdown.
+ * @param {String} statusUid An UID string that allows DOM logical connection between the
+ * {@link module:ui/labeledfield/labeledfieldview~LabeledFieldView#statusView labeled view status} and the dropdown.
+ * @returns {module:ui/dropdown/dropdownview~DropdownView} The dropdown view instance.
+ */
+export function createLabeledDropdown( labeledFieldView, viewUid, statusUid ) {
+	const dropdownView = createDropdown( labeledFieldView.locale );
+
+	dropdownView.set( {
+		id: viewUid,
+		ariaDescribedById: statusUid
+	} );
+
+	dropdownView.bind( 'isEnabled' ).to( labeledFieldView );
+
+	return dropdownView;
+}

+ 166 - 0
packages/ckeditor5-ui/tests/labeledfield/labeledfieldview.js

@@ -0,0 +1,166 @@
+/**
+ * @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 View from '../../src/view';
+import LabeledFieldView from '../../src/labeledfield/labeledfieldview';
+import LabelView from '../../src/label/labelview';
+
+describe( 'LabeledFieldView', () => {
+	const locale = {};
+
+	let labeledFieldView, view;
+
+	beforeEach( () => {
+		labeledFieldView = new LabeledFieldView( locale, ( labeledFieldView, viewUid, statusUid ) => {
+			view = new View( locale );
+			view.setTemplate( { tag: 'div' } );
+			view.focus = () => {};
+			view.viewUid = viewUid;
+			view.statusUid = statusUid;
+
+			return view;
+		} );
+
+		labeledFieldView.render();
+	} );
+
+	afterEach( () => {
+		labeledFieldView.destroy();
+	} );
+
+	describe( 'constructor()', () => {
+		it( 'should set labeledFieldView#locale', () => {
+			expect( labeledFieldView.locale ).to.deep.equal( locale );
+		} );
+
+		it( 'should set labeledFieldView#field', () => {
+			expect( labeledFieldView.field ).to.equal( view );
+		} );
+
+		it( 'should set labeledFieldView#label', () => {
+			expect( labeledFieldView.label ).to.be.undefined;
+		} );
+
+		it( 'should set labeledFieldView#isEnabled', () => {
+			expect( labeledFieldView.isEnabled ).to.be.true;
+		} );
+
+		it( 'should set labeledFieldView#errorText', () => {
+			expect( labeledFieldView.errorText ).to.be.null;
+		} );
+
+		it( 'should set labeledFieldView#infoText', () => {
+			expect( labeledFieldView.infoText ).to.be.null;
+		} );
+
+		it( 'should set labeledFieldView#class', () => {
+			expect( labeledFieldView.class ).to.be.undefined;
+		} );
+
+		it( 'should create labeledFieldView#labelView', () => {
+			expect( labeledFieldView.labelView ).to.instanceOf( LabelView );
+		} );
+
+		it( 'should create labeledFieldView#statusView', () => {
+			expect( labeledFieldView.statusView ).to.instanceOf( View );
+
+			expect( labeledFieldView.statusView.element.tagName ).to.equal( 'DIV' );
+			expect( labeledFieldView.statusView.element.classList.contains( 'ck' ) ).to.be.true;
+			expect( labeledFieldView.statusView.element.classList.contains( 'ck-labeled-view__status' ) ).to.be.true;
+		} );
+
+		it( 'should allow pairing #view and #labelView by unique id', () => {
+			expect( labeledFieldView.labelView.for ).to.equal( view.viewUid );
+		} );
+
+		it( 'should allow pairing #view and #statusView by unique id', () => {
+			expect( view.statusUid ).to.equal( labeledFieldView.statusView.element.id );
+		} );
+	} );
+
+	describe( 'template', () => {
+		it( 'should have the CSS class', () => {
+			expect( labeledFieldView.element.classList.contains( 'ck' ) ).to.be.true;
+			expect( labeledFieldView.element.classList.contains( 'ck-labeled-view' ) ).to.be.true;
+		} );
+
+		it( 'should have #labeledFieldView', () => {
+			expect( labeledFieldView.template.children[ 0 ] ).to.equal( labeledFieldView.labelView );
+		} );
+
+		it( 'should have #view', () => {
+			expect( labeledFieldView.template.children[ 1 ] ).to.equal( view );
+		} );
+
+		it( 'should have the #statusView container', () => {
+			expect( labeledFieldView.template.children[ 2 ] ).to.equal( labeledFieldView.statusView );
+		} );
+
+		describe( 'DOM bindings', () => {
+			describe( 'class', () => {
+				it( 'should react on labeledFieldView#class', () => {
+					labeledFieldView.class = 'foo';
+					expect( labeledFieldView.element.classList.contains( 'foo' ) ).to.be.true;
+
+					labeledFieldView.class = 'bar';
+					expect( labeledFieldView.element.classList.contains( 'foo' ) ).to.be.false;
+					expect( labeledFieldView.element.classList.contains( 'bar' ) ).to.be.true;
+				} );
+			} );
+
+			describe( 'status container', () => {
+				it( 'should react on labeledFieldView#errorText', () => {
+					const statusElement = labeledFieldView.statusView.element;
+
+					labeledFieldView.errorText = '';
+					expect( statusElement.classList.contains( 'ck-hidden' ) ).to.be.true;
+					expect( statusElement.classList.contains( 'ck-labeled-view__status_error' ) ).to.be.false;
+					expect( statusElement.hasAttribute( 'role' ) ).to.be.false;
+					expect( statusElement.innerHTML ).to.equal( '' );
+
+					labeledFieldView.errorText = 'foo';
+					expect( statusElement.classList.contains( 'ck-hidden' ) ).to.be.false;
+					expect( statusElement.classList.contains( 'ck-labeled-view__status_error' ) ).to.be.true;
+					expect( statusElement.getAttribute( 'role' ) ).to.equal( 'alert' );
+					expect( statusElement.innerHTML ).to.equal( 'foo' );
+				} );
+
+				it( 'should react on labeledFieldView#infoText', () => {
+					const statusElement = labeledFieldView.statusView.element;
+
+					labeledFieldView.infoText = '';
+					expect( statusElement.classList.contains( 'ck-hidden' ) ).to.be.true;
+					expect( statusElement.classList.contains( 'ck-labeled-view__status_error' ) ).to.be.false;
+					expect( statusElement.hasAttribute( 'role' ) ).to.be.false;
+					expect( statusElement.innerHTML ).to.equal( '' );
+
+					labeledFieldView.infoText = 'foo';
+					expect( statusElement.classList.contains( 'ck-hidden' ) ).to.be.false;
+					expect( statusElement.classList.contains( 'ck-labeled-view__status_error' ) ).to.be.false;
+					expect( statusElement.hasAttribute( 'role' ) ).to.be.false;
+					expect( statusElement.innerHTML ).to.equal( 'foo' );
+				} );
+			} );
+		} );
+	} );
+
+	describe( 'binding', () => {
+		it( 'should bind labeledFieldView#label to labeledFieldView.labelView#label', () => {
+			labeledFieldView.label = 'Foo bar';
+
+			expect( labeledFieldView.labelView.text ).to.equal( 'Foo bar' );
+		} );
+	} );
+
+	describe( 'focus()', () => {
+		it( 'should focus the #view in DOM', () => {
+			const spy = sinon.spy( view, 'focus' );
+
+			labeledFieldView.focus();
+
+			sinon.assert.calledOnce( spy );
+		} );
+	} );
+} );

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

@@ -0,0 +1,108 @@
+/**
+ * @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 {
+	createLabeledInputText,
+	createLabeledDropdown
+} from '../../src/labeledfield/utils';
+
+import LabeledFieldView from '../../src/labeledfield/labeledfieldview';
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+import InputTextView from '../../src/inputtext/inputtextview';
+import DropdownView from '../../src/dropdown/dropdownview';
+
+describe( 'LabeledFieldView utils', () => {
+	let locale;
+
+	testUtils.createSinonSandbox();
+
+	beforeEach( () => {
+		locale = { t: val => val };
+	} );
+
+	describe( 'createLabeledInputText()', () => {
+		let labeledFieldView;
+
+		beforeEach( () => {
+			labeledFieldView = new LabeledFieldView( locale, createLabeledInputText );
+		} );
+
+		afterEach( () => {
+			labeledFieldView.destroy();
+		} );
+
+		it( 'should create an InputTextView instance', () => {
+			expect( labeledFieldView.field ).to.be.instanceOf( InputTextView );
+		} );
+
+		it( 'should pass the Locale to the input', () => {
+			expect( labeledFieldView.field.locale ).to.equal( locale );
+		} );
+
+		it( 'should set input #id and #ariaDescribedById', () => {
+			labeledFieldView.render();
+
+			expect( labeledFieldView.field.id ).to.equal( labeledFieldView.labelView.for );
+			expect( labeledFieldView.field.ariaDescribedById ).to.equal( labeledFieldView.statusView.element.id );
+		} );
+
+		it( 'should bind input\'s #isReadOnly to LabeledFieldView#isEnabled', () => {
+			labeledFieldView.isEnabled = true;
+			expect( labeledFieldView.field.isReadOnly ).to.be.false;
+
+			labeledFieldView.isEnabled = false;
+			expect( labeledFieldView.field.isReadOnly ).to.be.true;
+		} );
+
+		it( 'should bind input\'s #hasError to LabeledFieldView#errorText', () => {
+			labeledFieldView.errorText = 'some error';
+			expect( labeledFieldView.field.hasError ).to.be.true;
+
+			labeledFieldView.errorText = null;
+			expect( labeledFieldView.field.hasError ).to.be.false;
+		} );
+
+		it( 'should clean LabeledFieldView#errorText upon input\'s DOM "update" event', () => {
+			labeledFieldView.errorText = 'some error';
+			labeledFieldView.field.fire( 'input' );
+			expect( labeledFieldView.errorText ).to.be.null;
+		} );
+	} );
+
+	describe( 'createLabeledDropdown', () => {
+		let labeledFieldView;
+
+		beforeEach( () => {
+			labeledFieldView = new LabeledFieldView( locale, createLabeledDropdown );
+		} );
+
+		afterEach( () => {
+			labeledFieldView.destroy();
+		} );
+
+		it( 'should create a DropdownView', () => {
+			expect( labeledFieldView.field ).to.be.instanceOf( DropdownView );
+		} );
+
+		it( 'should pass the Locale to the dropdown', () => {
+			expect( labeledFieldView.field.locale ).to.equal( locale );
+		} );
+
+		it( 'should set dropdown\'s #id and #ariaDescribedById', () => {
+			labeledFieldView.render();
+
+			expect( labeledFieldView.field.id ).to.equal( labeledFieldView.labelView.for );
+			expect( labeledFieldView.field.ariaDescribedById ).to.equal( labeledFieldView.statusView.element.id );
+		} );
+
+		it( 'should bind dropdown\'s #isEnabled to the labeled view', () => {
+			labeledFieldView.isEnabled = true;
+			expect( labeledFieldView.field.isEnabled ).to.be.true;
+
+			labeledFieldView.isEnabled = false;
+			expect( labeledFieldView.field.isEnabled ).to.be.false;
+		} );
+	} );
+} );

+ 10 - 0
packages/ckeditor5-ui/theme/components/labeledfield/labeledfieldview.css

@@ -0,0 +1,10 @@
+/*
+ * Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * 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.
+ */