Skip to content

Commit b847535

Browse files
authored
fix(material/datepicker): add label to dialog overlay (#22625)
Fixes that the datepicker overlay element doesn't have a label which causes screen readers to read out "dialog". These changes point the overlay's `aria-labelledby` either to the label of the form field or the `aria-labelledby` of the input.
1 parent 6043957 commit b847535

File tree

6 files changed

+76
-4
lines changed

6 files changed

+76
-4
lines changed

src/material/datepicker/date-range-input.spec.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,23 @@ describe('MatDateRangeInput', () => {
199199
expect(end.nativeElement.getAttribute('aria-labelledby')).toBeFalsy();
200200
});
201201

202+
it('should set aria-labelledby of the overlay to the form field label', fakeAsync(() => {
203+
const fixture = createComponent(StandardRangePicker);
204+
fixture.detectChanges();
205+
206+
const label: HTMLElement = fixture.nativeElement.querySelector('.mat-form-field-label');
207+
expect(label).toBeTruthy();
208+
expect(label.getAttribute('id')).toBeTruthy();
209+
210+
fixture.componentInstance.rangePicker.open();
211+
fixture.detectChanges();
212+
tick();
213+
214+
const popup = document.querySelector('.cdk-overlay-pane')!;
215+
expect(popup).toBeTruthy();
216+
expect(popup.getAttribute('aria-labelledby')).toBe(label.getAttribute('id'));
217+
}));
218+
202219
it('should float the form field label when either input is focused', () => {
203220
const fixture = createComponent(StandardRangePicker);
204221
fixture.detectChanges();

src/material/datepicker/date-range-input.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,11 @@ export class MatDateRangeInput<D> implements MatFormFieldControl<DateRange<D>>,
324324
return this._formField ? this._formField.getConnectedOverlayOrigin() : this._elementRef;
325325
}
326326

327+
/** Gets the ID of an element that should be used a description for the calendar overlay. */
328+
getOverlayLabelId(): string | null {
329+
return this._formField ? this._formField.getLabelId() : null;
330+
}
331+
327332
/** Gets the value that is used to mirror the state input. */
328333
_getInputMirrorValue() {
329334
return this._startInput ? this._startInput.getMirrorValue() : '';

src/material/datepicker/datepicker-base.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@ export interface MatDatepickerControl<D> {
243243
disabled: boolean;
244244
dateFilter: DateFilterFn<D>;
245245
getConnectedOverlayOrigin(): ElementRef;
246+
getOverlayLabelId(): string | null;
246247
stateChanges: Observable<void>;
247248
}
248249

@@ -615,6 +616,7 @@ export abstract class MatDatepickerBase<C extends MatDatepickerControl<D>, S,
615616
this._destroyOverlay();
616617

617618
const isDialog = this.touchUi;
619+
const labelId = this.datepickerInput.getOverlayLabelId();
618620
const portal = new ComponentPortal<MatDatepickerContent<S, D>>(MatDatepickerContent,
619621
this._viewContainerRef);
620622
const overlayRef = this._overlayRef = this._overlay.create(new OverlayConfig({
@@ -628,10 +630,15 @@ export abstract class MatDatepickerBase<C extends MatDatepickerControl<D>, S,
628630
scrollStrategy: isDialog ? this._overlay.scrollStrategies.block() : this._scrollStrategy(),
629631
panelClass: `mat-datepicker-${isDialog ? 'dialog' : 'popup'}`,
630632
}));
631-
overlayRef.overlayElement.setAttribute('role', 'dialog');
633+
const overlayElement = overlayRef.overlayElement;
634+
overlayElement.setAttribute('role', 'dialog');
635+
636+
if (labelId) {
637+
overlayElement.setAttribute('aria-labelledby', labelId);
638+
}
632639

633640
if (isDialog) {
634-
overlayRef.overlayElement.setAttribute('aria-modal', 'true');
641+
overlayElement.setAttribute('aria-modal', 'true');
635642
}
636643

637644
this._getCloseStream(overlayRef).subscribe(event => {

src/material/datepicker/datepicker-input.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ export class MatDatepickerInput<D> extends MatDatepickerInputBase<D | null, D>
134134
elementRef: ElementRef<HTMLInputElement>,
135135
@Optional() dateAdapter: DateAdapter<D>,
136136
@Optional() @Inject(MAT_DATE_FORMATS) dateFormats: MatDateFormats,
137-
@Optional() @Inject(MAT_FORM_FIELD) private _formField: MatFormField) {
137+
@Optional() @Inject(MAT_FORM_FIELD) private _formField?: MatFormField) {
138138
super(elementRef, dateAdapter, dateFormats);
139139
this._validator = Validators.compose(super._getValidators());
140140
}
@@ -147,6 +147,15 @@ export class MatDatepickerInput<D> extends MatDatepickerInputBase<D | null, D>
147147
return this._formField ? this._formField.getConnectedOverlayOrigin() : this._elementRef;
148148
}
149149

150+
/** Gets the ID of an element that should be used a description for the calendar overlay. */
151+
getOverlayLabelId(): string | null {
152+
if (this._formField) {
153+
return this._formField.getLabelId();
154+
}
155+
156+
return this._elementRef.nativeElement.getAttribute('aria-labelledby');
157+
}
158+
150159
/** Returns the palette used by the input's form field, if any. */
151160
getThemePalette(): ThemePalette {
152161
return this._formField ? this._formField.color : undefined;

src/material/datepicker/datepicker.spec.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,22 @@ describe('MatDatepicker', () => {
249249
expect(popup.getAttribute('role')).toBe('dialog');
250250
}));
251251

252+
it('should set aria-labelledby to the one from the input, if not placed inside ' +
253+
'a mat-form-field', fakeAsync(() => {
254+
expect(fixture.nativeElement.querySelector('mat-form-field')).toBeFalsy();
255+
256+
const input: HTMLInputElement = fixture.nativeElement.querySelector('input');
257+
input.setAttribute('aria-labelledby', 'test-label');
258+
259+
testComponent.datepicker.open();
260+
fixture.detectChanges();
261+
flush();
262+
263+
const popup = document.querySelector('.cdk-overlay-pane')!;
264+
expect(popup).toBeTruthy();
265+
expect(popup.getAttribute('aria-labelledby')).toBe('test-label');
266+
}));
267+
252268
it('close should close dialog', fakeAsync(() => {
253269
testComponent.touch = true;
254270
fixture.detectChanges();
@@ -1357,6 +1373,21 @@ describe('MatDatepicker', () => {
13571373
expect(contentEl.classList).toContain('mat-accent');
13581374
expect(contentEl.classList).not.toContain('mat-warn');
13591375
}));
1376+
1377+
it('should set aria-labelledby of the overlay to the form field label', fakeAsync(() => {
1378+
const label: HTMLElement = fixture.nativeElement.querySelector('.mat-form-field-label');
1379+
1380+
expect(label).toBeTruthy();
1381+
expect(label.getAttribute('id')).toBeTruthy();
1382+
1383+
testComponent.datepicker.open();
1384+
fixture.detectChanges();
1385+
flush();
1386+
1387+
const popup = document.querySelector('.cdk-overlay-pane')!;
1388+
expect(popup).toBeTruthy();
1389+
expect(popup.getAttribute('aria-labelledby')).toBe(label.getAttribute('id'));
1390+
}));
13601391
});
13611392

13621393
describe('datepicker with min and max dates and validation', () => {
@@ -2385,6 +2416,7 @@ class DatepickerWithCustomIcon {}
23852416
@Component({
23862417
template: `
23872418
<mat-form-field>
2419+
<mat-label>Pick a date</mat-label>
23882420
<input matInput [matDatepicker]="d">
23892421
<mat-datepicker #d></mat-datepicker>
23902422
</mat-form-field>

tools/public_api_guard/material/datepicker.d.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ export declare class MatDatepickerInput<D> extends MatDatepickerInputBase<D | nu
246246
set max(value: D | null);
247247
get min(): D | null;
248248
set min(value: D | null);
249-
constructor(elementRef: ElementRef<HTMLInputElement>, dateAdapter: DateAdapter<D>, dateFormats: MatDateFormats, _formField: MatFormField);
249+
constructor(elementRef: ElementRef<HTMLInputElement>, dateAdapter: DateAdapter<D>, dateFormats: MatDateFormats, _formField?: MatFormField | undefined);
250250
protected _assignValueToModel(value: D | null): void;
251251
protected _getDateFilter(): DateFilterFn<D | null>;
252252
_getMaxDate(): D | null;
@@ -255,6 +255,7 @@ export declare class MatDatepickerInput<D> extends MatDatepickerInputBase<D | nu
255255
protected _openPopup(): void;
256256
protected _shouldHandleChangeEvent(event: DateSelectionModelChange<D>): boolean;
257257
getConnectedOverlayOrigin(): ElementRef;
258+
getOverlayLabelId(): string | null;
258259
getStartValue(): D | null;
259260
getThemePalette(): ThemePalette;
260261
ngOnDestroy(): void;
@@ -360,6 +361,7 @@ export declare class MatDateRangeInput<D> implements MatFormFieldControl<DateRan
360361
_shouldHideSeparator(): boolean | "" | null;
361362
_updateFocus(origin: FocusOrigin): void;
362363
getConnectedOverlayOrigin(): ElementRef;
364+
getOverlayLabelId(): string | null;
363365
getStartValue(): D | null;
364366
getThemePalette(): ThemePalette;
365367
ngAfterContentInit(): void;

0 commit comments

Comments
 (0)