Skip to content

Commit 05c9a9a

Browse files
vandentsmattlewis92
authored andcommitted
feat: add accessibility support
Closes #941
1 parent 427e5bd commit 05c9a9a

21 files changed

+466
-20
lines changed

projects/angular-calendar/src/modules/calendar.module.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import {
33
CalendarCommonModule,
44
CalendarModuleConfig,
55
CalendarEventTitleFormatter,
6-
CalendarDateFormatter
6+
CalendarDateFormatter,
7+
CalendarA11y
78
} from './common/calendar-common.module';
89
import { CalendarMonthModule } from './month/calendar-month.module';
910
import { CalendarWeekModule } from './week/calendar-week.module';
@@ -55,7 +56,8 @@ export class CalendarModule {
5556
dateAdapter,
5657
config.eventTitleFormatter || CalendarEventTitleFormatter,
5758
config.dateFormatter || CalendarDateFormatter,
58-
config.utils || CalendarUtils
59+
config.utils || CalendarUtils,
60+
config.a11y || CalendarA11y
5961
]
6062
};
6163
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { MonthViewDay, CalendarEvent, EventAction } from 'calendar-utils';
2+
3+
/**
4+
* The parameters passed to the a11y methods.
5+
*/
6+
export interface A11yParams {
7+
/**
8+
* A day in the month view
9+
*/
10+
day?: MonthViewDay;
11+
12+
/**
13+
* A date
14+
*/
15+
date?: Date;
16+
17+
/**
18+
* A calendar event
19+
*/
20+
event?: CalendarEvent;
21+
22+
/**
23+
* Action button label e.g. 'Edit'
24+
*/
25+
action?: EventAction;
26+
27+
/**
28+
* Users preferred locale
29+
*/
30+
locale?: string;
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { Pipe, PipeTransform, LOCALE_ID, Inject } from '@angular/core';
2+
import { CalendarA11y } from './calendar-a11y.provider';
3+
import { A11yParams } from './calendar-a11y.interface';
4+
5+
/**
6+
* This pipe is primarily for rendering aria labels. Example usage:
7+
* ```typescript
8+
* // where `myEvent` is a `CalendarEvent` and myLocale is a locale identifier
9+
* {{ { event: myEvent, locale: myLocale } | calendarA11y: 'eventDescription' }}
10+
* ```
11+
*/
12+
@Pipe({
13+
name: 'calendarA11y'
14+
})
15+
export class CalendarA11yPipe implements PipeTransform {
16+
constructor(
17+
private calendarA11y: CalendarA11y,
18+
@Inject(LOCALE_ID) private locale: string
19+
) {}
20+
21+
transform(a11yParams: A11yParams, method: string): string {
22+
a11yParams.locale = a11yParams.locale || this.locale;
23+
if (typeof this.calendarA11y[method] === 'undefined') {
24+
const allowedMethods = Object.getOwnPropertyNames(
25+
Object.getPrototypeOf(CalendarA11y.prototype)
26+
).filter(iMethod => iMethod !== 'constructor');
27+
throw new Error(
28+
`${method} is not a valid a11y method. Can only be one of ${allowedMethods.join(
29+
', '
30+
)}`
31+
);
32+
}
33+
return this.calendarA11y[method](a11yParams);
34+
}
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import { Injectable } from '@angular/core';
2+
import { formatDate, I18nPluralPipe } from '@angular/common';
3+
import { A11yParams } from './calendar-a11y.interface';
4+
5+
/**
6+
* This class is responsible for adding accessibility to the calendar.
7+
* You may override any of its methods via angulars DI to suit your requirements.
8+
* For example:
9+
*
10+
* ```typescript
11+
* import { A11yParams, CalendarA11y } from 'angular-calendar';
12+
* import { formatDate, I18nPluralPipe } from '@angular/common';
13+
*
14+
* // adding your own a11y params
15+
* export interface CustomA11yParams extends A11yParams {
16+
* isDrSuess?: boolean;
17+
* }
18+
*
19+
* export class CustomCalendarA11y extends CalendarA11y {
20+
* constructor(protected i18nPlural: I18nPluralPipe) {
21+
* super(i18nPlural);
22+
* }
23+
*
24+
* // overriding a function
25+
* public openDayEventsLandmark({ date, locale, isDrSuess }: CustomA11yParams): string {
26+
* if (isDrSuess) {
27+
* return `
28+
* ${formatDate(date, 'EEEE MMMM d', locale)}
29+
* Today you are you! That is truer than true! There is no one alive
30+
* who is you-er than you!
31+
* `;
32+
* }
33+
* }
34+
* }
35+
*
36+
* // in your component that uses the calendar
37+
* providers: [{
38+
* provide: CalendarA11y,
39+
* useClass: CustomCalendarA11y
40+
* }]
41+
* ```
42+
*/
43+
@Injectable()
44+
export class CalendarA11y {
45+
constructor(protected i18nPlural: I18nPluralPipe) {}
46+
47+
/**
48+
* Aria label for the badges/date of a cell
49+
* @example: `Saturday October 19 1 event click to expand`
50+
*/
51+
public monthCell({ day, locale }: A11yParams): string {
52+
if (day.badgeTotal > 0) {
53+
return `
54+
${formatDate(day.date, 'EEEE MMMM d', locale)},
55+
${this.i18nPlural.transform(day.badgeTotal, {
56+
'=0': 'No events',
57+
'=1': 'One event',
58+
other: '# events'
59+
})},
60+
click to expand
61+
`;
62+
} else {
63+
return `${formatDate(day.date, 'EEEE MMMM d', locale)}`;
64+
}
65+
}
66+
67+
/**
68+
* Aria label for the open day events start landmark
69+
* @example: `Saturday October 19 expanded view`
70+
*/
71+
public openDayEventsLandmark({ date, locale }: A11yParams): string {
72+
return `
73+
Beginning of expanded view for ${formatDate(date, 'EEEE MMMM dd', locale)}
74+
`;
75+
}
76+
77+
/**
78+
* Aria label for alert that a day in the month view was expanded
79+
* @example: `Saturday October 19 expanded`
80+
*/
81+
public openDayEventsAlert({ date, locale }: A11yParams): string {
82+
return `${formatDate(date, 'EEEE MMMM dd', locale)} expanded`;
83+
}
84+
85+
/**
86+
* Descriptive aria label for an event
87+
* @example: `Saturday October 19th, Scott's Pizza Party, from 11:00am to 5:00pm`
88+
*/
89+
public eventDescription({ event, locale }: A11yParams): string {
90+
if (event.allDay === true) {
91+
return this.allDayEventDescription({ event, locale });
92+
}
93+
94+
const aria = `
95+
${formatDate(event.start, 'EEEE MMMM dd', locale)},
96+
${event.title}, from ${formatDate(event.start, 'hh:mm a', locale)}
97+
`;
98+
if (event.end) {
99+
return aria + ` to ${formatDate(event.end, 'hh:mm a', locale)}`;
100+
}
101+
return aria;
102+
}
103+
104+
/**
105+
* Descriptive aria label for an all day event
106+
* @example:
107+
* `Scott's Party, event spans multiple days: start time October 19 5:00pm, no stop time`
108+
*/
109+
public allDayEventDescription({ event, locale }: A11yParams): string {
110+
const aria = `
111+
${event.title}, event spans multiple days:
112+
start time ${formatDate(event.start, 'MMMM dd hh:mm a', locale)}
113+
`;
114+
if (event.end) {
115+
return (
116+
aria + `, stop time ${formatDate(event.end, 'MMMM d hh:mm a', locale)}`
117+
);
118+
}
119+
return aria + `, no stop time`;
120+
}
121+
122+
/**
123+
* Aria label for the calendar event actions icons
124+
* @returns 'Edit' for fa-pencil icons, and 'Delete' for fa-times icons
125+
*/
126+
public actionButtonLabel({ action }: A11yParams): string {
127+
return action.a11yLabel;
128+
}
129+
130+
/**
131+
* @returns {number} Tab index to be given to month cells
132+
*/
133+
public monthCellTabIndex(): number {
134+
return 0;
135+
}
136+
137+
/**
138+
* @returns true if the events inside the month cell should be aria-hidden
139+
*/
140+
public hideMonthCellEvents(): boolean {
141+
return true;
142+
}
143+
144+
/**
145+
* @returns true if event titles should be aria-hidden (global)
146+
*/
147+
public hideEventTitle(): boolean {
148+
return true;
149+
}
150+
151+
/**
152+
* @returns true if hour segments in the week view should be aria-hidden
153+
*/
154+
public hideWeekHourSegment(): boolean {
155+
return true;
156+
}
157+
158+
/**
159+
* @returns true if hour segments in the day view should be aria-hidden
160+
*/
161+
public hideDayHourSegment(): boolean {
162+
return true;
163+
}
164+
}

projects/angular-calendar/src/modules/common/calendar-common.module.ts

+16-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ModuleWithProviders, NgModule, Provider } from '@angular/core';
2-
import { CommonModule } from '@angular/common';
2+
import { CommonModule, I18nPluralPipe } from '@angular/common';
33
import { CalendarEventActionsComponent } from './calendar-event-actions.component';
44
import { CalendarEventTitleComponent } from './calendar-event-title.component';
55
import {
@@ -12,14 +12,18 @@ import { CalendarTodayDirective } from './calendar-today.directive';
1212
import { CalendarDatePipe } from './calendar-date.pipe';
1313
import { CalendarEventTitlePipe } from './calendar-event-title.pipe';
1414
import { ClickDirective } from './click.directive';
15+
import { KeydownEnterDirective } from './keydown-enter.directive';
1516
import { CalendarEventTitleFormatter } from './calendar-event-title-formatter.provider';
1617
import { CalendarDateFormatter } from './calendar-date-formatter.provider';
1718
import { CalendarUtils } from './calendar-utils.provider';
19+
import { CalendarA11y } from './calendar-a11y.provider';
20+
import { CalendarA11yPipe } from './calendar-a11y.pipe';
1821

1922
export interface CalendarModuleConfig {
2023
eventTitleFormatter?: Provider;
2124
dateFormatter?: Provider;
2225
utils?: Provider;
26+
a11y?: Provider;
2327
}
2428

2529
export * from './calendar-event-title-formatter.provider';
@@ -28,6 +32,8 @@ export * from './calendar-native-date-formatter.provider';
2832
export * from './calendar-angular-date-formatter.provider';
2933
export * from './calendar-date-formatter.provider';
3034
export * from './calendar-utils.provider';
35+
export * from './calendar-a11y.provider';
36+
export * from './calendar-a11y.interface';
3137
export * from './calendar-date-formatter.interface';
3238
export * from './calendar-event-times-changed-event.interface';
3339
export * from '../../date-adapters/date-adapter';
@@ -67,7 +73,9 @@ export {
6773
CalendarTodayDirective,
6874
CalendarDatePipe,
6975
CalendarEventTitlePipe,
70-
ClickDirective
76+
CalendarA11yPipe,
77+
ClickDirective,
78+
KeydownEnterDirective
7179
],
7280
imports: [CommonModule],
7381
exports: [
@@ -80,8 +88,11 @@ export {
8088
CalendarTodayDirective,
8189
CalendarDatePipe,
8290
CalendarEventTitlePipe,
83-
ClickDirective
91+
CalendarA11yPipe,
92+
ClickDirective,
93+
KeydownEnterDirective
8494
],
95+
providers: [I18nPluralPipe],
8596
entryComponents: [CalendarTooltipWindowComponent]
8697
})
8798
export class CalendarCommonModule {
@@ -95,7 +106,8 @@ export class CalendarCommonModule {
95106
dateAdapter,
96107
config.eventTitleFormatter || CalendarEventTitleFormatter,
97108
config.dateFormatter || CalendarDateFormatter,
98-
config.utils || CalendarUtils
109+
config.utils || CalendarUtils,
110+
config.a11y || CalendarA11y
99111
]
100112
};
101113
}

projects/angular-calendar/src/modules/common/calendar-event-actions.component.ts

+6
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,14 @@ import { CalendarEvent, EventAction } from 'calendar-utils';
1515
href="javascript:;"
1616
*ngFor="let action of event.actions; trackBy: trackByActionId"
1717
(mwlClick)="action.onClick({ event: event })"
18+
(mwlKeydownEnter)="action.onClick({ event: event })"
1819
[ngClass]="action.cssClass"
1920
[innerHtml]="action.label"
21+
tabindex="0"
22+
role="button"
23+
[attr.aria-label]="
24+
{ action: action } | calendarA11y: 'actionButtonLabel'
25+
"
2026
>
2127
</a>
2228
</span>

projects/angular-calendar/src/modules/common/calendar-event-title.component.ts

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { CalendarEvent } from 'calendar-utils';
88
<span
99
class="cal-event-title"
1010
[innerHTML]="event.title | calendarEventTitle: view:event"
11+
[attr.aria-hidden]="{} | calendarA11y: 'hideEventTitle'"
1112
>
1213
</span>
1314
</ng-template>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Directive, Output, EventEmitter, HostListener } from '@angular/core';
2+
3+
@Directive({
4+
selector: '[mwlKeydownEnter]'
5+
})
6+
export class KeydownEnterDirective {
7+
@Output('mwlKeydownEnter') keydown = new EventEmitter<KeyboardEvent>(); // tslint:disable-line
8+
9+
@HostListener('keydown', ['$event'])
10+
onKeyPress(event: KeyboardEvent) {
11+
if (event.keyCode === 13 || event.which === 13 || event.key === 'Enter') {
12+
event.preventDefault();
13+
event.stopPropagation();
14+
this.keydown.emit(event);
15+
}
16+
}
17+
}

projects/angular-calendar/src/modules/day/calendar-day-view-event.component.ts

+9
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ import { PlacementArray } from 'positioning';
3535
[tooltipAppendToBody]="tooltipAppendToBody"
3636
[tooltipDelay]="tooltipDelay"
3737
(mwlClick)="eventClicked.emit()"
38+
(mwlKeydownEnter)="eventClicked.emit()"
39+
tabindex="0"
40+
role="application"
41+
[attr.aria-label]="
42+
{ event: dayEvent.event, locale: locale }
43+
| calendarA11y: 'eventDescription'
44+
"
3845
>
3946
<mwl-calendar-event-actions
4047
[event]="dayEvent.event"
@@ -65,6 +72,8 @@ import { PlacementArray } from 'positioning';
6572
`
6673
})
6774
export class CalendarDayViewEventComponent {
75+
@Input() locale: string;
76+
6877
@Input() dayEvent: DayViewEvent;
6978

7079
@Input() tooltipPlacement: PlacementArray;

0 commit comments

Comments
 (0)