Skip to content
This repository was archived by the owner on Oct 12, 2023. It is now read-only.

Commit 7188905

Browse files
authored
Merge pull request #23 from misteinb/bugfix/date-picker-keyboard-nav
Bugfix/date picker keyboard nav
2 parents 7f9a406 + b5c3da8 commit 7188905

File tree

8 files changed

+205
-240
lines changed

8 files changed

+205
-240
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# CHANGELOG
22

3+
## v3.0.0
4+
### Changed
5+
- calendar api changed. no longer extends drop down
6+
### Fixed
7+
- calendar in datepicker is now reachable via keyboard
8+
39
## v2.0.5
410
### Fixed
511
- date picker should not open when input receives focus

lib/components/DateTime/Calendar.scss

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ $line-height-row: $calendar-column-width;
6767
&.disabled {
6868
pointer-events: none;
6969
}
70+
71+
&:focus {
72+
outline-offset: -1px;
73+
}
7074
}
7175
}
7276

@@ -100,9 +104,9 @@ $line-height-row: $calendar-column-width;
100104
background-color: themed('color-bg-btn-primary-rest');
101105

102106
&:focus {
103-
outline: none;
104-
color: themed('color-text-rest');
105-
background-color: transparent;
107+
outline: 1px dashed themed('color-outline-btn-primary-focus');
108+
background-color: themed('color-bg-btn-primary-focus');
109+
outline-offset: -2px;
106110
}
107111
}
108112
}
@@ -131,9 +135,8 @@ $line-height-row: $calendar-column-width;
131135
color: inherit;
132136
}
133137

134-
&:focus {
135-
outline-offset: -1px;
136-
background-color: transparent !important;
138+
&:focus:not(.selected) {
139+
outline-offset: -2px;
137140
@include themify{
138141
outline: 1px dashed themed('color-border-focus');
139142
}

lib/components/DateTime/Calendar.tsx

Lines changed: 104 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,6 @@ export interface CalendarProps extends React.Props<CalendarComponentType> {
3636
*/
3737
localTimezone?: boolean;
3838

39-
/** Tab index of calendar buttons */
40-
tabIndex?: number;
41-
4239
/**
4340
* Callback for date change events
4441
* */
@@ -67,7 +64,6 @@ export interface CalendarState {
6764
export class Calendar extends React.Component<CalendarProps, Partial<CalendarState>> {
6865
static defaultProps = {
6966
localTimezone: true,
70-
tabIndex: -1,
7167
attr: {
7268
container: {},
7369
header: {},
@@ -84,8 +80,10 @@ export class Calendar extends React.Component<CalendarProps, Partial<CalendarSta
8480
private value: MethodDate;
8581
private monthNames: string[];
8682
private dayNames: string[];
87-
private buttons: { [date: string]: HTMLButtonElement };
88-
private buttonIndex: number;
83+
private _container: HTMLDivElement;
84+
private nextFocusRow?: number;
85+
private nextFocusCol?: number;
86+
8987

9088
constructor(props: CalendarProps) {
9189
const locale = navigator['userLanguage'] || (navigator.language || 'en-us');
@@ -117,15 +115,10 @@ export class Calendar extends React.Component<CalendarProps, Partial<CalendarSta
117115

118116
this.dayNames = getLocalWeekdays(locale);
119117

120-
this.buttons = {};
121-
this.buttonIndex = 0;
122-
this.dayRef = this.dayRef.bind(this);
123-
118+
this.onPrevMonth = this.onPrevMonth.bind(this);
119+
this.onNextMonth = this.onNextMonth.bind(this);
124120
this.onKeyDown = this.onKeyDown.bind(this);
125-
}
126-
127-
get focusedButton(): HTMLButtonElement {
128-
return this.buttons[this.state.currentDate.date - 1];
121+
this.setContainerRef = this.setContainerRef.bind(this);
129122
}
130123

131124
public startAccessibility() {
@@ -144,28 +137,6 @@ export class Calendar extends React.Component<CalendarProps, Partial<CalendarSta
144137
});
145138
}
146139

147-
dayRef(element: HTMLButtonElement) {
148-
if (element) {
149-
this.buttons[this.buttonIndex] = element;
150-
this.buttonIndex++;
151-
}
152-
}
153-
154-
componentWillMount() {
155-
window.addEventListener('keydown', this.onKeyDown);
156-
}
157-
158-
componentWillUnmount() {
159-
window.removeEventListener('keydown', this.onKeyDown);
160-
}
161-
162-
componentDidUpdate(oldProps: CalendarProps, oldState: CalendarState) {
163-
if (this.state.accessibility && this.state.currentDate !== oldState.currentDate) {
164-
this.focusedButton.focus();
165-
}
166-
this.buttonIndex = 0;
167-
}
168-
169140
componentWillReceiveProps(newProps: CalendarProps) {
170141
const date = this.state.currentDate.copy();
171142
let update = false;
@@ -195,75 +166,14 @@ export class Calendar extends React.Component<CalendarProps, Partial<CalendarSta
195166
}
196167
}
197168

198-
onKeyDown(event) {
199-
if (!this.state.accessibility) {
200-
return;
201-
}
202-
/** So that we don't block any browser shortcuts */
203-
if (event.ctrlKey || event.altKey) {
204-
return;
205-
}
206-
if (document.activeElement === this.focusedButton) {
207-
const date = this.state.currentDate.copy();
208-
let detached = this.state.detached;
209-
let newDay = date.date;
210-
let newMonth = date.month;
211-
let newYear = date.year;
212-
let weekMove = false;
213-
switch (event.keyCode) {
214-
case keyCode.left:
215-
newDay -= 1;
216-
break;
217-
case keyCode.right:
218-
newDay += 1;
219-
break;
220-
case keyCode.up:
221-
weekMove = true;
222-
newDay -= 7;
223-
break;
224-
case keyCode.down:
225-
weekMove = true;
226-
newDay += 7;
227-
break;
228-
case keyCode.pageup:
229-
if (event.ctrlKey) {
230-
newYear -= 1;
231-
} else {
232-
newMonth -= 1;
233-
}
234-
break;
235-
case keyCode.pagedown:
236-
if (event.ctrlKey) {
237-
newYear += 1;
238-
} else {
239-
newMonth += 1;
240-
}
241-
break;
242-
case keyCode.home:
243-
newDay = 1;
244-
break;
245-
case keyCode.end:
246-
newDay = 0;
247-
newMonth += 1;
248-
break;
249-
default:
250-
return;
251-
}
252-
date.year = newYear;
253-
date.month = newMonth;
254-
date.date = newDay;
255-
256-
if (newDay > 0 && date.date !== newDay && !weekMove) {
257-
date.month += 1;
258-
date.date = 0;
169+
componentDidUpdate() {
170+
if (this.nextFocusRow != null && this.nextFocusCol != null) {
171+
const nextFocus = this._container.querySelectorAll(`[data-row="${this.nextFocusRow}"][data-col="${this.nextFocusCol}"]`)[0] as HTMLElement;
172+
if (nextFocus != null) {
173+
nextFocus.focus();
259174
}
260-
261-
event.stopPropagation();
262-
event.preventDefault();
263-
this.setState({
264-
currentDate: date,
265-
detached: detached
266-
});
175+
this.nextFocusRow = undefined;
176+
this.nextFocusCol = undefined;
267177
}
268178
}
269179

@@ -289,6 +199,16 @@ export class Calendar extends React.Component<CalendarProps, Partial<CalendarSta
289199
onPrevMonth(event) {
290200
event.preventDefault();
291201

202+
this.decrementMonth();
203+
}
204+
205+
onNextMonth(event) {
206+
event.preventDefault();
207+
208+
this.incrementMonth();
209+
}
210+
211+
decrementMonth() {
292212
/** Dates are mutable so we're going to copy it over */
293213
const newDate = this.state.currentDate.copy();
294214
const curDate = newDate.date;
@@ -303,9 +223,7 @@ export class Calendar extends React.Component<CalendarProps, Partial<CalendarSta
303223
this.setState({ currentDate: newDate, detached: true });
304224
}
305225

306-
onNextMonth(event) {
307-
event.preventDefault();
308-
226+
incrementMonth() {
309227
/** Dates are mutable so we're going to copy it over */
310228
const newDate = this.state.currentDate.copy();
311229
const curDate = newDate.date;
@@ -319,14 +237,77 @@ export class Calendar extends React.Component<CalendarProps, Partial<CalendarSta
319237
this.setState({ currentDate: newDate, detached: true });
320238
}
321239

240+
onKeyDown(e: React.KeyboardEvent<any>) {
241+
const element: HTMLElement = e.currentTarget;
242+
const row = parseInt(element.getAttribute('data-row'), 10);
243+
const col = parseInt(element.getAttribute('data-col'), 10);
244+
245+
if (!isNaN(row) && !isNaN(col)) {
246+
let nextRow = row;
247+
let nextCol = col;
248+
let nextFocus: HTMLElement;
249+
switch (e.keyCode) {
250+
case keyCode.pagedown:
251+
e.preventDefault();
252+
e.stopPropagation();
253+
this.nextFocusCol = nextCol;
254+
this.nextFocusRow = nextRow;
255+
this.incrementMonth();
256+
break;
257+
case keyCode.pageup:
258+
e.preventDefault();
259+
e.stopPropagation();
260+
this.nextFocusCol = nextCol;
261+
this.nextFocusRow = nextRow;
262+
this.decrementMonth();
263+
break;
264+
case keyCode.up:
265+
e.preventDefault();
266+
e.stopPropagation();
267+
nextRow -= 1;
268+
break;
269+
case keyCode.down:
270+
e.preventDefault();
271+
e.stopPropagation();
272+
nextRow += 1;
273+
break;
274+
case keyCode.left:
275+
e.preventDefault();
276+
e.stopPropagation();
277+
nextCol -= 1;
278+
if (nextCol < 0) {
279+
nextCol = 6;
280+
nextRow -= 1;
281+
}
282+
break;
283+
case keyCode.right:
284+
e.preventDefault();
285+
e.stopPropagation();
286+
nextCol += 1;
287+
if (nextCol > 6) {
288+
nextCol = 0;
289+
nextRow += 1;
290+
}
291+
break;
292+
}
293+
nextFocus = this._container.querySelectorAll(`[data-row="${nextRow}"][data-col="${nextCol}"]`)[0] as HTMLElement;
294+
// if we found the next button to focus on, focus it
295+
if (nextFocus != null) {
296+
nextFocus.focus();
297+
}
298+
}
299+
}
300+
301+
setContainerRef(element: HTMLDivElement) {
302+
this._container = element;
303+
}
304+
322305
render() {
323306
const rowClassName = css('calendar-row');
324307
const colClassName = css('disabled');
325-
const tabIndex = this.props.tabIndex;
326308

327309
const curYear = this.state.currentDate.year;
328310
const curMonth = this.state.currentDate.month;
329-
const curDate = this.state.currentDate.date;
330311

331312
const weekdays = this.dayNames.map(day => {
332313
return (
@@ -368,6 +349,8 @@ export class Calendar extends React.Component<CalendarProps, Partial<CalendarSta
368349
event.preventDefault();
369350
};
370351

352+
// TODO aria-label with date
353+
371354
const date = col.date;
372355
const colMonth = col.month;
373356
const key = `${colMonth}-${date}`;
@@ -376,10 +359,12 @@ export class Calendar extends React.Component<CalendarProps, Partial<CalendarSta
376359
return (
377360
<Attr.button
378361
type='button'
362+
data-row={rowIndex}
363+
data-col={colIndex}
364+
onKeyDown={this.onKeyDown}
379365
className={colClassName}
380366
onClick={onClick}
381367
key={key}
382-
tabIndex={tabIndex}
383368
attr={this.props.attr.dateButton}
384369
>
385370
{date}
@@ -399,11 +384,12 @@ export class Calendar extends React.Component<CalendarProps, Partial<CalendarSta
399384
return (
400385
<Attr.button
401386
type='button'
387+
data-row={rowIndex}
388+
data-col={colIndex}
389+
onKeyDown={this.onKeyDown}
402390
className={css('selected')}
403391
onClick={onClick}
404392
key={key}
405-
tabIndex={tabIndex}
406-
methodRef={this.dayRef}
407393
onFocus={this.onFocus.bind(this, date)}
408394
attr={this.props.attr.dateButton}
409395
>
@@ -417,10 +403,11 @@ export class Calendar extends React.Component<CalendarProps, Partial<CalendarSta
417403
return (
418404
<Attr.button
419405
type='button'
406+
data-row={rowIndex}
407+
data-col={colIndex}
408+
onKeyDown={this.onKeyDown}
420409
onClick={onClick}
421410
key={key}
422-
tabIndex={tabIndex}
423-
methodRef={this.dayRef}
424411
onFocus={this.onFocus.bind(this, date)}
425412
attr={this.props.attr.dateButton}
426413
>
@@ -441,6 +428,7 @@ export class Calendar extends React.Component<CalendarProps, Partial<CalendarSta
441428
});
442429
return (
443430
<Attr.div
431+
methodRef={this.setContainerRef}
444432
className={css('calendar', this.props.className)}
445433
attr={this.props.attr.container}
446434
>
@@ -456,16 +444,14 @@ export class Calendar extends React.Component<CalendarProps, Partial<CalendarSta
456444
</Attr.div>
457445
<ActionTriggerButton
458446
className={css('calendar-chevron')}
459-
onClick={event => this.onPrevMonth(event)}
460-
tabIndex={tabIndex}
447+
onClick={this.onPrevMonth}
461448
icon='chevronUp'
462449
attr={this.props.attr.prevMonthButton}
463450
/>
464451
<ActionTriggerButton
465452
icon='chevronDown'
466453
className={css('calendar-chevron')}
467-
onClick={event => this.onNextMonth(event)}
468-
tabIndex={tabIndex}
454+
onClick={this.onNextMonth}
469455
attr={this.props.attr.nextMonthButton}
470456
/>
471457
</Attr.div>

0 commit comments

Comments
 (0)