Skip to content

Commit ba2f2c8

Browse files
committed
fix(material/datepicker): add aria labels to <input/>s for Start/End Date
Give `matDateStart` aria label of "Start Date" and give `matDateEnd` a label of "End Date" by wrapping in a `<label>` element. Apply the aria label of the form field to the `<mat-date-range-input>` component, which has role of group. Previously the placeholder was used to communicate which of the inputs was the start date and which was the end date. Only affects the DOM structure and a11y tree. Does not change the visual appearance. Consider the [Basic date range picker example](https://material.angular.io/components/datepicker/overview#date-range-picker-overview): ``` <mat-form-field appearance="fill"> <mat-label>Enter a date range</mat-label> <mat-date-range-input [rangePicker]="picker"> <input matStartDate placeholder="Start date"> <input matEndDate placeholder="End date"> </mat-date-range-input> ... </mat-form-field> ``` Previously, it would produce an accessibility tree that looks something like this. ``` group "Enter a date range" LabelText StaticText "Enter a date range" textbox "Enter a date range" Textbox "End date" ``` Problems with this approach. 1. Screen reader does not announce "Start Date" right away or not at 2. "Start date"/"End date" come from the placeholder put a label would be more appropriate. With this commit applied, accessibility is consistent between both inputs, and it is easier to tell which of the two is the start and which is the end. ``` group "Enter a date range" LabelText textbox "Start Date" LabelText textbox "End Date" ``` Fixes: #23445
1 parent 744a59d commit ba2f2c8

File tree

4 files changed

+30
-10
lines changed

4 files changed

+30
-10
lines changed

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,21 @@
22
class="mat-date-range-input-container"
33
cdkMonitorSubtreeFocus
44
(cdkFocusChange)="_updateFocus($event)">
5-
<div class="mat-date-range-input-start-wrapper">
5+
<label class="mat-date-range-input-start-wrapper">
66
<ng-content select="input[matStartDate]"></ng-content>
77
<span
88
class="mat-date-range-input-mirror"
99
aria-hidden="true">{{_getInputMirrorValue()}}</span>
10-
</div>
10+
<span class="cdk-visually-hidden">{{_getStartDateLabel()}}</span>
11+
</label>
1112

1213
<span
1314
class="mat-date-range-input-separator"
1415
[class.mat-date-range-input-separator-hidden]="_shouldHideSeparator()">{{separator}}</span>
1516

16-
<div class="mat-date-range-input-end-wrapper">
17+
<label class="mat-date-range-input-end-wrapper">
1718
<ng-content select="input[matEndDate]"></ng-content>
18-
</div>
19+
<span class="cdk-visually-hidden">{{_getEndDateLabel()}}</span>
20+
</label>
1921
</div>
2022

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ describe('MatDateRangeInput', () => {
160160
it('should point the label aria-owns to the id of the start input', () => {
161161
const fixture = createComponent(StandardRangePicker);
162162
fixture.detectChanges();
163-
const label = fixture.nativeElement.querySelector('label');
163+
const label = fixture.nativeElement.querySelector('label.mat-form-field-label');
164164
const start = fixture.componentInstance.start.nativeElement;
165165

166166
expect(start.id).toBeTruthy();
@@ -170,7 +170,7 @@ describe('MatDateRangeInput', () => {
170170
it('should point the range input aria-labelledby to the form field label', () => {
171171
const fixture = createComponent(StandardRangePicker);
172172
fixture.detectChanges();
173-
const labelId = fixture.nativeElement.querySelector('label').id;
173+
const labelId = fixture.nativeElement.querySelector('label.mat-form-field-label').id;
174174
const rangeInput = fixture.nativeElement.querySelector('.mat-date-range-input');
175175

176176
expect(labelId).toBeTruthy();

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import {createMissingDateImplError} from './datepicker-errors';
3939
import {DateFilterFn, dateInputsHaveChanged} from './datepicker-input-base';
4040
import {MatDateRangePickerInput} from './date-range-picker';
4141
import {DateRange, MatDateSelectionModel} from './date-selection-model';
42+
import {MatDatepickerIntl} from './datepicker-intl';
4243

4344
let nextUniqueId = 0;
4445

@@ -253,6 +254,7 @@ export class MatDateRangeInput<D>
253254
constructor(
254255
private _changeDetectorRef: ChangeDetectorRef,
255256
private _elementRef: ElementRef<HTMLElement>,
257+
private _intl: MatDatepickerIntl,
256258
@Optional() @Self() control: ControlContainer,
257259
@Optional() private _dateAdapter: DateAdapter<D>,
258260
@Optional() @Inject(MAT_FORM_FIELD) private _formField?: MatFormField,
@@ -380,7 +382,7 @@ export class MatDateRangeInput<D>
380382
);
381383
}
382384

383-
/** Gets the value for the `aria-labelledby` attribute of the inputs. */
385+
/** Gets the value for the `aria-labelledby` attribute of the group. */
384386
_getAriaLabelledby() {
385387
const formField = this._formField;
386388
return formField && formField._hasFloatingLabel() ? formField._labelId : null;
@@ -392,6 +394,16 @@ export class MatDateRangeInput<D>
392394
this.stateChanges.next();
393395
}
394396

397+
/** Gets the value for the aria label for the start date input. */
398+
_getStartDateLabel() {
399+
return this._intl.startDateLabel;
400+
}
401+
402+
/** Gets the value for the aria label for the end date input. */
403+
_getEndDateLabel() {
404+
return this._intl.endDateLabel;
405+
}
406+
395407
/** Re-runs the validators on the start/end inputs. */
396408
private _revalidate() {
397409
if (this._startInput) {

tools/public_api_guard/material/datepicker.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ export class MatCalendar<D> implements AfterContentInit, AfterViewChecked, OnDes
198198

199199
// @public
200200
export class MatCalendarBody implements OnChanges, OnDestroy, AfterViewChecked {
201-
constructor(_elementRef: ElementRef<HTMLElement>, _ngZone: NgZone);
201+
constructor(_elementRef: ElementRef<HTMLElement>, _ngZone: NgZone, _intl: MatDatepickerIntl);
202202
activeCell: number;
203203
// (undocumented)
204204
readonly activeDateChange: EventEmitter<MatCalendarUserEvent<number>>;
@@ -213,6 +213,7 @@ export class MatCalendarBody implements OnChanges, OnDestroy, AfterViewChecked {
213213
endValue: number;
214214
_firstRowOffset: number;
215215
_focusActiveCell(movePreview?: boolean): void;
216+
_getAriaDescription(value: number): string | null;
216217
_isActiveCell(rowIndex: number, colIndex: number): boolean;
217218
_isComparisonBridgeEnd(value: number, rowIndex: number, colIndex: number): boolean;
218219
_isComparisonBridgeStart(value: number, rowIndex: number, colIndex: number): boolean;
@@ -525,6 +526,7 @@ export class MatDatepickerIntl {
525526
calendarLabel: string;
526527
readonly changes: Subject<void>;
527528
closeCalendarLabel: string;
529+
endDateLabel: string;
528530
formatYearRange(start: string, end: string): string;
529531
nextMonthLabel: string;
530532
nextMultiYearLabel: string;
@@ -533,6 +535,8 @@ export class MatDatepickerIntl {
533535
prevMonthLabel: string;
534536
prevMultiYearLabel: string;
535537
prevYearLabel: string;
538+
startAndEndDateLabel: string;
539+
startDateLabel: string;
536540
switchToMonthViewLabel: string;
537541
switchToMultiYearViewLabel: string;
538542
// (undocumented)
@@ -602,7 +606,7 @@ export class MatDatepickerToggleIcon {
602606

603607
// @public (undocumented)
604608
export class MatDateRangeInput<D> implements MatFormFieldControl<DateRange<D>>, MatDatepickerControl<D>, MatDateRangeInputParent<D>, MatDateRangePickerInput<D>, AfterContentInit, OnChanges, OnDestroy {
605-
constructor(_changeDetectorRef: ChangeDetectorRef, _elementRef: ElementRef<HTMLElement>, control: ControlContainer, _dateAdapter: DateAdapter<D>, _formField?: MatFormField | undefined);
609+
constructor(_changeDetectorRef: ChangeDetectorRef, _elementRef: ElementRef<HTMLElement>, _intl: MatDatepickerIntl, control: ControlContainer, _dateAdapter: DateAdapter<D>, _formField?: MatFormField | undefined);
606610
_ariaDescribedBy: string | null;
607611
comparisonEnd: D | null;
608612
comparisonStart: D | null;
@@ -618,8 +622,10 @@ export class MatDateRangeInput<D> implements MatFormFieldControl<DateRange<D>>,
618622
focused: boolean;
619623
_getAriaLabelledby(): string | null;
620624
getConnectedOverlayOrigin(): ElementRef;
625+
_getEndDateLabel(): string;
621626
_getInputMirrorValue(): string;
622627
getOverlayLabelId(): string | null;
628+
_getStartDateLabel(): string;
623629
getStartValue(): D | null;
624630
getThemePalette(): ThemePalette;
625631
// (undocumented)
@@ -657,7 +663,7 @@ export class MatDateRangeInput<D> implements MatFormFieldControl<DateRange<D>>,
657663
// (undocumented)
658664
static ɵcmp: i0.ɵɵComponentDeclaration<MatDateRangeInput<any>, "mat-date-range-input", ["matDateRangeInput"], { "rangePicker": "rangePicker"; "required": "required"; "dateFilter": "dateFilter"; "min": "min"; "max": "max"; "disabled": "disabled"; "separator": "separator"; "comparisonStart": "comparisonStart"; "comparisonEnd": "comparisonEnd"; }, {}, ["_startInput", "_endInput"], ["input[matStartDate]", "input[matEndDate]"], false>;
659665
// (undocumented)
660-
static ɵfac: i0.ɵɵFactoryDeclaration<MatDateRangeInput<any>, [null, null, { optional: true; self: true; }, { optional: true; }, { optional: true; }]>;
666+
static ɵfac: i0.ɵɵFactoryDeclaration<MatDateRangeInput<any>, [null, null, null, { optional: true; self: true; }, { optional: true; }, { optional: true; }]>;
661667
}
662668

663669
// @public

0 commit comments

Comments
 (0)