Kaynağa Gözat

Allowed LabeledInputView's error to be read by screen readers. Fixed docs.

Aleksander Nowodzinski 7 yıl önce
ebeveyn
işleme
5f6837e879

+ 13 - 3
packages/ckeditor5-ui/src/inputtext/inputtextview.js

@@ -8,7 +8,6 @@
  */
 
 import View from '../view';
-
 import '../../theme/components/inputtext/inputtext.css';
 
 /**
@@ -59,10 +58,19 @@ export default class InputTextView extends View {
 		 * `true` when the field has some error.
 		 *
 		 * @observable
-		 * @member {Boolean} #error
+		 * @member {Boolean} #hasError
 		 */
 		this.set( 'hasError', false );
 
+		/**
+		 * The `id` of the element describing this field, e.g. when it has
+		 * some error, which helps screen readers read the error text.
+		 *
+		 * @observable
+		 * @member {Boolean} #ariaDesribedById
+		 */
+		this.set( 'ariaDesribedById' );
+
 		const bind = this.bindTemplate;
 
 		this.setTemplate( {
@@ -77,7 +85,9 @@ export default class InputTextView extends View {
 				],
 				id: bind.to( 'id' ),
 				placeholder: bind.to( 'placeholder' ),
-				readonly: bind.to( 'isReadOnly' )
+				readonly: bind.to( 'isReadOnly' ),
+				'aria-invalid': bind.if( 'hasError', true ),
+				'aria-describedby': bind.to( 'ariaDesribedById' ),
 			},
 			on: {
 				input: bind.to( 'input' )

+ 50 - 23
packages/ckeditor5-ui/src/labeledinput/labeledinputview.js

@@ -27,7 +27,8 @@ export default class LabeledInputView extends View {
 	constructor( locale, InputView ) {
 		super( locale );
 
-		const id = `ck-input-${ uid() }`;
+		const inputUid = `ck-input-${ uid() }`;
+		const errorUid = `ck-error-${ uid() }`;
 
 		/**
 		 * The text of the label.
@@ -57,7 +58,7 @@ export default class LabeledInputView extends View {
 		 * The error text associated with the field.
 		 *
 		 * @observable
-		 * @member {String} #error
+		 * @member {String|null} #errorText
 		 */
 		this.set( 'errorText', null );
 
@@ -66,14 +67,21 @@ export default class LabeledInputView extends View {
 		 *
 		 * @member {module:ui/label/labelview~LabelView} #labelView
 		 */
-		this.labelView = this._createLabelView( id );
+		this.labelView = this._createLabelView( inputUid );
 
 		/**
 		 * The input view.
 		 *
 		 * @member {module:ui/view~View} #inputView
 		 */
-		this.inputView = this._createInputView( InputView, id );
+		this.inputView = this._createInputView( InputView, inputUid, errorUid );
+
+		/**
+		 * The error view for the {@link #inputView}.
+		 *
+		 * @member {module:ui/view~View} #errorView
+		 */
+		this.errorView = this._createErrorView( errorUid );
 
 		const bind = this.bindTemplate;
 
@@ -89,21 +97,7 @@ export default class LabeledInputView extends View {
 			children: [
 				this.labelView,
 				this.inputView,
-				{
-					tag: 'div',
-					attributes: {
-						class: [
-							'ck',
-							'ck-labeled-input__error',
-							bind.if( 'errorText', 'ck-hidden', value => !value )
-						]
-					},
-					children: [
-						{
-							text: bind.to( 'errorText' )
-						}
-					]
-				}
+				this.errorView
 			]
 		} );
 	}
@@ -129,13 +123,15 @@ export default class LabeledInputView extends View {
 	 *
 	 * @private
 	 * @param {Function} InputView Input view constructor.
-	 * @param {String} id Unique id to set as inputView#id attribute.
+	 * @param {String} inputUid Unique id to set as inputView#id attribute.
+	 * @param {String} errorUid Unique id of the error for the input's `aria-describedby` attribute.
 	 * @returns {module:ui/inputtext/inputtextview~InputTextView}
 	 */
-	_createInputView( InputView, id ) {
-		const inputView = new InputView( this.locale );
+	_createInputView( InputView, inputUid, errorUid ) {
+		const inputView = new InputView( this.locale, errorUid );
 
-		inputView.id = id;
+		inputView.id = inputUid;
+		inputView.ariaDesribedById = errorUid;
 		inputView.bind( 'value' ).to( this );
 		inputView.bind( 'isReadOnly' ).to( this );
 		inputView.bind( 'hasError' ).to( this, 'errorText', value => !!value );
@@ -149,6 +145,37 @@ export default class LabeledInputView extends View {
 		return inputView;
 	}
 
+	/**
+	 * Creates the error view instance.
+	 *
+	 * @private
+	 * @param {String} errorUid Unique id of the error, shared with the input's `aria-describedby` attribute.
+	 * @returns {module:ui/view~View}
+	 */
+	_createErrorView( errorUid ) {
+		const errorView = new View( this.locale );
+		const bind = this.bindTemplate;
+
+		errorView.setTemplate( 				{
+			tag: 'div',
+			attributes: {
+				class: [
+					'ck',
+					'ck-labeled-input__error',
+					bind.if( 'errorText', 'ck-hidden', value => !value )
+				],
+				id: errorUid
+			},
+			children: [
+				{
+					text: bind.to( 'errorText' )
+				}
+			]
+		} );
+
+		return errorView;
+	}
+
 	/**
 	 * Moves the focus to the input and selects the value.
 	 */

+ 23 - 2
packages/ckeditor5-ui/tests/inputtext/inputtextview.js

@@ -8,9 +8,10 @@
 import InputTextView from '../../src/inputtext/inputtextview';
 
 describe( 'InputTextView', () => {
-	let view;
+	let view, ariaDesribedById;
 
 	beforeEach( () => {
+		ariaDesribedById = 'ck-error-1234567890';
 		view = new InputTextView();
 
 		view.render();
@@ -101,7 +102,7 @@ describe( 'InputTextView', () => {
 			} );
 		} );
 
-		describe( 'hasErrors', () => {
+		describe( 'class', () => {
 			it( 'should react on view#hasErrors', () => {
 				expect( view.element.classList.contains( 'ck-error' ) ).to.be.false;
 
@@ -111,6 +112,26 @@ describe( 'InputTextView', () => {
 			} );
 		} );
 
+		describe( 'aria-invalid', () => {
+			it( 'should react on view#hasError', () => {
+				expect( view.element.getAttribute( 'aria-invalid' ) ).to.be.null;
+
+				view.hasError = true;
+
+				expect( view.element.getAttribute( 'aria-invalid' ) ).to.equal( 'true' );
+			} );
+		} );
+
+		describe( 'aria-describedby', () => {
+			it( 'should react on view#hasError', () => {
+				expect( view.element.getAttribute( 'aria-describedby' ) ).to.be.null;
+
+				view.ariaDesribedById = ariaDesribedById;
+
+				expect( view.element.getAttribute( 'aria-describedby' ) ).to.equal( ariaDesribedById );
+			} );
+		} );
+
 		describe( 'input event', () => {
 			it( 'triggers view#input', () => {
 				const spy = sinon.spy();

+ 16 - 7
packages/ckeditor5-ui/tests/labeledinput/labeledinputview.js

@@ -3,6 +3,7 @@
  * For licensing, see LICENSE.md.
  */
 
+import View from '../../src/view';
 import LabeledInputView from '../../src/labeledinput/labeledinputview';
 import InputView from '../../src/inputtext/inputtextview';
 import LabelView from '../../src/label/labelview';
@@ -35,8 +36,20 @@ describe( 'LabeledInputView', () => {
 			expect( view.labelView ).to.instanceOf( LabelView );
 		} );
 
-		it( 'should pair inputView and labelView by unique id', () => {
-			expect( view.labelView.for ).to.equal( view.inputView.id ).to.ok;
+		it( 'should create view#errorView', () => {
+			expect( view.errorView ).to.instanceOf( View );
+
+			expect( view.errorView.element.tagName ).to.equal( 'DIV' );
+			expect( view.errorView.element.classList.contains( 'ck' ) ).to.be.true;
+			expect( view.errorView.element.classList.contains( 'ck-labeled-input__error' ) ).to.be.true;
+		} );
+
+		it( 'should pair #inputView and #labelView by unique id', () => {
+			expect( view.labelView.for ).to.equal( view.inputView.id );
+		} );
+
+		it( 'should pair #inputView and #errorView by unique id', () => {
+			expect( view.inputView.ariaDesribedById ).to.equal( view.errorView.element.id );
 		} );
 	} );
 
@@ -55,11 +68,7 @@ describe( 'LabeledInputView', () => {
 		} );
 
 		it( 'should have the error container', () => {
-			const errorContainer = view.element.lastChild;
-
-			expect( errorContainer.tagName ).to.equal( 'DIV' );
-			expect( errorContainer.classList.contains( 'ck' ) ).to.be.true;
-			expect( errorContainer.classList.contains( 'ck-labeled-input__error' ) ).to.be.true;
+			expect( view.template.children[ 2 ] ).to.equal( view.errorView );
 		} );
 
 		describe( 'DOM bindings', () => {