Skip to content

Commit 407398f

Browse files
authored
fix(material/datepicker): add close button for screen readers (#20666)
Adds an invisible close button for screen reader users that becomes visible when the user tabs into it. Fixes #14379.
1 parent 33a43f7 commit 407398f

File tree

6 files changed

+78
-6
lines changed

6 files changed

+78
-6
lines changed

src/material/datepicker/datepicker-base.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ import {
6464
MAT_DATE_RANGE_SELECTION_STRATEGY,
6565
MatDateRangeSelectionStrategy,
6666
} from './date-range-selection-strategy';
67+
import {MatDatepickerIntl} from './datepicker-intl';
6768

6869
/** Used to generate a unique ID for each datepicker instance. */
6970
let datepickerUid = 0;
@@ -149,14 +150,27 @@ export class MatDatepickerContent<S, D = ExtractDateTypeFromSelection<S>>
149150
/** Emits when an animation has finished. */
150151
_animationDone = new Subject<void>();
151152

153+
/** Text for the close button. */
154+
_closeButtonText: string;
155+
156+
/** Whether the close button currently has focus. */
157+
_closeButtonFocused: boolean;
158+
152159
constructor(
153160
elementRef: ElementRef,
154161
private _changeDetectorRef: ChangeDetectorRef,
155162
private _model: MatDateSelectionModel<S, D>,
156163
private _dateAdapter: DateAdapter<D>,
157164
@Optional() @Inject(MAT_DATE_RANGE_SELECTION_STRATEGY)
158-
private _rangeSelectionStrategy: MatDateRangeSelectionStrategy<D>) {
165+
private _rangeSelectionStrategy: MatDateRangeSelectionStrategy<D>,
166+
/**
167+
* @deprecated `intl` argument to become required.
168+
* @breaking-change 12.0.0
169+
*/
170+
intl?: MatDatepickerIntl) {
159171
super(elementRef);
172+
// @breaking-change 12.0.0 Remove fallback for `intl`.
173+
this._closeButtonText = intl?.closeCalendarLabel || 'Close calendar';
160174
}
161175

162176
ngAfterViewInit() {

src/material/datepicker/datepicker-content.html

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
<mat-calendar cdkTrapFocus
1+
<div cdkTrapFocus>
2+
<mat-calendar
23
[id]="datepicker.id"
34
[ngClass]="datepicker.panelClass"
45
[startAt]="datepicker.startAt"
@@ -15,5 +16,16 @@
1516
(yearSelected)="datepicker._selectYear($event)"
1617
(monthSelected)="datepicker._selectMonth($event)"
1718
(viewChanged)="datepicker._viewChanged($event)"
18-
(_userSelection)="_handleUserSelection($event)">
19-
</mat-calendar>
19+
(_userSelection)="_handleUserSelection($event)"></mat-calendar>
20+
21+
<!-- Invisible close button for screen reader users. -->
22+
<button
23+
type="button"
24+
mat-raised-button
25+
color="primary"
26+
class="mat-datepicker-close-button"
27+
[class.cdk-visually-hidden]="!_closeButtonFocused"
28+
(focus)="_closeButtonFocused = true"
29+
(blur)="_closeButtonFocused = false"
30+
(click)="datepicker.close()">{{ _closeButtonText }}</button>
31+
</div>

src/material/datepicker/datepicker-content.scss

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,13 @@ $mat-datepicker-touch-max-height: 788px;
4949
}
5050
}
5151

52+
.mat-datepicker-close-button {
53+
position: absolute;
54+
top: 100%;
55+
left: 0;
56+
margin-top: 8px;
57+
}
58+
5259
@media all and (orientation: landscape) {
5360
.mat-datepicker-content-touch .mat-calendar {
5461
width: $mat-datepicker-touch-landscape-width;

src/material/datepicker/datepicker-intl.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ export class MatDatepickerIntl {
2525
/** A label for the button used to open the calendar popup (used by screen readers). */
2626
openCalendarLabel: string = 'Open calendar';
2727

28+
/** Label for the button used to close the calendar popup. */
29+
closeCalendarLabel: string = 'Close calendar';
30+
2831
/** A label for the previous month button (used by screen readers). */
2932
prevMonthLabel: string = 'Previous month';
3033

src/material/datepicker/datepicker.spec.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,38 @@ describe('MatDatepicker', () => {
501501
expect(event.defaultPrevented).toBe(false);
502502
}));
503503

504+
it('should show the invisible close button on focus', fakeAsync(() => {
505+
testComponent.opened = true;
506+
fixture.detectChanges();
507+
flush();
508+
509+
const button = document.querySelector('.mat-datepicker-close-button') as HTMLButtonElement;
510+
expect(button.classList).toContain('cdk-visually-hidden');
511+
512+
dispatchFakeEvent(button, 'focus');
513+
fixture.detectChanges();
514+
expect(button.classList).not.toContain('cdk-visually-hidden');
515+
516+
dispatchFakeEvent(button, 'blur');
517+
fixture.detectChanges();
518+
expect(button.classList).toContain('cdk-visually-hidden');
519+
}));
520+
521+
it('should close the overlay when clicking on the invisible close button', fakeAsync(() => {
522+
testComponent.opened = true;
523+
fixture.detectChanges();
524+
flush();
525+
526+
const button = document.querySelector('.mat-datepicker-close-button') as HTMLButtonElement;
527+
expect(document.querySelector('.mat-datepicker-content')).not.toBeNull();
528+
529+
button.click();
530+
fixture.detectChanges();
531+
flush();
532+
533+
expect(document.querySelector('.mat-datepicker-content')).toBeNull();
534+
}));
535+
504536
});
505537

506538
describe('datepicker with too many inputs', () => {

tools/public_api_guard/material/datepicker.d.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,18 +192,21 @@ export declare class MatDatepickerContent<S, D = ExtractDateTypeFromSelection<S>
192192
_animationDone: Subject<void>;
193193
_animationState: 'enter' | 'void';
194194
_calendar: MatCalendar<D>;
195+
_closeButtonFocused: boolean;
196+
_closeButtonText: string;
195197
_isAbove: boolean;
196198
comparisonEnd: D | null;
197199
comparisonStart: D | null;
198200
datepicker: MatDatepickerBase<any, S, D>;
199-
constructor(elementRef: ElementRef, _changeDetectorRef: ChangeDetectorRef, _model: MatDateSelectionModel<S, D>, _dateAdapter: DateAdapter<D>, _rangeSelectionStrategy: MatDateRangeSelectionStrategy<D>);
201+
constructor(elementRef: ElementRef, _changeDetectorRef: ChangeDetectorRef, _model: MatDateSelectionModel<S, D>, _dateAdapter: DateAdapter<D>, _rangeSelectionStrategy: MatDateRangeSelectionStrategy<D>,
202+
intl?: MatDatepickerIntl);
200203
_getSelected(): D | DateRange<D> | null;
201204
_handleUserSelection(event: MatCalendarUserEvent<D | null>): void;
202205
_startExitAnimation(): void;
203206
ngAfterViewInit(): void;
204207
ngOnDestroy(): void;
205208
static ɵcmp: i0.ɵɵComponentDefWithMeta<MatDatepickerContent<any, any>, "mat-datepicker-content", ["matDatepickerContent"], { "color": "color"; }, {}, never, never>;
206-
static ɵfac: i0.ɵɵFactoryDef<MatDatepickerContent<any, any>, [null, null, null, null, { optional: true; }]>;
209+
static ɵfac: i0.ɵɵFactoryDef<MatDatepickerContent<any, any>, [null, null, null, null, { optional: true; }, null]>;
207210
}
208211

209212
export declare class MatDatepickerInput<D> extends MatDatepickerInputBase<D | null, D> implements MatDatepickerControl<D | null> {
@@ -245,6 +248,7 @@ export declare class MatDatepickerInputEvent<D, S = unknown> {
245248
export declare class MatDatepickerIntl {
246249
calendarLabel: string;
247250
readonly changes: Subject<void>;
251+
closeCalendarLabel: string;
248252
nextMonthLabel: string;
249253
nextMultiYearLabel: string;
250254
nextYearLabel: string;

0 commit comments

Comments
 (0)