Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 33 additions & 17 deletions src/lib/datepicker/datepicker-content.html
Original file line number Diff line number Diff line change
@@ -1,17 +1,33 @@
<mat-calendar cdkTrapFocus
[id]="datepicker.id"
[ngClass]="datepicker.panelClass"
[startAt]="datepicker.startAt"
[startView]="datepicker.startView"
[minDate]="datepicker._minDate"
[maxDate]="datepicker._maxDate"
[dateFilter]="datepicker._dateFilter"
[headerComponent]="datepicker.calendarHeaderComponent"
[selected]="datepicker._selected"
[dateClass]="datepicker.dateClass"
[@fadeInCalendar]="'enter'"
(selectedChange)="datepicker.select($event)"
(yearSelected)="datepicker._selectYear($event)"
(monthSelected)="datepicker._selectMonth($event)"
(_userSelection)="datepicker.close()">
</mat-calendar>
<div cdkTrapFocus>
<mat-calendar
[id]="datepicker.id"
[ngClass]="datepicker.panelClass"
[startAt]="datepicker.startAt"
[startView]="datepicker.startView"
[minDate]="datepicker._minDate"
[maxDate]="datepicker._maxDate"
[dateFilter]="datepicker._dateFilter"
[headerComponent]="datepicker.calendarHeaderComponent"
[selected]="datepicker._selected"
[dateClass]="datepicker.dateClass"
[@fadeInCalendar]="'enter'"
(selectedChange)="datepicker.select($event)"
(yearSelected)="datepicker._selectYear($event)"
(monthSelected)="datepicker._selectMonth($event)"
(_userSelection)="datepicker.close()">
</mat-calendar>

<!-- An invisible close button so users with screen readers can easily close the datepicker -->
<div cdkMonitorSubtreeFocus
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like overkill, you can just use (focus) and (blur) events on the button

(cdkFocusChange)="closeButtonElementOrigin = formatOrigin($event); markForCheck()">
<button mat-button
disableRipple
type="button"
[class.cdk-visually-hidden]="closeButtonElementOrigin === 'blurred'"
class="mat-datepicker-close-button"
[attr.aria-label]="closeButtonLabel"
(click)="datepicker.close()">
{{closeButtonLabel}}
</button>
</div>
</div>
11 changes: 11 additions & 0 deletions src/lib/datepicker/datepicker-content.scss
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,17 @@ $mat-datepicker-touch-max-height: 788px;
}
}

.mat-datepicker-close-button {
position: absolute;
margin-top: 10px;
&.cdk-keyboard-focused {
background: #2468cf;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where are these colors coming from? Would a mat-raised-button with color="primary" work instead?

color: #ffffff;
z-index: 999;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems unnecessarily high, will z-index: 1 work?

text-decoration: none;
}
}

.mat-datepicker-content-touch {
display: block;
// make sure the dialog scrolls rather than being cropped on ludicrously small screens
Expand Down
3 changes: 3 additions & 0 deletions src/lib/datepicker/datepicker-intl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
34 changes: 33 additions & 1 deletion src/lib/datepicker/datepicker.spec.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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();
Expand Down
23 changes: 22 additions & 1 deletion src/lib/datepicker/datepicker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {DOCUMENT} from '@angular/common';
import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ComponentRef,
ElementRef,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -117,13 +120,31 @@ export class MatDatepickerContent<D> 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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Getters compile down to a lot of code (relative to just a plain method). Can you change this to a method (e.g. getCloseButtonLabel), or just make _intl public and access it directly in the template

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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use a boolean variable (e.g. isCloseButtonFocused)

return origin ? origin + ' focused' : 'blurred';
}

markForCheck() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this shouldn't be necessary if you switch to just using the (focus) and (blur) events

this._ngZone.run(() => this._changeDetectorRef.markForCheck());
}
}


Expand Down