+
+
+
diff --git a/src/material/datepicker/calendar-body.scss b/src/material/datepicker/calendar-body.scss
index 9dce40071d02..2a4135dea210 100644
--- a/src/material/datepicker/calendar-body.scss
+++ b/src/material/datepicker/calendar-body.scss
@@ -33,6 +33,11 @@ $calendar-range-end-body-cell-size:
padding-right: $calendar-body-label-side-padding;
}
+// Label that is not rendered and removed from the accessibility tree.
+.mat-calendar-body-hidden-label {
+ display: none;
+}
+
.mat-calendar-body-cell-container {
position: relative;
height: 0;
diff --git a/src/material/datepicker/calendar-body.ts b/src/material/datepicker/calendar-body.ts
index 39bdc4101822..ceeaef2516c9 100644
--- a/src/material/datepicker/calendar-body.ts
+++ b/src/material/datepicker/calendar-body.ts
@@ -53,6 +53,8 @@ export interface MatCalendarUserEvent
{
event: Event;
}
+let calendarBodyId = 1;
+
/**
* An internal component used to display calendar data in a table.
* @docs-private
@@ -132,6 +134,12 @@ export class MatCalendarBody implements OnChanges, OnDestroy, AfterViewChecked {
/** End of the preview range. */
@Input() previewEnd: number | null = null;
+ /** ARIA Accessible name of the `` */
+ @Input() startDateAccessibleName: string | null;
+
+ /** ARIA Accessible name of the `` */
+ @Input() endDateAccessibleName: string | null;
+
/** Emits when a new value is selected. */
@Output() readonly selectedValueChange = new EventEmitter>();
@@ -356,6 +364,22 @@ export class MatCalendarBody implements OnChanges, OnDestroy, AfterViewChecked {
return isInRange(value, this.previewStart, this.previewEnd, this.isRange);
}
+ /** Gets ids of aria descriptions for the start and end of a date range. */
+ _getDescribedby(value: number): string | null {
+ if (!this.isRange) {
+ return null;
+ }
+
+ if (this.startValue === value && this.endValue === value) {
+ return `${this._startDateLabelId} ${this._endDateLabelId}`;
+ } else if (this.startValue === value) {
+ return this._startDateLabelId;
+ } else if (this.endValue === value) {
+ return this._endDateLabelId;
+ }
+ return null;
+ }
+
/**
* Event handler for when the user enters an element
* inside the calendar body (e.g. by hovering in or focus).
@@ -413,6 +437,12 @@ export class MatCalendarBody implements OnChanges, OnDestroy, AfterViewChecked {
return null;
}
+
+ private _id = `mat-calendar-body-${calendarBodyId++}`;
+
+ _startDateLabelId = `${this._id}-start-date`;
+
+ _endDateLabelId = `${this._id}-end-date`;
}
/** Checks whether a node is a table cell element. */
diff --git a/src/material/datepicker/calendar.html b/src/material/datepicker/calendar.html
index 42f5a91c66b1..1072d92384cc 100644
--- a/src/material/datepicker/calendar.html
+++ b/src/material/datepicker/calendar.html
@@ -11,6 +11,8 @@
[dateClass]="dateClass"
[comparisonStart]="comparisonStart"
[comparisonEnd]="comparisonEnd"
+ [startDateAccessibleName]="startDateAccessibleName"
+ [endDateAccessibleName]="endDateAccessibleName"
(_userSelection)="_dateSelected($event)">
diff --git a/src/material/datepicker/calendar.ts b/src/material/datepicker/calendar.ts
index 1dc5c263d3a8..1bf1423d5394 100644
--- a/src/material/datepicker/calendar.ts
+++ b/src/material/datepicker/calendar.ts
@@ -282,6 +282,12 @@ export class MatCalendar implements AfterContentInit, AfterViewChecked, OnDes
/** End of the comparison range. */
@Input() comparisonEnd: D | null;
+ /** ARIA Accessible name of the `` */
+ @Input() startDateAccessibleName: string | null;
+
+ /** ARIA Accessible name of the `` */
+ @Input() endDateAccessibleName: string | null;
+
/** Emits when the currently selected date changes. */
@Output() readonly selectedChange: EventEmitter = new EventEmitter();
diff --git a/src/material/datepicker/date-range-input-parts.ts b/src/material/datepicker/date-range-input-parts.ts
index 9f7f460e2a37..d0cc54b49334 100644
--- a/src/material/datepicker/date-range-input-parts.ts
+++ b/src/material/datepicker/date-range-input-parts.ts
@@ -41,6 +41,7 @@ import {Directionality} from '@angular/cdk/bidi';
import {BACKSPACE, LEFT_ARROW, RIGHT_ARROW} from '@angular/cdk/keycodes';
import {MatDatepickerInputBase, DateFilterFn} from './datepicker-input-base';
import {DateRange, DateSelectionModelChange} from './date-selection-model';
+import {_computeAriaAccessibleName} from './aria-accessible-name';
/** Parent component that should be wrapped around `MatStartDate` and `MatEndDate`. */
export interface MatDateRangeInputParent {
@@ -185,6 +186,11 @@ abstract class MatDateRangeInputPartBase
) as MatDateRangeInputPartBase | undefined;
opposite?._validatorOnChange();
}
+
+ /** return the ARIA accessible name of the input element */
+ _getAccessibleName(): string {
+ return _computeAriaAccessibleName(this._elementRef.nativeElement);
+ }
}
const _MatDateRangeInputBase = mixinErrorState(MatDateRangeInputPartBase);
@@ -198,7 +204,6 @@ const _MatDateRangeInputBase = mixinErrorState(MatDateRangeInputPartBase);
'(input)': '_onInput($event.target.value)',
'(change)': '_onChange()',
'(keydown)': '_onKeydown($event)',
- '[attr.id]': '_rangeInput.id',
'[attr.aria-haspopup]': '_rangeInput.rangePicker ? "dialog" : null',
'[attr.aria-owns]': '(_rangeInput.rangePicker?.opened && _rangeInput.rangePicker.id) || null',
'[attr.min]': '_getMinDate() ? _dateAdapter.toIso8601(_getMinDate()) : null',
diff --git a/src/material/datepicker/date-range-input.spec.ts b/src/material/datepicker/date-range-input.spec.ts
index 6993d2a6d3da..4418b27fb2e7 100644
--- a/src/material/datepicker/date-range-input.spec.ts
+++ b/src/material/datepicker/date-range-input.spec.ts
@@ -53,7 +53,7 @@ describe('MatDateRangeInput', () => {
const mirror = fixture.nativeElement.querySelector('.mat-date-range-input-mirror');
const startInput = fixture.componentInstance.start.nativeElement;
- expect(mirror.textContent).toBe('Start date');
+ expect(mirror.textContent).toBe('Start Date');
startInput.value = 'hello';
dispatchFakeEvent(startInput, 'input');
@@ -69,7 +69,7 @@ describe('MatDateRangeInput', () => {
dispatchFakeEvent(startInput, 'input');
fixture.detectChanges();
- expect(mirror.textContent).toBe('Start date');
+ expect(mirror.textContent).toBe('Start Date');
});
it('should hide the mirror value from assistive technology', () => {
@@ -160,20 +160,20 @@ describe('MatDateRangeInput', () => {
expect(rangeInput.classList).toContain(hideClass);
});
- it('should point the label aria-owns to the id of the start input', () => {
+ it('should point the label aria-owns to the ', () => {
const fixture = createComponent(StandardRangePicker);
fixture.detectChanges();
- const label = fixture.nativeElement.querySelector('label');
- const start = fixture.componentInstance.start.nativeElement;
+ const label = fixture.nativeElement.querySelector('label.mat-form-field-label');
+ const rangeInput = fixture.componentInstance.rangeInput;
- expect(start.id).toBeTruthy();
- expect(label.getAttribute('aria-owns')).toBe(start.id);
+ expect(rangeInput.id).toBeTruthy();
+ expect(label.getAttribute('aria-owns')).toBe(rangeInput.id);
});
it('should point the range input aria-labelledby to the form field label', () => {
const fixture = createComponent(StandardRangePicker);
fixture.detectChanges();
- const labelId = fixture.nativeElement.querySelector('label').id;
+ const labelId = fixture.nativeElement.querySelector('label.mat-form-field-label').id;
const rangeInput = fixture.nativeElement.querySelector('.mat-date-range-input');
expect(labelId).toBeTruthy();
@@ -544,6 +544,65 @@ describe('MatDateRangeInput', () => {
expect(rangeTexts).toEqual(['2', '3', '4', '5']);
}));
+ it("should have aria-desciredby on start and end date cells that point to the 's accessible name", fakeAsync(() => {
+ const fixture = createComponent(StandardRangePicker);
+ const {start, end} = fixture.componentInstance.range.controls;
+ let overlayContainerElement: HTMLElement;
+ start.setValue(new Date(2020, 1, 2));
+ end.setValue(new Date(2020, 1, 5));
+ inject([OverlayContainer], (overlayContainer: OverlayContainer) => {
+ overlayContainerElement = overlayContainer.getContainerElement();
+ })();
+ fixture.detectChanges();
+ tick();
+
+ fixture.componentInstance.rangePicker.open();
+ fixture.detectChanges();
+ tick();
+
+ const rangeStart = overlayContainerElement!.querySelector('.mat-calendar-body-range-start');
+ const rangeEnd = overlayContainerElement!.querySelector('.mat-calendar-body-range-end');
+
+ // query for targets of `aria-describedby`. Query from document instead of fixture.nativeElement as calendar UI is rendered in an overlay.
+ const rangeStartDescriptions = Array.from(
+ document.querySelectorAll(
+ rangeStart!
+ .getAttribute('aria-describedby')!
+ .split(/\s+/g)
+ .map(x => `#${x}`)
+ .join(' '),
+ ),
+ );
+ const rangeEndDescriptions = Array.from(
+ document.querySelectorAll(
+ rangeEnd!
+ .getAttribute('aria-describedby')!
+ .split(/\s+/g)
+ .map(x => `#${x}`)
+ .join(' '),
+ ),
+ );
+
+ expect(rangeStartDescriptions)
+ .withContext('target of aria-descriedby should exist')
+ .not.toBeNull();
+ expect(rangeEndDescriptions)
+ .withContext('target of aria-descriedby should exist')
+ .not.toBeNull();
+ expect(
+ rangeStartDescriptions
+ .map(x => x.textContent)
+ .join(' ')
+ .trim(),
+ ).toEqual('Start date');
+ expect(
+ rangeEndDescriptions
+ .map(x => x.textContent)
+ .join(' ')
+ .trim(),
+ ).toEqual('End date');
+ }));
+
it('should pass the comparison range through to the calendar', fakeAsync(() => {
const fixture = createComponent(StandardRangePicker);
let overlayContainerElement: HTMLElement;
@@ -819,7 +878,7 @@ describe('MatDateRangeInput', () => {
it('should be able to get the input placeholder', () => {
const fixture = createComponent(StandardRangePicker);
fixture.detectChanges();
- expect(fixture.componentInstance.rangeInput.placeholder).toBe('Start date – End date');
+ expect(fixture.componentInstance.rangeInput.placeholder).toBe('Start Date – End Date');
});
it('should emit to the stateChanges stream when typing a value into an input', () => {
@@ -1068,9 +1127,13 @@ describe('MatDateRangeInput', () => {
[dateFilter]="dateFilter"
[comparisonStart]="comparisonStart"
[comparisonEnd]="comparisonEnd">
-
-
+
+
+
+
return this._model ? this._model.selection : null;
}
- /** Unique ID for the input. */
+ /** Unique ID for the group. */
id = `mat-date-range-input-${nextUniqueId++}`;
/** Whether the control is focused. */
@@ -390,6 +390,14 @@ export class MatDateRangeInput
return formField && formField._hasFloatingLabel() ? formField._labelId : null;
}
+ _getStartDateAccessibleName(): string {
+ return this._startInput._getAccessibleName();
+ }
+
+ _getEndDateAccessibleName(): string {
+ return this._endInput._getAccessibleName();
+ }
+
/** Updates the focused state of the range input. */
_updateFocus(origin: FocusOrigin) {
this.focused = origin !== null;
diff --git a/src/material/datepicker/date-range-picker.ts b/src/material/datepicker/date-range-picker.ts
index 365b7abd0d93..d13fd65d13db 100644
--- a/src/material/datepicker/date-range-picker.ts
+++ b/src/material/datepicker/date-range-picker.ts
@@ -16,6 +16,8 @@ import {MAT_CALENDAR_RANGE_STRATEGY_PROVIDER} from './date-range-selection-strat
* @docs-private
*/
export interface MatDateRangePickerInput extends MatDatepickerControl {
+ _getEndDateAccessibleName(): string | null;
+ _getStartDateAccessibleName(): string | null;
comparisonStart: D | null;
comparisonEnd: D | null;
}
@@ -49,6 +51,8 @@ export class MatDateRangePicker extends MatDatepickerBase<
if (input) {
instance.comparisonStart = input.comparisonStart;
instance.comparisonEnd = input.comparisonEnd;
+ instance.startDateAccessibleName = input._getStartDateAccessibleName();
+ instance.endDateAccessibleName = input._getEndDateAccessibleName();
}
}
}
diff --git a/src/material/datepicker/datepicker-base.ts b/src/material/datepicker/datepicker-base.ts
index 1f47a696c96f..2e4b5813d185 100644
--- a/src/material/datepicker/datepicker-base.ts
+++ b/src/material/datepicker/datepicker-base.ts
@@ -144,6 +144,12 @@ export class MatDatepickerContent>
/** End of the comparison range. */
comparisonEnd: D | null;
+ /** ARIA Accessible name of the `` */
+ startDateAccessibleName: string | null;
+
+ /** ARIA Accessible name of the `` */
+ endDateAccessibleName: string | null;
+
/** Whether the datepicker is above or below the input. */
_isAbove: boolean;
diff --git a/src/material/datepicker/datepicker-content.html b/src/material/datepicker/datepicker-content.html
index 2c4b931305fd..fa280568e767 100644
--- a/src/material/datepicker/datepicker-content.html
+++ b/src/material/datepicker/datepicker-content.html
@@ -20,6 +20,8 @@
[comparisonStart]="comparisonStart"
[comparisonEnd]="comparisonEnd"
[@fadeInCalendar]="'enter'"
+ [startDateAccessibleName]="startDateAccessibleName"
+ [endDateAccessibleName]="endDateAccessibleName"
(yearSelected)="datepicker._selectYear($event)"
(monthSelected)="datepicker._selectMonth($event)"
(viewChanged)="datepicker._viewChanged($event)"
diff --git a/src/material/datepicker/month-view.html b/src/material/datepicker/month-view.html
index d002f925afbc..ef727c191e6a 100644
--- a/src/material/datepicker/month-view.html
+++ b/src/material/datepicker/month-view.html
@@ -21,6 +21,8 @@
[isRange]="_isRange"
[labelMinRequiredCells]="3"
[activeCell]="_dateAdapter.getDate(activeDate) - 1"
+ [startDateAccessibleName]="startDateAccessibleName"
+ [endDateAccessibleName]="endDateAccessibleName"
(selectedValueChange)="_dateSelected($event)"
(activeDateChange)="_updateActiveDate($event)"
(previewChange)="_previewChanged($event)"
diff --git a/src/material/datepicker/month-view.ts b/src/material/datepicker/month-view.ts
index 5cf3a745d087..95487a0a0d56 100644
--- a/src/material/datepicker/month-view.ts
+++ b/src/material/datepicker/month-view.ts
@@ -139,6 +139,12 @@ export class MatMonthView implements AfterContentInit, OnChanges, OnDestroy {
/** End of the comparison range. */
@Input() comparisonEnd: D | null;
+ /** ARIA Accessible name of the `` */
+ @Input() startDateAccessibleName: string | null;
+
+ /** ARIA Accessible name of the `` */
+ @Input() endDateAccessibleName: string | null;
+
/** Emits when a new date is selected. */
@Output() readonly selectedChange: EventEmitter = new EventEmitter();
diff --git a/tools/public_api_guard/material/datepicker.md b/tools/public_api_guard/material/datepicker.md
index b0eb5c7ff909..d09ddc30d8d2 100644
--- a/tools/public_api_guard/material/datepicker.md
+++ b/tools/public_api_guard/material/datepicker.md
@@ -158,6 +158,7 @@ export class MatCalendar implements AfterContentInit, AfterViewChecked, OnDes
dateClass: MatCalendarCellClassFunction;
dateFilter: (date: D) => boolean;
_dateSelected(event: MatCalendarUserEvent): void;
+ endDateAccessibleName: string | null;
focusActiveCell(): void;
_goToDateInView(date: D, view: 'month' | 'year' | 'multi-year'): void;
headerComponent: ComponentType;
@@ -182,6 +183,7 @@ export class MatCalendar implements AfterContentInit, AfterViewChecked, OnDes
readonly selectedChange: EventEmitter;
get startAt(): D | null;
set startAt(value: D | null);
+ startDateAccessibleName: string | null;
startView: MatCalendarView;
readonly stateChanges: Subject;
updateTodaysDate(): void;
@@ -191,7 +193,7 @@ export class MatCalendar implements AfterContentInit, AfterViewChecked, OnDes
_yearSelectedInMultiYearView(normalizedYear: D): void;
yearView: MatYearView;
// (undocumented)
- static ɵcmp: i0.ɵɵComponentDeclaration, "mat-calendar", ["matCalendar"], { "headerComponent": "headerComponent"; "startAt": "startAt"; "startView": "startView"; "selected": "selected"; "minDate": "minDate"; "maxDate": "maxDate"; "dateFilter": "dateFilter"; "dateClass": "dateClass"; "comparisonStart": "comparisonStart"; "comparisonEnd": "comparisonEnd"; }, { "selectedChange": "selectedChange"; "yearSelected": "yearSelected"; "monthSelected": "monthSelected"; "viewChanged": "viewChanged"; "_userSelection": "_userSelection"; }, never, never, false>;
+ static ɵcmp: i0.ɵɵComponentDeclaration, "mat-calendar", ["matCalendar"], { "headerComponent": "headerComponent"; "startAt": "startAt"; "startView": "startView"; "selected": "selected"; "minDate": "minDate"; "maxDate": "maxDate"; "dateFilter": "dateFilter"; "dateClass": "dateClass"; "comparisonStart": "comparisonStart"; "comparisonEnd": "comparisonEnd"; "startDateAccessibleName": "startDateAccessibleName"; "endDateAccessibleName": "endDateAccessibleName"; }, { "selectedChange": "selectedChange"; "yearSelected": "yearSelected"; "monthSelected": "monthSelected"; "viewChanged": "viewChanged"; "_userSelection": "_userSelection"; }, never, never, false>;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration, [null, { optional: true; }, { optional: true; }, null]>;
}
@@ -210,9 +212,13 @@ export class MatCalendarBody implements OnChanges, OnDestroy, AfterViewChecked {
comparisonStart: number | null;
// (undocumented)
_emitActiveDateChange(cell: MatCalendarCell, event: FocusEvent): void;
+ endDateAccessibleName: string | null;
+ // (undocumented)
+ _endDateLabelId: string;
endValue: number;
_firstRowOffset: number;
_focusActiveCell(movePreview?: boolean): void;
+ _getDescribedby(value: number): string | null;
_isActiveCell(rowIndex: number, colIndex: number): boolean;
_isComparisonBridgeEnd(value: number, rowIndex: number, colIndex: number): boolean;
_isComparisonBridgeStart(value: number, rowIndex: number, colIndex: number): boolean;
@@ -243,10 +249,13 @@ export class MatCalendarBody implements OnChanges, OnDestroy, AfterViewChecked {
rows: MatCalendarCell[][];
_scheduleFocusActiveCellAfterViewChecked(): void;
readonly selectedValueChange: EventEmitter>;
+ startDateAccessibleName: string | null;
+ // (undocumented)
+ _startDateLabelId: string;
startValue: number;
todayValue: number;
// (undocumented)
- static ɵcmp: i0.ɵɵComponentDeclaration;
+ static ɵcmp: i0.ɵɵComponentDeclaration;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration;
}
@@ -435,6 +444,7 @@ export class MatDatepickerContent> extend
comparisonStart: D | null;
datepicker: MatDatepickerBase;
_dialogLabelId: string | null;
+ endDateAccessibleName: string | null;
// (undocumented)
_getSelected(): D | DateRange | null;
// (undocumented)
@@ -446,6 +456,7 @@ export class MatDatepickerContent> extend
ngOnDestroy(): void;
// (undocumented)
ngOnInit(): void;
+ startDateAccessibleName: string | null;
// (undocumented)
_startExitAnimation(): void;
// (undocumented)
@@ -622,8 +633,12 @@ export class MatDateRangeInput implements MatLegacyFormFieldControl extends MatDatepickerControl {
comparisonEnd: D | null;
// (undocumented)
comparisonStart: D | null;
+ // (undocumented)
+ _getEndDateAccessibleName(): string | null;
+ // (undocumented)
+ _getStartDateAccessibleName(): string | null;
}
// @public
@@ -774,6 +793,7 @@ export class MatMonthView implements AfterContentInit, OnChanges, OnDestroy {
dateClass: MatCalendarCellClassFunction;
dateFilter: (date: D) => boolean;
_dateSelected(event: MatCalendarUserEvent): void;
+ endDateAccessibleName: string | null;
_firstWeekOffset: number;
_focusActiveCell(movePreview?: boolean): void;
_focusActiveCellAfterViewChecked(): void;
@@ -801,6 +821,7 @@ export class MatMonthView implements AfterContentInit, OnChanges, OnDestroy {
get selected(): DateRange | D | null;
set selected(value: DateRange | D | null);
readonly selectedChange: EventEmitter;
+ startDateAccessibleName: string | null;
_todayDate: number | null;
_updateActiveDate(event: MatCalendarUserEvent): void;
readonly _userSelection: EventEmitter>;
@@ -810,7 +831,7 @@ export class MatMonthView implements AfterContentInit, OnChanges, OnDestroy {
}[];
_weeks: MatCalendarCell[][];
// (undocumented)
- static ɵcmp: i0.ɵɵComponentDeclaration, "mat-month-view", ["matMonthView"], { "activeDate": "activeDate"; "selected": "selected"; "minDate": "minDate"; "maxDate": "maxDate"; "dateFilter": "dateFilter"; "dateClass": "dateClass"; "comparisonStart": "comparisonStart"; "comparisonEnd": "comparisonEnd"; }, { "selectedChange": "selectedChange"; "_userSelection": "_userSelection"; "activeDateChange": "activeDateChange"; }, never, never, false>;
+ static ɵcmp: i0.ɵɵComponentDeclaration, "mat-month-view", ["matMonthView"], { "activeDate": "activeDate"; "selected": "selected"; "minDate": "minDate"; "maxDate": "maxDate"; "dateFilter": "dateFilter"; "dateClass": "dateClass"; "comparisonStart": "comparisonStart"; "comparisonEnd": "comparisonEnd"; "startDateAccessibleName": "startDateAccessibleName"; "endDateAccessibleName": "endDateAccessibleName"; }, { "selectedChange": "selectedChange"; "_userSelection": "_userSelection"; "activeDateChange": "activeDateChange"; }, never, never, false>;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration, [null, { optional: true; }, { optional: true; }, { optional: true; }, { optional: true; }]>;
}