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()); + } }