Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(datepicker): make datepicker work with screen readers #4349

Merged
merged 7 commits into from
May 2, 2017
Merged
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
7 changes: 4 additions & 3 deletions src/demo-app/datepicker/datepicker-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,22 @@ <h1>Work in progress, not ready for use.</h1>
</p>
<br>
<p>
<input [mdDatepicker]="dp" [(ngModel)]="date">
<input [mdDatepicker]="dp" [(ngModel)]="date" aria-label="Departure date">
<button [mdDatepickerToggle]="dp"></button>
<md-datepicker #dp [touchUi]="touch"></md-datepicker>
</p>
<p>
<md-input-container>
<input mdInput [mdDatepicker]="dp2" [(ngModel)]="date">
<input mdInput [mdDatepicker]="dp2" [(ngModel)]="date" aria-label="Departure date">
<button mdSuffix [mdDatepickerToggle]="dp2"></button>
<md-datepicker #dp2 [touchUi]="touch"></md-datepicker>
</md-input-container>
</p>
<p>
<button [mdDatepickerToggle]="dp3"></button>
<md-input-container>
<input mdInput [mdDatepicker]="dp3" [(ngModel)]="date" min="1/1/16" max="1/1/18">
<input mdInput [mdDatepicker]="dp3" [(ngModel)]="date" min="1/1/16" max="1/1/18"
aria-label="Departure date">
<md-datepicker #dp3 [touchUi]="touch" startAt="1/1/17" [dateFilter]="dateFilter">
</md-datepicker>
</md-input-container>
Expand Down
2 changes: 2 additions & 0 deletions src/lib/core/datetime/date-formats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export type MdDateFormats = {
display: {
dateInput: any,
monthYearLabel: any,
dateA11yLabel: any,
monthYearA11yLabel: any,
}
};

Expand Down
2 changes: 2 additions & 0 deletions src/lib/core/datetime/native-date-formats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ export const MD_NATIVE_DATE_FORMATS: MdDateFormats = {
display: {
dateInput: {year: 'numeric', month: 'numeric', day: 'numeric'},
monthYearLabel: {year: 'numeric', month: 'short'},
dateA11yLabel: {year: 'numeric', month: 'long', day: 'numeric'},
monthYearA11yLabel: {year: 'numeric', month: 'long'},
}
};
16 changes: 13 additions & 3 deletions src/lib/datepicker/calendar-body.html
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
<!-- If there's not enough space in the first row, create a separate label row. -->
<tr *ngIf="_firstRowOffset < labelMinRequiredCells">
<!--
If there's not enough space in the first row, create a separate label row. We mark this row as
aria-hidden because we don't want it to be read out as one of the weeks in the month.
-->
<tr *ngIf="_firstRowOffset < labelMinRequiredCells" aria-hidden="true">
<td class="mat-calendar-body-label" [attr.colspan]="numCols" >{{label}}</td>
</tr>

<!-- Create the first row separately so we can include a special spacer cell. -->
<tr *ngFor="let row of rows; let rowIndex = index">
<tr *ngFor="let row of rows; let rowIndex = index" role="row">
<!--
We mark this cell as aria-hidden so it doesn't get read out as one of the days in the week.
-->
<td *ngIf="rowIndex === 0 && _firstRowOffset"
aria-hidden="true"
class="mat-calendar-body-label"
[attr.colspan]="_firstRowOffset">
{{_firstRowOffset >= labelMinRequiredCells ? label : ''}}
</td>
<td *ngFor="let item of row; let colIndex = index"
role="gridcell"
class="mat-calendar-body-cell"
[tabindex]="_isActiveCell(rowIndex, colIndex) ? 0 : -1"
[class.mat-calendar-body-disabled]="!item.enabled"
[class.mat-calendar-body-active]="_isActiveCell(rowIndex, colIndex)"
[attr.aria-label]="item.ariaLabel"
(click)="_cellClicked(item)">
<div class="mat-calendar-body-cell-content"
[class.mat-calendar-body-selected]="selectedValue === item.value"
Expand Down
1 change: 1 addition & 0 deletions src/lib/datepicker/calendar-body.scss
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ $mat-calendar-body-cell-content-size: 100% - $mat-calendar-body-cell-content-mar
line-height: 0;
padding: $mat-calendar-body-cell-padding 0;
text-align: center;
outline: none;
}

.mat-calendar-body-cell-content {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/datepicker/calendar-body.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,5 +171,5 @@ class CalendarBodyWithDisabledCells {


function createCell(value: number) {
return new MdCalendarCell(value, `${value}`, true);
return new MdCalendarCell(value, `${value}`, `${value}-label`, true);
}
5 changes: 4 additions & 1 deletion src/lib/datepicker/calendar-body.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ import {
* @docs-private
*/
export class MdCalendarCell {
constructor(public value: number, public displayValue: string, public enabled: boolean) {}
constructor(public value: number,
public displayValue: string,
public ariaLabel: string,
public enabled: boolean) {}
}


Expand Down
2 changes: 1 addition & 1 deletion src/lib/datepicker/calendar.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
</div>
</div>

<div class="mat-calendar-content" tabindex="0" (keydown)="_handleCalendarBodyKeydown($event)"
<div class="mat-calendar-content" (keydown)="_handleCalendarBodyKeydown($event)"
cdkMonitorSubtreeFocus>
<md-month-view
*ngIf="_monthView"
Expand Down
42 changes: 30 additions & 12 deletions src/lib/datepicker/calendar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import {
AfterContentInit,
ChangeDetectionStrategy,
Component,
ElementRef,
EventEmitter,
Inject,
Input,
NgZone,
Optional,
Output,
ViewEncapsulation
Expand Down Expand Up @@ -126,7 +128,9 @@ export class MdCalendar<D> implements AfterContentInit {
return this._monthView ? this._intl.nextMonthLabel : this._intl.nextYearLabel;
}

constructor(private _intl: MdDatepickerIntl,
constructor(private _elementRef: ElementRef,
private _intl: MdDatepickerIntl,
private _ngZone: NgZone,
@Optional() private _dateAdapter: DateAdapter<D>,
@Optional() @Inject(MD_DATE_FORMATS) private _dateFormats: MdDateFormats) {
if (!this._dateAdapter) {
Expand All @@ -139,6 +143,7 @@ export class MdCalendar<D> implements AfterContentInit {

ngAfterContentInit() {
this._activeDate = this.startAt || this._dateAdapter.today();
this._focusActiveCell();
this._monthView = this.startView != 'year';
}

Expand Down Expand Up @@ -187,14 +192,6 @@ export class MdCalendar<D> implements AfterContentInit {
return !this.maxDate || !this._isSameView(this._activeDate, this.maxDate);
}

/** Whether the two dates represent the same view in the current view mode (month or year). */
private _isSameView(date1: D, date2: D): boolean {
return this._monthView ?
this._dateAdapter.getYear(date1) == this._dateAdapter.getYear(date2) &&
this._dateAdapter.getMonth(date1) == this._dateAdapter.getMonth(date2) :
this._dateAdapter.getYear(date1) == this._dateAdapter.getYear(date2);
}

/** Handles keydown events on the calendar body. */
_handleCalendarBodyKeydown(event: KeyboardEvent): void {
// TODO(mmalerba): We currently allow keyboard navigation to disabled dates, but just prevent
Expand All @@ -207,6 +204,22 @@ export class MdCalendar<D> implements AfterContentInit {
}
}

/** Focuses the active cell after the microtask queue is empty. */
_focusActiveCell() {
this._ngZone.runOutsideAngular(() => this._ngZone.onStable.first().subscribe(() => {
let activeEl = this._elementRef.nativeElement.querySelector('.mat-calendar-body-active');
activeEl.focus();
}));
}

/** Whether the two dates represent the same view in the current view mode (month or year). */
private _isSameView(date1: D, date2: D): boolean {
return this._monthView ?
this._dateAdapter.getYear(date1) == this._dateAdapter.getYear(date2) &&
this._dateAdapter.getMonth(date1) == this._dateAdapter.getMonth(date2) :
this._dateAdapter.getYear(date1) == this._dateAdapter.getYear(date2);
}

/** Handles keydown events on the calendar body when calendar is in month view. */
private _handleCalendarBodyKeydownInMonthView(event: KeyboardEvent): void {
switch (event.keyCode) {
Expand Down Expand Up @@ -244,14 +257,17 @@ export class MdCalendar<D> implements AfterContentInit {
case ENTER:
if (this._dateFilterForViews(this._activeDate)) {
this._dateSelected(this._activeDate);
break;
// Prevent unexpected default actions such as form submission.
event.preventDefault();
}
return;
default:
// Don't prevent default on keys that we don't explicitly handle.
// Don't prevent default or focus active cell on keys that we don't explicitly handle.
return;
}

this._focusActiveCell();
// Prevent unexpected default actions such as form submission.
event.preventDefault();
}

Expand Down Expand Up @@ -290,10 +306,12 @@ export class MdCalendar<D> implements AfterContentInit {
this._monthSelected(this._activeDate);
break;
default:
// Don't prevent default on keys that we don't explicitly handle.
// Don't prevent default or focus active cell on keys that we don't explicitly handle.
return;
}

this._focusActiveCell();
// Prevent unexpected default actions such as form submission.
event.preventDefault();
}

Expand Down
3 changes: 2 additions & 1 deletion src/lib/datepicker/datepicker-toggle.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ $mat-datepicker-toggle-icon-size: 24px !default;

.mat-datepicker-toggle {
display: inline-block;
background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 0 24 24" fill="currentColor"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11zM7 10h5v5H7z"/></svg>') no-repeat;
// Note: SVG needs to be base64 encoded or it will not work on IE11.
background: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNHB4IiBoZWlnaHQ9IjI0cHgiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iY3VycmVudENvbG9yIj48cGF0aCBkPSJNMCAwaDI0djI0SDB6IiBmaWxsPSJub25lIi8+PHBhdGggZD0iTTE5IDNoLTFWMWgtMnYySDhWMUg2djJINWMtMS4xMSAwLTEuOTkuOS0xLjk5IDJMMyAxOWMwIDEuMS44OSAyIDIgMmgxNGMxLjEgMCAyLS45IDItMlY1YzAtMS4xLS45LTItMi0yem0wIDE2SDVWOGgxNHYxMXpNNyAxMGg1djVIN3oiLz48L3N2Zz4=') no-repeat;
background-size: contain;
height: $mat-datepicker-toggle-icon-size;
width: $mat-datepicker-toggle-icon-size;
Expand Down
26 changes: 23 additions & 3 deletions src/lib/datepicker/datepicker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import {
ChangeDetectionStrategy,
Component,
ComponentRef,
ElementRef,
EventEmitter,
Inject,
Input,
OnDestroy,
Optional,
Output,
ViewChild,
ViewContainerRef,
ViewEncapsulation
} from '@angular/core';
Expand All @@ -33,6 +33,8 @@ import {MdDialogConfig} from '../dialog/dialog-config';
import {DateAdapter} from '../core/datetime/index';
import {createMissingDateImplError} from './datepicker-errors';
import {MD_DATE_FORMATS, MdDateFormats} from '../core/datetime/date-formats';
import {ESCAPE} from '../core/keyboard/keycodes';
import {MdCalendar} from './calendar';


/** Used to generate a unique ID for each datepicker instance. */
Expand All @@ -54,17 +56,35 @@ let datepickerUid = 0;
host: {
'class': 'mat-datepicker-content',
'[class.mat-datepicker-content-touch]': 'datepicker.touchUi',
'(keydown)': '_handleKeydown($event)',
},
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MdDatepickerContent<D> implements AfterContentInit {
datepicker: MdDatepicker<D>;

constructor(private _elementRef: ElementRef) {}
@ViewChild(MdCalendar) _calendar: MdCalendar<D>;

ngAfterContentInit() {
this._elementRef.nativeElement.querySelector('.mat-calendar-content').focus();
this._calendar._focusActiveCell();
}

/**
* Handles keydown event on datepicker content.
* @param event The event.
*/
_handleKeydown(event: KeyboardEvent): void {
switch (event.keyCode) {
case ESCAPE:
this.datepicker.close();
break;
default:
// Return so that we don't preventDefault on keys that are not explicitly handled.
return;
}

event.preventDefault();
}
}

Expand Down
5 changes: 3 additions & 2 deletions src/lib/datepicker/month-view.html
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
<table class="mat-calendar-table">
<thead class="mat-calendar-table-header">
<tr><th *ngFor="let day of _weekdays">{{day}}</th></tr>
<tr><th class="mat-calendar-table-header-divider" colspan="7"></th></tr>
<tr><th *ngFor="let day of _weekdays" [attr.aria-label]="day.long">{{day.narrow}}</th></tr>
<tr><th class="mat-calendar-table-header-divider" colspan="7" aria-hidden="true"></th></tr>
</thead>
<tbody md-calendar-body
role="grid"
[label]="_monthLabel"
[rows]="_weeks"
[todayValue]="_todayDate"
Expand Down
18 changes: 12 additions & 6 deletions src/lib/datepicker/month-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export class MdMonthView<D> implements AfterContentInit {
_todayDate: number;

/** The names of the weekdays. */
_weekdays: string[];
_weekdays: {long: string, narrow: string}[];

constructor(@Optional() public _dateAdapter: DateAdapter<D>,
@Optional() @Inject(MD_DATE_FORMATS) private _dateFormats: MdDateFormats) {
Expand All @@ -91,9 +91,13 @@ export class MdMonthView<D> implements AfterContentInit {
}

const firstDayOfWeek = this._dateAdapter.getFirstDayOfWeek();
const weekdays = this._dateAdapter.getDayOfWeekNames('narrow');
const narrowWeekdays = this._dateAdapter.getDayOfWeekNames('narrow');
const longWeekdays = this._dateAdapter.getDayOfWeekNames('long');

// Rotate the labels for days of the week based on the configured first day of the week.
let weekdays = longWeekdays.map((long, i) => {
return {long, narrow: narrowWeekdays[i]};
});
this._weekdays = weekdays.slice(firstDayOfWeek).concat(weekdays.slice(0, firstDayOfWeek));

this._activeDate = this._dateAdapter.today();
Expand Down Expand Up @@ -140,12 +144,14 @@ export class MdMonthView<D> implements AfterContentInit {
this._weeks.push([]);
cell = 0;
}
let date = this._dateAdapter.createDate(
this._dateAdapter.getYear(this.activeDate),
this._dateAdapter.getMonth(this.activeDate), i + 1);
let enabled = !this.dateFilter ||
this.dateFilter(this._dateAdapter.createDate(
this._dateAdapter.getYear(this.activeDate),
this._dateAdapter.getMonth(this.activeDate), i + 1));
this.dateFilter(date);
let ariaLabel = this._dateAdapter.format(date, this._dateFormats.display.dateA11yLabel);
this._weeks[this._weeks.length - 1]
.push(new MdCalendarCell(i + 1, dateNames[i], enabled));
.push(new MdCalendarCell(i + 1, dateNames[i], ariaLabel, enabled));
}
}

Expand Down
1 change: 1 addition & 0 deletions src/lib/datepicker/year-view.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<tr><th class="mat-calendar-table-header-divider" colspan="7"></th></tr>
</thead>
<tbody md-calendar-body
role="grid"
allowDisabledSelection="true"
[label]="_yearLabel"
[rows]="_months"
Expand Down
5 changes: 4 additions & 1 deletion src/lib/datepicker/year-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,11 @@ export class MdYearView<D> implements AfterContentInit {

/** Creates an MdCalendarCell for the given month. */
private _createCellForMonth(month: number, monthName: string) {
let ariaLabel = this._dateAdapter.format(
this._dateAdapter.createDate(this._dateAdapter.getYear(this.activeDate), month, 1),
this._dateFormats.display.monthYearA11yLabel);
return new MdCalendarCell(
month, monthName.toLocaleUpperCase(), this._isMonthEnabled(month));
month, monthName.toLocaleUpperCase(), ariaLabel, this._isMonthEnabled(month));
}

/** Whether the given month is enabled. */
Expand Down