Skip to content

Commit eba3f7f

Browse files
authored
Improve accessibility (#553)
* Improve accessibility * Fix lint * Add missing `isDatePicker` to range / multi calendar * Add `@autofocus` parameter and implement
1 parent 2fcc101 commit eba3f7f

21 files changed

Lines changed: 537 additions & 36 deletions

File tree

docs/app/components/snippets/datepicker-1.hbs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
<BasicDropdown as |dropdown|>
2-
{{! template-lint-disable require-input-label }}
2+
<div>
3+
<label for="datepicker-1">Date picker</label>
4+
</div>
5+
36
<input
47
type="text"
58
data-ebd-id="{{dropdown.uniqueId}}-trigger"
69
class="datepicker-demo-input"
7-
onclick={{dropdown.actions.toggle}}
810
value={{if this.selected (format-date this.selected "DD-MM-YYYY")}}
11+
id="datepicker-1"
912
readonly
13+
{{on "focus" dropdown.actions.toggle}}
1014
/>
1115

1216
<dropdown.Content class="datepicker-demo-dropdown">
@@ -16,6 +20,8 @@
1620
@onCenterChange={{this.onCenterChange}}
1721
@selected={{this.selected}}
1822
@onSelect={{this.onSelect}}
23+
@isDatePicker={{true}}
24+
@autofocus={{true}}
1925
as |calendar|
2026
>
2127
<calendar.Nav />

docs/app/components/snippets/multiple-selection-1.hbs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
@center={{this.center}}
33
@selected={{this.collection}}
44
@onSelect={{this.onSelect}}
5+
@onCenterChange={{this.onCenterChange}}
56
as |calendar|
67
>
78
<calendar.Nav />

docs/app/components/snippets/multiple-selection-1.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ export default class extends Component {
66
@tracked center = new Date('2016-05-17');
77
@tracked collection = [];
88

9+
@action
10+
onCenterChange(selected) {
11+
this.center = selected.date;
12+
}
13+
914
@action
1015
onSelect(selected) {
1116
this.collection = selected.date;

ember-power-calendar/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
"@embroider/macros": "^1.17.3",
6969
"ember-assign-helper": "^0.5.1",
7070
"ember-element-helper": "^0.8.7",
71+
"ember-modifier": "^4.2.0",
7172
"ember-truth-helpers": "^4.0.3"
7273
},
7374
"devDependencies": {

ember-power-calendar/src/-private/days-utils.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
type PowerCalendarDay,
1515
} from '../utils.ts';
1616

17+
export const DAY_IN_MS = 86400000;
1718
export const WEEK_DAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
1819
export type TWeekdayFormat = 'min' | 'short' | 'long';
1920

@@ -95,13 +96,13 @@ export function handleDayKeyDown(
9596
}
9697
}
9798

98-
if (!index) {
99+
if (index === undefined) {
99100
return;
100101
}
101102

102103
if (e.key === 'ArrowUp') {
103104
e.preventDefault();
104-
const newIndex = Math.max(index - 7, 0);
105+
const newIndex = index - 7;
105106
day = days[newIndex];
106107
if (day?.isDisabled) {
107108
for (let i = newIndex + 1; i <= index; i++) {
@@ -113,7 +114,7 @@ export function handleDayKeyDown(
113114
}
114115
} else if (e.key === 'ArrowDown') {
115116
e.preventDefault();
116-
const newIndex = Math.min(index + 7, days.length - 1);
117+
const newIndex = index + 7;
117118
day = days[newIndex];
118119
if (day?.isDisabled) {
119120
for (let i = newIndex - 1; i >= index; i--) {
@@ -124,12 +125,12 @@ export function handleDayKeyDown(
124125
}
125126
}
126127
} else if (e.key === 'ArrowLeft') {
127-
day = days[Math.max(index - 1, 0)];
128+
day = days[index - 1];
128129
if (day?.isDisabled) {
129130
return;
130131
}
131132
} else if (e.key === 'ArrowRight') {
132-
day = days[Math.min(index + 1, days.length - 1)];
133+
day = days[index + 1];
133134
if (day?.isDisabled) {
134135
return;
135136
}
@@ -243,7 +244,7 @@ export function handleClick(
243244
e: MouseEvent,
244245
days: PowerCalendarDay[],
245246
calendar: CalendarAPI,
246-
): void {
247+
): PowerCalendarDay | undefined {
247248
const dayEl: HTMLElement | null | undefined = (
248249
e.target as HTMLElement | null
249250
)?.closest('[data-date]');
@@ -254,6 +255,8 @@ export function handleClick(
254255
if (calendar.actions.select) {
255256
calendar.actions.select(day, calendar, e);
256257
}
258+
259+
return day;
257260
}
258261
}
259262
}

ember-power-calendar/src/-private/utils.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@ export function publicActionsObject(
2525
| undefined,
2626
changeCenterTask: TaskForAsyncTaskFunction<
2727
unknown,
28-
(newCenter: Date, calendar: CalendarAPI, e: MouseEvent) => Promise<void>
28+
(
29+
newCenter: Date,
30+
calendar: CalendarAPI,
31+
e: MouseEvent | KeyboardEvent,
32+
) => Promise<void>
2933
>,
3034
currentCenter: Date,
3135
): PowerCalendarActions {
@@ -37,7 +41,7 @@ export function publicActionsObject(
3741
const changeCenter = (
3842
newCenter: Date,
3943
calendar: CalendarAPI,
40-
e: MouseEvent,
44+
e: MouseEvent | KeyboardEvent,
4145
) => {
4246
return changeCenterTask.perform(newCenter, calendar, e);
4347
};

ember-power-calendar/src/components/power-calendar-multiple.hbs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,32 @@
55
Nav=(component
66
(ensure-safe-component (or @navComponent this.navComponent))
77
calendar=(readonly this.publicAPI)
8+
isDatePicker=@isDatePicker
89
)
910
Days=(component
1011
(ensure-safe-component (or @daysComponent this.daysComponent))
1112
calendar=(readonly this.publicAPI)
13+
isDatePicker=@isDatePicker
14+
autofocus=@autofocus
1215
)
1316
)
1417
)
1518
as |calendar|
1619
}}
1720
{{#let (element this.tagWithDefault) as |Tag|}}
18-
<Tag class="ember-power-calendar" ...attributes id={{calendar.uniqueId}}>
21+
<Tag
22+
class="ember-power-calendar"
23+
role={{if @isDatePicker "dialog" "group"}}
24+
aria-modal={{if @isDatePicker "true"}}
25+
aria-label={{if
26+
@ariaLabel
27+
@ariaLabel
28+
(unless @ariaLabeledBy "Choose Multiple Dates")
29+
}}
30+
aria-labelledby={{@ariaLabeledBy}}
31+
...attributes
32+
id={{calendar.uniqueId}}
33+
>
1934
{{#if (has-block)}}
2035
{{yield calendar}}
2136
{{else}}

ember-power-calendar/src/components/power-calendar-multiple/days.hbs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,38 @@
22
<div
33
class="ember-power-calendar-days"
44
data-power-calendar-id={{or @calendar.calendarUniqueId @calendar.uniqueId}}
5+
role="grid"
6+
aria-labelledby="ember-power-calendar-nav-title-{{@calendar.uniqueId}}"
57
{{on "click" this.handleClick}}
8+
{{this.setup}}
69
...attributes
710
>
8-
<div class="ember-power-calendar-row ember-power-calendar-weekdays">
11+
<div
12+
class="ember-power-calendar-row ember-power-calendar-weekdays"
13+
role="row"
14+
>
915
{{#each this.weekdaysNames as |wdn|}}
10-
<div class="ember-power-calendar-weekday">{{wdn}}</div>
16+
<div
17+
class="ember-power-calendar-weekday"
18+
role="columnheader"
19+
>{{wdn}}</div>
1120
{{/each}}
1221
</div>
1322
<div
1423
class="ember-power-calendar-day-grid"
24+
role="rowgroup"
1525
{{on "keydown" this.handleKeyDown}}
1626
>
1727
{{#each this.weeks key="id" as |week|}}
1828
<div
1929
class="ember-power-calendar-row ember-power-calendar-week"
30+
role="row"
2031
data-missing-days={{week.missingDays}}
2132
>
2233
{{#each week.days key="id" as |day|}}
2334
<button
2435
type="button"
36+
role="gridcell"
2537
data-date="{{day.id}}"
2638
class={{ember-power-calendar-day-classes
2739
day
@@ -32,6 +44,8 @@
3244
{{on "focus" this.handleDayFocus}}
3345
{{on "blur" this.handleDayBlur}}
3446
disabled={{day.isDisabled}}
47+
tabindex={{if day.isFocused "0" "-1"}}
48+
aria-selected={{if day.isSelected "true"}}
3549
>
3650
{{#if (has-block)}}
3751
{{yield day @calendar this.weeks}}

ember-power-calendar/src/components/power-calendar-multiple/days.ts

Lines changed: 120 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { scheduleOnce } from '@ember/runloop';
55
import service from '../../-private/service.ts';
66
import {
77
add,
8+
formatDate,
89
getWeekdays,
910
getWeekdaysMin,
1011
getWeekdaysShort,
@@ -35,9 +36,11 @@ import type {
3536
PowerCalendarDaysArgs,
3637
PowerCalendarDaysSignature,
3738
} from '../power-calendar/days.ts';
39+
import { modifier } from 'ember-modifier';
3840

3941
interface PowerCalendarMultipleDaysArgs
40-
extends Omit<PowerCalendarDaysArgs, 'selected'> {
42+
extends Omit<PowerCalendarDaysArgs, 'calendar' | 'selected'> {
43+
calendar: PowerCalendarMultipleAPI;
4144
selected?: Date[];
4245
maxLength?: number;
4346
}
@@ -52,6 +55,8 @@ export default class PowerCalendarMultipleDaysComponent extends Component<PowerC
5255

5356
@tracked focusedId: string | null = null;
5457

58+
didSetup = false;
59+
5560
get weekdayFormat(): TWeekdayFormat {
5661
return this.args.weekdayFormat || 'short'; // "min" | "short" | "long"
5762
}
@@ -151,11 +156,48 @@ export default class PowerCalendarMultipleDaysComponent extends Component<PowerC
151156
}
152157

153158
@action
154-
handleKeyDown(e: KeyboardEvent): void {
159+
async handleKeyDown(e: KeyboardEvent): Promise<void> {
155160
const day = handleDayKeyDown(e, this.focusedId, this.days);
161+
162+
if (!day || !day?.isCurrentMonth) {
163+
if (this.args.calendar.actions.moveCenter) {
164+
if (
165+
e.key === 'ArrowUp' ||
166+
e.key === 'ArrowRight' ||
167+
e.key === 'ArrowDown' ||
168+
e.key === 'ArrowLeft'
169+
) {
170+
const currentDay = this.days.find(
171+
(x) => x.id === this.focusedId,
172+
)?.date;
173+
174+
if (currentDay) {
175+
let date = currentDay;
176+
let step = 1;
177+
if (e.key === 'ArrowUp') {
178+
date = add(currentDay, -7, 'day');
179+
step = -1;
180+
} else if (e.key === 'ArrowLeft') {
181+
date = add(currentDay, -1, 'day');
182+
step = -1;
183+
} else if (e.key === 'ArrowRight') {
184+
date = add(currentDay, 1, 'day');
185+
} else if (e.key === 'ArrowDown') {
186+
date = add(currentDay, 7, 'day');
187+
}
188+
189+
await this.focusDay(e, date, step);
190+
191+
return;
192+
}
193+
}
194+
}
195+
}
196+
156197
if (!day) {
157198
return;
158199
}
200+
159201
this.focusedId = day.id;
160202
scheduleOnce(
161203
'afterRender',
@@ -171,6 +213,82 @@ export default class PowerCalendarMultipleDaysComponent extends Component<PowerC
171213
handleClick(e, this.days, this.args.calendar);
172214
}
173215

216+
setup = modifier(
217+
() => {
218+
if (this.didSetup) {
219+
return;
220+
}
221+
222+
this.didSetup = true;
223+
224+
if (this.args.autofocus) {
225+
scheduleOnce('afterRender', this, this.initialFocus.bind(this));
226+
}
227+
},
228+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
229+
// @ts-ignore
230+
{ eager: false },
231+
);
232+
233+
initialFocus() {
234+
const activeDay = this.days.find((x) => x.isSelected && !x.isDisabled);
235+
236+
if (activeDay) {
237+
this.focusedId = activeDay.id;
238+
} else {
239+
const todayDay = this.days.find((x) => x.isToday && !x.isDisabled);
240+
if (todayDay) {
241+
this.focusedId = todayDay.id ?? '';
242+
} else {
243+
const firstSelectableDay = this.days.find((x) => !x.isDisabled);
244+
if (firstSelectableDay) {
245+
this.focusedId = firstSelectableDay.id ?? '';
246+
} else {
247+
this.focusedId = this.days.find((x) => !x.isCurrentMonth)?.id ?? '';
248+
}
249+
}
250+
}
251+
252+
focusDate(this.args.calendar.uniqueId, this.focusedId ?? '');
253+
}
254+
255+
async focusDay(e: MouseEvent | KeyboardEvent, date: Date, step: number = 0) {
256+
if (
257+
dayIsDisabled(
258+
date,
259+
this.args.calendar,
260+
this.args.minDate,
261+
this.args.maxDate,
262+
this.args.disabledDates,
263+
)
264+
) {
265+
return;
266+
}
267+
268+
if (this.args.calendar.actions.moveCenter && step !== 0) {
269+
await this.args.calendar.actions.moveCenter(
270+
step,
271+
'month',
272+
this.args.calendar,
273+
e,
274+
);
275+
}
276+
277+
this.focusedId = formatDate(date, 'YYYY-MM-DD');
278+
279+
if (step !== 0) {
280+
scheduleOnce(
281+
'afterRender',
282+
this,
283+
focusDate,
284+
this.args.calendar.uniqueId,
285+
this.focusedId ?? '',
286+
);
287+
} else {
288+
focusDate(this.args.calendar.uniqueId, this.focusedId ?? '');
289+
}
290+
}
291+
174292
// Methods
175293
dayIsSelected(date: Date, calendar: CalendarAPI = this.args.calendar) {
176294
const selected = (calendar as PowerCalendarMultipleAPI).selected || [];

0 commit comments

Comments
 (0)