Skip to content

Commit

Permalink
fix(material/datepicker): add label to dialog overlay (#22625)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
crisbeto authored May 7, 2021
1 parent 6043957 commit b847535
Show file tree
Hide file tree
Showing 6 changed files with 76 additions and 4 deletions.
17 changes: 17 additions & 0 deletions src/material/datepicker/date-range-input.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,23 @@ describe('MatDateRangeInput', () => {
expect(end.nativeElement.getAttribute('aria-labelledby')).toBeFalsy();
});

it('should set aria-labelledby of the overlay to the form field label', fakeAsync(() => {
const fixture = createComponent(StandardRangePicker);
fixture.detectChanges();

const label: HTMLElement = fixture.nativeElement.querySelector('.mat-form-field-label');
expect(label).toBeTruthy();
expect(label.getAttribute('id')).toBeTruthy();

fixture.componentInstance.rangePicker.open();
fixture.detectChanges();
tick();

const popup = document.querySelector('.cdk-overlay-pane')!;
expect(popup).toBeTruthy();
expect(popup.getAttribute('aria-labelledby')).toBe(label.getAttribute('id'));
}));

it('should float the form field label when either input is focused', () => {
const fixture = createComponent(StandardRangePicker);
fixture.detectChanges();
Expand Down
5 changes: 5 additions & 0 deletions src/material/datepicker/date-range-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,11 @@ export class MatDateRangeInput<D> implements MatFormFieldControl<DateRange<D>>,
return this._formField ? this._formField.getConnectedOverlayOrigin() : this._elementRef;
}

/** Gets the ID of an element that should be used a description for the calendar overlay. */
getOverlayLabelId(): string | null {
return this._formField ? this._formField.getLabelId() : null;
}

/** Gets the value that is used to mirror the state input. */
_getInputMirrorValue() {
return this._startInput ? this._startInput.getMirrorValue() : '';
Expand Down
11 changes: 9 additions & 2 deletions src/material/datepicker/datepicker-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ export interface MatDatepickerControl<D> {
disabled: boolean;
dateFilter: DateFilterFn<D>;
getConnectedOverlayOrigin(): ElementRef;
getOverlayLabelId(): string | null;
stateChanges: Observable<void>;
}

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

const isDialog = this.touchUi;
const labelId = this.datepickerInput.getOverlayLabelId();
const portal = new ComponentPortal<MatDatepickerContent<S, D>>(MatDatepickerContent,
this._viewContainerRef);
const overlayRef = this._overlayRef = this._overlay.create(new OverlayConfig({
Expand All @@ -628,10 +630,15 @@ export abstract class MatDatepickerBase<C extends MatDatepickerControl<D>, S,
scrollStrategy: isDialog ? this._overlay.scrollStrategies.block() : this._scrollStrategy(),
panelClass: `mat-datepicker-${isDialog ? 'dialog' : 'popup'}`,
}));
overlayRef.overlayElement.setAttribute('role', 'dialog');
const overlayElement = overlayRef.overlayElement;
overlayElement.setAttribute('role', 'dialog');

if (labelId) {
overlayElement.setAttribute('aria-labelledby', labelId);
}

if (isDialog) {
overlayRef.overlayElement.setAttribute('aria-modal', 'true');
overlayElement.setAttribute('aria-modal', 'true');
}

this._getCloseStream(overlayRef).subscribe(event => {
Expand Down
11 changes: 10 additions & 1 deletion src/material/datepicker/datepicker-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ export class MatDatepickerInput<D> extends MatDatepickerInputBase<D | null, D>
elementRef: ElementRef<HTMLInputElement>,
@Optional() dateAdapter: DateAdapter<D>,
@Optional() @Inject(MAT_DATE_FORMATS) dateFormats: MatDateFormats,
@Optional() @Inject(MAT_FORM_FIELD) private _formField: MatFormField) {
@Optional() @Inject(MAT_FORM_FIELD) private _formField?: MatFormField) {
super(elementRef, dateAdapter, dateFormats);
this._validator = Validators.compose(super._getValidators());
}
Expand All @@ -147,6 +147,15 @@ export class MatDatepickerInput<D> extends MatDatepickerInputBase<D | null, D>
return this._formField ? this._formField.getConnectedOverlayOrigin() : this._elementRef;
}

/** Gets the ID of an element that should be used a description for the calendar overlay. */
getOverlayLabelId(): string | null {
if (this._formField) {
return this._formField.getLabelId();
}

return this._elementRef.nativeElement.getAttribute('aria-labelledby');
}

/** Returns the palette used by the input's form field, if any. */
getThemePalette(): ThemePalette {
return this._formField ? this._formField.color : undefined;
Expand Down
32 changes: 32 additions & 0 deletions src/material/datepicker/datepicker.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,22 @@ describe('MatDatepicker', () => {
expect(popup.getAttribute('role')).toBe('dialog');
}));

it('should set aria-labelledby to the one from the input, if not placed inside ' +
'a mat-form-field', fakeAsync(() => {
expect(fixture.nativeElement.querySelector('mat-form-field')).toBeFalsy();

const input: HTMLInputElement = fixture.nativeElement.querySelector('input');
input.setAttribute('aria-labelledby', 'test-label');

testComponent.datepicker.open();
fixture.detectChanges();
flush();

const popup = document.querySelector('.cdk-overlay-pane')!;
expect(popup).toBeTruthy();
expect(popup.getAttribute('aria-labelledby')).toBe('test-label');
}));

it('close should close dialog', fakeAsync(() => {
testComponent.touch = true;
fixture.detectChanges();
Expand Down Expand Up @@ -1357,6 +1373,21 @@ describe('MatDatepicker', () => {
expect(contentEl.classList).toContain('mat-accent');
expect(contentEl.classList).not.toContain('mat-warn');
}));

it('should set aria-labelledby of the overlay to the form field label', fakeAsync(() => {
const label: HTMLElement = fixture.nativeElement.querySelector('.mat-form-field-label');

expect(label).toBeTruthy();
expect(label.getAttribute('id')).toBeTruthy();

testComponent.datepicker.open();
fixture.detectChanges();
flush();

const popup = document.querySelector('.cdk-overlay-pane')!;
expect(popup).toBeTruthy();
expect(popup.getAttribute('aria-labelledby')).toBe(label.getAttribute('id'));
}));
});

describe('datepicker with min and max dates and validation', () => {
Expand Down Expand Up @@ -2385,6 +2416,7 @@ class DatepickerWithCustomIcon {}
@Component({
template: `
<mat-form-field>
<mat-label>Pick a date</mat-label>
<input matInput [matDatepicker]="d">
<mat-datepicker #d></mat-datepicker>
</mat-form-field>
Expand Down
4 changes: 3 additions & 1 deletion tools/public_api_guard/material/datepicker.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ export declare class MatDatepickerInput<D> extends MatDatepickerInputBase<D | nu
set max(value: D | null);
get min(): D | null;
set min(value: D | null);
constructor(elementRef: ElementRef<HTMLInputElement>, dateAdapter: DateAdapter<D>, dateFormats: MatDateFormats, _formField: MatFormField);
constructor(elementRef: ElementRef<HTMLInputElement>, dateAdapter: DateAdapter<D>, dateFormats: MatDateFormats, _formField?: MatFormField | undefined);
protected _assignValueToModel(value: D | null): void;
protected _getDateFilter(): DateFilterFn<D | null>;
_getMaxDate(): D | null;
Expand All @@ -255,6 +255,7 @@ export declare class MatDatepickerInput<D> extends MatDatepickerInputBase<D | nu
protected _openPopup(): void;
protected _shouldHandleChangeEvent(event: DateSelectionModelChange<D>): boolean;
getConnectedOverlayOrigin(): ElementRef;
getOverlayLabelId(): string | null;
getStartValue(): D | null;
getThemePalette(): ThemePalette;
ngOnDestroy(): void;
Expand Down Expand Up @@ -360,6 +361,7 @@ export declare class MatDateRangeInput<D> implements MatFormFieldControl<DateRan
_shouldHideSeparator(): boolean | "" | null;
_updateFocus(origin: FocusOrigin): void;
getConnectedOverlayOrigin(): ElementRef;
getOverlayLabelId(): string | null;
getStartValue(): D | null;
getThemePalette(): ThemePalette;
ngAfterContentInit(): void;
Expand Down

0 comments on commit b847535

Please sign in to comment.