From 10c477ae54b2d2ff73af3b65b15d7ab2cdfeef96 Mon Sep 17 00:00:00 2001
From: Nigel Huang <28766663+nigel5@users.noreply.github.com>
Date: Mon, 7 Jan 2019 07:54:22 -0500
Subject: [PATCH] fix(datepicker): add footer with missing close button for
screen readers
Users with screen readers have difficulty closing the datepicker popup
obviously because they would have a hard time tapping the backdrop.
Also, on mobile device screen readers they do not have an escape key.
---
src/lib/datepicker/datepicker-content.html | 50 ++++++++++++++--------
src/lib/datepicker/datepicker-content.scss | 11 +++++
src/lib/datepicker/datepicker-intl.ts | 3 ++
src/lib/datepicker/datepicker.spec.ts | 34 ++++++++++++++-
src/lib/datepicker/datepicker.ts | 23 +++++++++-
5 files changed, 102 insertions(+), 19 deletions(-)
diff --git a/src/lib/datepicker/datepicker-content.html b/src/lib/datepicker/datepicker-content.html
index fbca010906e8..3df4f7355d81 100644
--- a/src/lib/datepicker/datepicker-content.html
+++ b/src/lib/datepicker/datepicker-content.html
@@ -1,17 +1,33 @@
-
-
+
+
+
+
+
+
+
+
+
diff --git a/src/lib/datepicker/datepicker-content.scss b/src/lib/datepicker/datepicker-content.scss
index 6a5326adb6e4..8b687ddf1b51 100644
--- a/src/lib/datepicker/datepicker-content.scss
+++ b/src/lib/datepicker/datepicker-content.scss
@@ -31,6 +31,17 @@ $mat-datepicker-touch-max-height: 788px;
}
}
+.mat-datepicker-close-button {
+ position: absolute;
+ margin-top: 10px;
+ &.cdk-keyboard-focused {
+ background: #2468cf;
+ color: #ffffff;
+ z-index: 999;
+ text-decoration: none;
+ }
+}
+
.mat-datepicker-content-touch {
display: block;
// make sure the dialog scrolls rather than being cropped on ludicrously small screens
diff --git a/src/lib/datepicker/datepicker-intl.ts b/src/lib/datepicker/datepicker-intl.ts
index d2cdc9fbfb7d..43fac6610bbc 100644
--- a/src/lib/datepicker/datepicker-intl.ts
+++ b/src/lib/datepicker/datepicker-intl.ts
@@ -25,6 +25,9 @@ export class MatDatepickerIntl {
/** A label for the button used to open the calendar popup (used by screen readers). */
openCalendarLabel: string = 'Open calendar';
+ /** A label for the button used to close the calendar popup (used by screen readers). */
+ closeCalendarLabel: string = 'Close calendar';
+
/** A label for the previous month button (used by screen readers). */
prevMonthLabel: string = 'Previous month';
diff --git a/src/lib/datepicker/datepicker.spec.ts b/src/lib/datepicker/datepicker.spec.ts
index f3c5c37d8702..6f86b192499f 100644
--- a/src/lib/datepicker/datepicker.spec.ts
+++ b/src/lib/datepicker/datepicker.spec.ts
@@ -1,5 +1,5 @@
import {Directionality} from '@angular/cdk/bidi';
-import {DOWN_ARROW, ENTER, ESCAPE, RIGHT_ARROW, UP_ARROW} from '@angular/cdk/keycodes';
+import {DOWN_ARROW, ENTER, ESCAPE, RIGHT_ARROW, UP_ARROW, TAB} from '@angular/cdk/keycodes';
import {Overlay, OverlayContainer} from '@angular/cdk/overlay';
import {ScrollDispatcher} from '@angular/cdk/scrolling';
import {
@@ -200,6 +200,38 @@ describe('MatDatepicker', () => {
expect(testComponent.datepicker.opened).toBe(false, 'Expected datepicker to be closed.');
}));
+ it('should close the popup when the close button is clicked', fakeAsync(() => {
+ testComponent.datepicker.open();
+ fixture.detectChanges();
+
+ expect(testComponent.datepicker.opened).toBe(true, 'Expected datepicker to be open.');
+
+ const closeButton = document.querySelector('.mat-datepicker-close-button') as HTMLElement;
+
+ closeButton.click();
+ fixture.detectChanges();
+ flush();
+
+ expect(testComponent.datepicker.opened).toBe(false, 'Expected datepicker to be closed.');
+ }));
+
+ it('close button should appear when focused', () => {
+ testComponent.datepicker.open();
+ fixture.detectChanges();
+
+ expect(testComponent.datepicker.opened).toBe(true, 'Expected datepicker to be open.');
+
+ const closeButton: HTMLElement =
+ fixture.debugElement.query(By.css('.mat-datepicker-close-button')).nativeElement;
+
+ dispatchKeyboardEvent(closeButton, 'keydown', TAB);
+ closeButton.focus();
+ fixture.detectChanges();
+
+ expect(closeButton.classList.contains('cdk-keyboard-focused')).toEqual(true);
+ expect(closeButton.classList.contains('cdk-visually-hidden')).toEqual(false);
+ });
+
it('should set the proper role on the popup', fakeAsync(() => {
testComponent.datepicker.open();
fixture.detectChanges();
diff --git a/src/lib/datepicker/datepicker.ts b/src/lib/datepicker/datepicker.ts
index d952a7d8f917..7a289c0ead69 100644
--- a/src/lib/datepicker/datepicker.ts
+++ b/src/lib/datepicker/datepicker.ts
@@ -21,6 +21,7 @@ import {DOCUMENT} from '@angular/common';
import {
AfterViewInit,
ChangeDetectionStrategy,
+ ChangeDetectorRef,
Component,
ComponentRef,
ElementRef,
@@ -51,6 +52,8 @@ import {matDatepickerAnimations} from './datepicker-animations';
import {createMissingDateImplError} from './datepicker-errors';
import {MatDatepickerInput} from './datepicker-input';
import {MatCalendarCellCssClasses} from './calendar-body';
+import {MatDatepickerIntl} from './datepicker-intl';
+import {FocusOrigin} from '@angular/cdk/a11y';
/** Used to generate a unique ID for each datepicker instance. */
let datepickerUid = 0;
@@ -117,13 +120,31 @@ export class MatDatepickerContent extends _MatDatepickerContentMixinBase
/** Whether the datepicker is above or below the input. */
_isAbove: boolean;
- constructor(elementRef: ElementRef) {
+ /** For the focus monitor */
+ closeButtonElementOrigin: string = this.formatOrigin(null);
+
+ get closeButtonLabel(): string {
+ return this._intl.closeCalendarLabel;
+ }
+
+ constructor(private _intl: MatDatepickerIntl,
+ private _ngZone: NgZone,
+ private _changeDetectorRef: ChangeDetectorRef,
+ elementRef: ElementRef) {
super(elementRef);
}
ngAfterViewInit() {
this._calendar.focusActiveCell();
}
+
+ formatOrigin(origin: FocusOrigin): string {
+ return origin ? origin + ' focused' : 'blurred';
+ }
+
+ markForCheck() {
+ this._ngZone.run(() => this._changeDetectorRef.markForCheck());
+ }
}