Skip to content

Commit 2efc531

Browse files
committed
fix(material/input): do not override existing aria-describedby value
The form-field notifies controls whenever hints or errors have been displayed. It does this, so that controls like the input can refresh their `aria-describedby` value to point to the errors and hints. This currently has the downside of overriding existing `aria-describedby` values that have been manually set by developers. We could fix this by reading the initial value and merging it with the ids received from the form-field.
1 parent b089abe commit 2efc531

File tree

4 files changed

+30
-13
lines changed

4 files changed

+30
-13
lines changed

src/material-experimental/mdc-input/input.spec.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -474,10 +474,11 @@ describe('MatMdcInput without forms', () => {
474474
fixture.componentInstance.label = 'label';
475475
fixture.detectChanges();
476476

477-
let hint = fixture.debugElement.query(By.css('.mat-mdc-form-field-hint'))!.nativeElement;
478-
let input = fixture.debugElement.query(By.css('input'))!.nativeElement;
477+
const hint = fixture.debugElement.query(By.css('.mat-mdc-form-field-hint'))!.nativeElement;
478+
const input = fixture.debugElement.query(By.css('input'))!.nativeElement;
479+
const hintId = hint.getAttribute('id');
479480

480-
expect(input.getAttribute('aria-describedby')).toBe(hint.getAttribute('id'));
481+
expect(input.getAttribute('aria-describedby')).toBe(`initial ${hintId}`);
481482
}));
482483

483484
it('sets the aria-describedby to the id of the mat-hint', fakeAsync(() => {
@@ -1236,7 +1237,10 @@ class MatInputHintLabel2TestController {
12361237
}
12371238

12381239
@Component({
1239-
template: `<mat-form-field [hintLabel]="label"><input matInput></mat-form-field>`
1240+
template: `
1241+
<mat-form-field [hintLabel]="label">
1242+
<input matInput aria-describedby="initial">
1243+
</mat-form-field>`
12401244
})
12411245
class MatInputHintLabelTestController {
12421246
label: string = '';

src/material/input/input.spec.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -555,10 +555,11 @@ describe('MatInput without forms', () => {
555555
fixture.componentInstance.label = 'label';
556556
fixture.detectChanges();
557557

558-
let hint = fixture.debugElement.query(By.css('.mat-hint'))!.nativeElement;
559-
let input = fixture.debugElement.query(By.css('input'))!.nativeElement;
558+
const hint = fixture.debugElement.query(By.css('.mat-hint'))!.nativeElement;
559+
const input = fixture.debugElement.query(By.css('input'))!.nativeElement;
560+
const hintId = hint.getAttribute('id');
560561

561-
expect(input.getAttribute('aria-describedby')).toBe(hint.getAttribute('id'));
562+
expect(input.getAttribute('aria-describedby')).toBe(`initial ${hintId}`);
562563
}));
563564

564565
it('sets the aria-describedby to the id of the mat-hint', fakeAsync(() => {
@@ -1746,7 +1747,10 @@ class MatInputHintLabel2TestController {
17461747
}
17471748

17481749
@Component({
1749-
template: `<mat-form-field [hintLabel]="label"><input matInput></mat-form-field>`
1750+
template: `
1751+
<mat-form-field [hintLabel]="label">
1752+
<input matInput aria-describedby="initial">
1753+
</mat-form-field>`
17501754
})
17511755
class MatInputHintLabelTestController {
17521756
label: string = '';

src/material/input/input.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion';
1010
import {getSupportedInputTypes, Platform} from '@angular/cdk/platform';
1111
import {AutofillMonitor} from '@angular/cdk/text-field';
1212
import {
13+
Attribute,
1314
Directive,
1415
DoCheck,
1516
ElementRef,
@@ -92,8 +93,9 @@ export class MatInput extends _MatInputMixinBase implements MatFormFieldControl<
9293
protected _uid = `mat-input-${nextUniqueId++}`;
9394
protected _previousNativeValue: any;
9495
private _inputValueAccessor: {value: any};
96+
9597
/** The aria-describedby attribute on the input for improved a11y. */
96-
_ariaDescribedby: string;
98+
_ariaDescribedby: string = this._initialAriaDescribedBy || '';
9799

98100
/** Whether the component is being rendered on the server. */
99101
readonly _isServer: boolean;
@@ -232,7 +234,8 @@ export class MatInput extends _MatInputMixinBase implements MatFormFieldControl<
232234
_defaultErrorStateMatcher: ErrorStateMatcher,
233235
@Optional() @Self() @Inject(MAT_INPUT_VALUE_ACCESSOR) inputValueAccessor: any,
234236
private _autofillMonitor: AutofillMonitor,
235-
ngZone: NgZone) {
237+
ngZone: NgZone,
238+
@Attribute('aria-describedby') private _initialAriaDescribedBy?: string|null) {
236239

237240
super(_defaultErrorStateMatcher, _parentForm, _parentFormGroup, ngControl);
238241

@@ -413,7 +416,13 @@ export class MatInput extends _MatInputMixinBase implements MatFormFieldControl<
413416
* @docs-private
414417
*/
415418
setDescribedByIds(ids: string[]) {
416-
this._ariaDescribedby = ids.join(' ');
419+
let value = ids.join(' ');
420+
// Append the describe-by ids from the form-field without discarding the initial
421+
// `aria-describedby` value that has been specified as static attribute.
422+
if (this._initialAriaDescribedBy != null) {
423+
value = `${this._initialAriaDescribedBy} ${value}`;
424+
}
425+
this._ariaDescribedby = value;
417426
}
418427

419428
/**

tools/public_api_guard/material/input.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export declare class MatInput extends _MatInputMixinBase implements MatFormField
4040
get value(): string;
4141
set value(value: string);
4242
constructor(_elementRef: ElementRef<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>, _platform: Platform,
43-
ngControl: NgControl, _parentForm: NgForm, _parentFormGroup: FormGroupDirective, _defaultErrorStateMatcher: ErrorStateMatcher, inputValueAccessor: any, _autofillMonitor: AutofillMonitor, ngZone: NgZone);
43+
ngControl: NgControl, _parentForm: NgForm, _parentFormGroup: FormGroupDirective, _defaultErrorStateMatcher: ErrorStateMatcher, inputValueAccessor: any, _autofillMonitor: AutofillMonitor, ngZone: NgZone, _initialAriaDescribedBy?: string | null | undefined);
4444
protected _dirtyCheckNativeValue(): void;
4545
_focusChanged(isFocused: boolean): void;
4646
protected _isBadInput(): boolean;
@@ -59,7 +59,7 @@ export declare class MatInput extends _MatInputMixinBase implements MatFormField
5959
static ngAcceptInputType_required: BooleanInput;
6060
static ngAcceptInputType_value: any;
6161
static ɵdir: i0.ɵɵDirectiveDefWithMeta<MatInput, "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", ["matInput"], { "disabled": "disabled"; "id": "id"; "placeholder": "placeholder"; "required": "required"; "type": "type"; "errorStateMatcher": "errorStateMatcher"; "value": "value"; "readonly": "readonly"; }, {}, never>;
62-
static ɵfac: i0.ɵɵFactoryDef<MatInput, [null, null, { optional: true; self: true; }, { optional: true; }, { optional: true; }, null, { optional: true; self: true; }, null, null]>;
62+
static ɵfac: i0.ɵɵFactoryDef<MatInput, [null, null, { optional: true; self: true; }, { optional: true; }, { optional: true; }, null, { optional: true; self: true; }, null, null, { attribute: "aria-describedby"; }]>;
6363
}
6464

6565
export declare class MatInputModule {

0 commit comments

Comments
 (0)