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

Commit ec39406

Browse files
authored
Merge pull request #537 from ckeditor/i/6049
Feature: Created the `LabeledView` class (see ckeditor/ckeditor5-table#227). Also added `id` properties to the `DropdownView` and `LabelView` for compatibility with the `LabeledView`.
2 parents 6b3c558 + 19a6af1 commit ec39406

File tree

11 files changed

+666
-9
lines changed

11 files changed

+666
-9
lines changed

src/dropdown/dropdownview.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,14 @@ export default class DropdownView extends View {
130130
*/
131131
this.set( 'class' );
132132

133+
/**
134+
* (Optional) The `id` attribute of the dropdown (i.e. to pair with a `<label>` element).
135+
*
136+
* @observable
137+
* @member {String} #id
138+
*/
139+
this.set( 'id' );
140+
133141
/**
134142
* The position of the panel, relative to the dropdown.
135143
*
@@ -176,7 +184,9 @@ export default class DropdownView extends View {
176184
'ck-dropdown',
177185
bind.to( 'class' ),
178186
bind.if( 'isEnabled', 'ck-disabled', value => !value )
179-
]
187+
],
188+
id: bind.to( 'id' ),
189+
'aria-describedby': bind.to( 'ariaDescribedById' )
180190
},
181191

182192
children: [

src/editorui/boxed/boxededitoruiview.js

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99

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

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

29-
const ariaLabelUid = uid();
30-
3128
/**
3229
* Collection of the child views located in the top (`.ck-editor__top`)
3330
* area of the UI.
@@ -53,7 +50,7 @@ export default class BoxedEditorUIView extends EditorUIView {
5350
* @readonly
5451
* @member {module:ui/view~View} #_voiceLabelView
5552
*/
56-
this._voiceLabelView = this._createVoiceLabel( ariaLabelUid );
53+
this._voiceLabelView = this._createVoiceLabel();
5754

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

7471
children: [
@@ -106,15 +103,14 @@ export default class BoxedEditorUIView extends EditorUIView {
106103
* @private
107104
* @returns {module:ui/label/labelview~LabelView}
108105
*/
109-
_createVoiceLabel( ariaLabelUid ) {
106+
_createVoiceLabel() {
110107
const t = this.t;
111108
const voiceLabel = new LabelView();
112109

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

115112
voiceLabel.extendTemplate( {
116113
attributes: {
117-
id: `ck-editor__aria-label_${ ariaLabelUid }`,
118114
class: 'ck-voice-label'
119115
}
120116
} );

src/label/labelview.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*/
99

1010
import View from '../view';
11+
import uid from '@ckeditor/ckeditor5-utils/src/uid';
1112

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

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

43+
/**
44+
* An unique id of the label. It can be used by other UI components to reference
45+
* the label, for instance, using the `aria-describedby` DOM attribute.
46+
*
47+
* @member {String} #id
48+
*/
49+
this.id = `ck-editor__label_${ uid() }`;
50+
4251
const bind = this.bindTemplate;
4352

4453
this.setTemplate( {
@@ -48,6 +57,7 @@ export default class LabelView extends View {
4857
'ck',
4958
'ck-label'
5059
],
60+
id: this.id,
5161
for: bind.to( 'for' )
5262
},
5363
children: [

src/labeledview/labeledview.js

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
/**
2+
* @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
3+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4+
*/
5+
6+
/**
7+
* @module ui/labeledview/labeledview
8+
*/
9+
10+
import View from '../view';
11+
import uid from '@ckeditor/ckeditor5-utils/src/uid';
12+
import LabelView from '../label/labelview';
13+
import '../../theme/components/labeledview/labeledview.css';
14+
15+
/**
16+
* The labeled view class. It can be used to enhance any view with the following features:
17+
*
18+
* * a label,
19+
* * (optional) an error message,
20+
* * (optional) an info (status) text,
21+
*
22+
* all bound logically by proper DOM attributes for UX and accessibility. It also provides an interface
23+
* (e.g. observable properties) that allows controlling those additional features.
24+
*
25+
* The constructor of this class requires a callback that returns a view to be labeled. The callback
26+
* is called with unique ids that allow binding of DOM properties:
27+
*
28+
* const labeledInputView = new LabeledView( locale, ( labeledView, viewUid, statusUid ) => {
29+
* const inputView = new InputTextView( labeledView.locale );
30+
*
31+
* inputView.set( {
32+
* id: viewUid,
33+
* ariaDescribedById: statusUid
34+
* } );
35+
*
36+
* inputView.bind( 'isReadOnly' ).to( labeledView, 'isEnabled', value => !value );
37+
* inputView.bind( 'hasError' ).to( labeledView, 'errorText', value => !!value );
38+
*
39+
* return inputView;
40+
* } );
41+
*
42+
* labeledInputView.label = 'User name';
43+
* labeledInputView.infoText = 'Full name like for instance, John Doe.';
44+
* labeledInputView.render();
45+
*
46+
* document.body.append( labeledInputView.element );
47+
*
48+
* See {@link module:ui/labeledview/utils} to discover ready–to–use labeled input helpers for common
49+
* UI components.
50+
*
51+
* @extends module:ui/view~View
52+
*/
53+
export default class LabeledView extends View {
54+
/**
55+
* Creates an instance of the labeled view class using a provided creator function
56+
* that provides the view to be labeled.
57+
*
58+
* @param {module:utils/locale~Locale} locale The locale instance.
59+
* @param {Function} viewCreator A function that returns a {@link module:ui/view~View}
60+
* that will be labeled. The following arguments are passed to the creator function:
61+
*
62+
* * an instance of the `LabeledView` to allow binding observable properties,
63+
* * an UID string that connects the {@link #labelView label} and the labeled view in DOM,
64+
* * an UID string that connects the {@link #statusView status} and the labeled view in DOM.
65+
*/
66+
constructor( locale, viewCreator ) {
67+
super( locale );
68+
69+
const viewUid = `ck-labeled-view-${ uid() }`;
70+
const statusUid = `ck-labeled-view-status-${ uid() }`;
71+
72+
/**
73+
* The view that gets labeled.
74+
*
75+
* @member {module:ui/view~View} #view
76+
*/
77+
this.view = viewCreator( this, viewUid, statusUid );
78+
79+
/**
80+
* The text of the label.
81+
*
82+
* @observable
83+
* @member {String} #label
84+
*/
85+
this.set( 'label' );
86+
87+
/**
88+
* Controls whether the component is in read-only mode.
89+
*
90+
* @observable
91+
* @member {Boolean} #isEnabled
92+
*/
93+
this.set( 'isEnabled', true );
94+
95+
/**
96+
* The validation error text. When set, it will be displayed
97+
* next to the {@link #view} as a typical validation error message.
98+
* Set it to `null` to hide the message.
99+
*
100+
* **Note:** Setting this property to anything but `null` will automatically
101+
* make the `hasError` of the {@link #view} `true`.
102+
*
103+
* @observable
104+
* @member {String|null} #errorText
105+
*/
106+
this.set( 'errorText', null );
107+
108+
/**
109+
* The additional information text displayed next to the {@link #view} which can
110+
* be used to inform the user about its purpose, provide help or hints.
111+
*
112+
* Set it to `null` to hide the message.
113+
*
114+
* **Note:** This text will be displayed in the same place as {@link #errorText} but the
115+
* latter always takes precedence: if the {@link #errorText} is set, it replaces
116+
* {@link #infoText}.
117+
*
118+
* @observable
119+
* @member {String|null} #infoText
120+
*/
121+
this.set( 'infoText', null );
122+
123+
/**
124+
* (Optional) The additional CSS class set on the dropdown {@link #element}.
125+
*
126+
* @observable
127+
* @member {String} #class
128+
*/
129+
this.set( 'class' );
130+
131+
/**
132+
* The label view instance that describes the entire view.
133+
*
134+
* @member {module:ui/label/labelview~LabelView} #labelView
135+
*/
136+
this.labelView = this._createLabelView( viewUid );
137+
138+
/**
139+
* The status view for the {@link #view}. It displays {@link #errorText} and
140+
* {@link #infoText}.
141+
*
142+
* @member {module:ui/view~View} #statusView
143+
*/
144+
this.statusView = this._createStatusView( statusUid );
145+
146+
/**
147+
* The combined status text made of {@link #errorText} and {@link #infoText}.
148+
* Note that when present, {@link #errorText} always takes precedence in the
149+
* status.
150+
*
151+
* @see #errorText
152+
* @see #infoText
153+
* @see #statusView
154+
* @private
155+
* @observable
156+
* @member {String|null} #_statusText
157+
*/
158+
this.bind( '_statusText' ).to(
159+
this, 'errorText',
160+
this, 'infoText',
161+
( errorText, infoText ) => errorText || infoText
162+
);
163+
164+
const bind = this.bindTemplate;
165+
166+
this.setTemplate( {
167+
tag: 'div',
168+
attributes: {
169+
class: [
170+
'ck',
171+
'ck-labeled-view',
172+
bind.to( 'class' ),
173+
bind.if( 'isEnabled', 'ck-disabled', value => !value )
174+
]
175+
},
176+
children: [
177+
this.labelView,
178+
this.view,
179+
this.statusView
180+
]
181+
} );
182+
}
183+
184+
/**
185+
* Creates label view class instance and bind with view.
186+
*
187+
* @private
188+
* @param {String} id Unique id to set as labelView#for attribute.
189+
* @returns {module:ui/label/labelview~LabelView}
190+
*/
191+
_createLabelView( id ) {
192+
const labelView = new LabelView( this.locale );
193+
194+
labelView.for = id;
195+
labelView.bind( 'text' ).to( this, 'label' );
196+
197+
return labelView;
198+
}
199+
200+
/**
201+
* Creates the status view instance. It displays {@link #errorText} and {@link #infoText}
202+
* next to the {@link #view}. See {@link #_statusText}.
203+
*
204+
* @private
205+
* @param {String} statusUid Unique id of the status, shared with the {@link #view view's}
206+
* `aria-describedby` attribute.
207+
* @returns {module:ui/view~View}
208+
*/
209+
_createStatusView( statusUid ) {
210+
const statusView = new View( this.locale );
211+
const bind = this.bindTemplate;
212+
213+
statusView.setTemplate( {
214+
tag: 'div',
215+
attributes: {
216+
class: [
217+
'ck',
218+
'ck-labeled-view__status',
219+
bind.if( 'errorText', 'ck-labeled-view__status_error' ),
220+
bind.if( '_statusText', 'ck-hidden', value => !value )
221+
],
222+
id: statusUid,
223+
role: bind.if( 'errorText', 'alert' )
224+
},
225+
children: [
226+
{
227+
text: bind.to( '_statusText' )
228+
}
229+
]
230+
} );
231+
232+
return statusView;
233+
}
234+
235+
/**
236+
* Focuses the {@link #view}.
237+
*/
238+
focus() {
239+
this.view.focus();
240+
}
241+
}

0 commit comments

Comments
 (0)