Skip to content
This repository has been archived by the owner on Jun 26, 2020. It is now read-only.

i/6049: Created the LabeledView class #537

Merged
merged 13 commits into from
Jan 24, 2020
12 changes: 11 additions & 1 deletion src/dropdown/dropdownview.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,14 @@ export default class DropdownView extends View {
*/
this.set( 'class' );

/**
* (Optional) The `id` attribute of the dropdown (i.e. to pair with a `<label>` element).
*
* @observable
* @member {String} #id
*/
this.set( 'id' );

/**
* The position of the panel, relative to the dropdown.
*
Expand Down Expand Up @@ -176,7 +184,9 @@ export default class DropdownView extends View {
'ck-dropdown',
bind.to( 'class' ),
bind.if( 'isEnabled', 'ck-disabled', value => !value )
]
],
id: bind.to( 'id' ),
'aria-describedby': bind.to( 'ariaDescribedById' )
},

children: [
Expand Down
10 changes: 3 additions & 7 deletions src/editorui/boxed/boxededitoruiview.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@

import EditorUIView from '../../editorui/editoruiview';
import LabelView from '../../label/labelview';
import uid from '@ckeditor/ckeditor5-utils/src/uid';

/**
* The boxed editor UI view class. This class represents an editor interface
Expand All @@ -26,8 +25,6 @@ export default class BoxedEditorUIView extends EditorUIView {
constructor( locale ) {
super( locale );

const ariaLabelUid = uid();

/**
* Collection of the child views located in the top (`.ck-editor__top`)
* area of the UI.
Expand All @@ -53,7 +50,7 @@ export default class BoxedEditorUIView extends EditorUIView {
* @readonly
* @member {module:ui/view~View} #_voiceLabelView
*/
this._voiceLabelView = this._createVoiceLabel( ariaLabelUid );
this._voiceLabelView = this._createVoiceLabel();

this.setTemplate( {
tag: 'div',
Expand All @@ -68,7 +65,7 @@ export default class BoxedEditorUIView extends EditorUIView {
role: 'application',
dir: locale.uiLanguageDirection,
lang: locale.uiLanguage,
'aria-labelledby': `ck-editor__aria-label_${ ariaLabelUid }`
'aria-labelledby': this._voiceLabelView.id
},

children: [
Expand Down Expand Up @@ -106,15 +103,14 @@ export default class BoxedEditorUIView extends EditorUIView {
* @private
* @returns {module:ui/label/labelview~LabelView}
*/
_createVoiceLabel( ariaLabelUid ) {
_createVoiceLabel() {
const t = this.t;
const voiceLabel = new LabelView();

voiceLabel.text = t( 'Rich Text Editor' );

voiceLabel.extendTemplate( {
attributes: {
id: `ck-editor__aria-label_${ ariaLabelUid }`,
class: 'ck-voice-label'
}
} );
Expand Down
10 changes: 10 additions & 0 deletions src/label/labelview.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/

import View from '../view';
import uid from '@ckeditor/ckeditor5-utils/src/uid';

import '../../theme/components/label/label.css';

Expand Down Expand Up @@ -39,6 +40,14 @@ export default class LabelView extends View {
*/
this.set( 'for' );

/**
* An unique id of the label. It can be used by other UI components to reference
* the label, for instance, using the `aria-describedby` DOM attribute.
*
* @member {String} #id
*/
this.id = `ck-editor__label_${ uid() }`;

const bind = this.bindTemplate;

this.setTemplate( {
Expand All @@ -48,6 +57,7 @@ export default class LabelView extends View {
'ck',
'ck-label'
],
id: this.id,
for: bind.to( 'for' )
},
children: [
Expand Down
241 changes: 241 additions & 0 deletions src/labeledview/labeledview.js
Original file line number Diff line number Diff line change
@@ -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/labeledview/labeledview
*/

import View from '../view';
import uid from '@ckeditor/ckeditor5-utils/src/uid';
import LabelView from '../label/labelview';
import '../../theme/components/labeledview/labeledview.css';

/**
* The labeled 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 LabeledView( locale, ( labeledView, viewUid, statusUid ) => {
* const inputView = new InputTextView( labeledView.locale );
*
* inputView.set( {
* id: viewUid,
* ariaDescribedById: statusUid
* } );
*
* inputView.bind( 'isReadOnly' ).to( labeledView, 'isEnabled', value => !value );
* inputView.bind( 'hasError' ).to( labeledView, '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/labeledview/utils} to discover ready–to–use labeled input helpers for common
* UI components.
*
* @extends module:ui/view~View
*/
export default class LabeledView extends View {
/**
* Creates an instance of the labeled 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 `LabeledView` to allow binding observable properties,
* * an UID string that connects the {@link #labelView label} and the labeled view in DOM,
* * an UID string that connects the {@link #statusView status} and the labeled view in DOM.
*/
constructor( locale, viewCreator ) {
super( locale );

const viewUid = `ck-labeled-view-${ uid() }`;
const statusUid = `ck-labeled-view-status-${ uid() }`;

/**
* The view that gets labeled.
*
* @member {module:ui/view~View} #view
*/
this.view = 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 #view} 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 #view} `true`.
*
* @observable
* @member {String|null} #errorText
*/
this.set( 'errorText', null );

/**
* The additional information text displayed next to the {@link #view} 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 #view}. 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.view,
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 #view}. See {@link #_statusText}.
*
* @private
* @param {String} statusUid Unique id of the status, shared with the {@link #view 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 #view}.
*/
focus() {
this.view.focus();
}
}
Loading