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

Commit

Permalink
Merge pull request #23 from misteinb/bugfix/date-picker-keyboard-nav
Browse files Browse the repository at this point in the history
Bugfix/date picker keyboard nav
  • Loading branch information
misteinb authored Aug 6, 2018
2 parents 7f9a406 + b5c3da8 commit 7188905
Show file tree
Hide file tree
Showing 8 changed files with 205 additions and 240 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# CHANGELOG

## v3.0.0
### Changed
- calendar api changed. no longer extends drop down
### Fixed
- calendar in datepicker is now reachable via keyboard

## v2.0.5
### Fixed
- date picker should not open when input receives focus
Expand Down
15 changes: 9 additions & 6 deletions lib/components/DateTime/Calendar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ $line-height-row: $calendar-column-width;
&.disabled {
pointer-events: none;
}

&:focus {
outline-offset: -1px;
}
}
}

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

&:focus {
outline: none;
color: themed('color-text-rest');
background-color: transparent;
outline: 1px dashed themed('color-outline-btn-primary-focus');
background-color: themed('color-bg-btn-primary-focus');
outline-offset: -2px;
}
}
}
Expand Down Expand Up @@ -131,9 +135,8 @@ $line-height-row: $calendar-column-width;
color: inherit;
}

&:focus {
outline-offset: -1px;
background-color: transparent !important;
&:focus:not(.selected) {
outline-offset: -2px;
@include themify{
outline: 1px dashed themed('color-border-focus');
}
Expand Down
222 changes: 104 additions & 118 deletions lib/components/DateTime/Calendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,6 @@ export interface CalendarProps extends React.Props<CalendarComponentType> {
*/
localTimezone?: boolean;

/** Tab index of calendar buttons */
tabIndex?: number;

/**
* Callback for date change events
* */
Expand Down Expand Up @@ -67,7 +64,6 @@ export interface CalendarState {
export class Calendar extends React.Component<CalendarProps, Partial<CalendarState>> {
static defaultProps = {
localTimezone: true,
tabIndex: -1,
attr: {
container: {},
header: {},
Expand All @@ -84,8 +80,10 @@ export class Calendar extends React.Component<CalendarProps, Partial<CalendarSta
private value: MethodDate;
private monthNames: string[];
private dayNames: string[];
private buttons: { [date: string]: HTMLButtonElement };
private buttonIndex: number;
private _container: HTMLDivElement;
private nextFocusRow?: number;
private nextFocusCol?: number;


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

this.dayNames = getLocalWeekdays(locale);

this.buttons = {};
this.buttonIndex = 0;
this.dayRef = this.dayRef.bind(this);

this.onPrevMonth = this.onPrevMonth.bind(this);
this.onNextMonth = this.onNextMonth.bind(this);
this.onKeyDown = this.onKeyDown.bind(this);
}

get focusedButton(): HTMLButtonElement {
return this.buttons[this.state.currentDate.date - 1];
this.setContainerRef = this.setContainerRef.bind(this);
}

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

dayRef(element: HTMLButtonElement) {
if (element) {
this.buttons[this.buttonIndex] = element;
this.buttonIndex++;
}
}

componentWillMount() {
window.addEventListener('keydown', this.onKeyDown);
}

componentWillUnmount() {
window.removeEventListener('keydown', this.onKeyDown);
}

componentDidUpdate(oldProps: CalendarProps, oldState: CalendarState) {
if (this.state.accessibility && this.state.currentDate !== oldState.currentDate) {
this.focusedButton.focus();
}
this.buttonIndex = 0;
}

componentWillReceiveProps(newProps: CalendarProps) {
const date = this.state.currentDate.copy();
let update = false;
Expand Down Expand Up @@ -195,75 +166,14 @@ export class Calendar extends React.Component<CalendarProps, Partial<CalendarSta
}
}

onKeyDown(event) {
if (!this.state.accessibility) {
return;
}
/** So that we don't block any browser shortcuts */
if (event.ctrlKey || event.altKey) {
return;
}
if (document.activeElement === this.focusedButton) {
const date = this.state.currentDate.copy();
let detached = this.state.detached;
let newDay = date.date;
let newMonth = date.month;
let newYear = date.year;
let weekMove = false;
switch (event.keyCode) {
case keyCode.left:
newDay -= 1;
break;
case keyCode.right:
newDay += 1;
break;
case keyCode.up:
weekMove = true;
newDay -= 7;
break;
case keyCode.down:
weekMove = true;
newDay += 7;
break;
case keyCode.pageup:
if (event.ctrlKey) {
newYear -= 1;
} else {
newMonth -= 1;
}
break;
case keyCode.pagedown:
if (event.ctrlKey) {
newYear += 1;
} else {
newMonth += 1;
}
break;
case keyCode.home:
newDay = 1;
break;
case keyCode.end:
newDay = 0;
newMonth += 1;
break;
default:
return;
}
date.year = newYear;
date.month = newMonth;
date.date = newDay;

if (newDay > 0 && date.date !== newDay && !weekMove) {
date.month += 1;
date.date = 0;
componentDidUpdate() {
if (this.nextFocusRow != null && this.nextFocusCol != null) {
const nextFocus = this._container.querySelectorAll(`[data-row="${this.nextFocusRow}"][data-col="${this.nextFocusCol}"]`)[0] as HTMLElement;
if (nextFocus != null) {
nextFocus.focus();
}

event.stopPropagation();
event.preventDefault();
this.setState({
currentDate: date,
detached: detached
});
this.nextFocusRow = undefined;
this.nextFocusCol = undefined;
}
}

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

this.decrementMonth();
}

onNextMonth(event) {
event.preventDefault();

this.incrementMonth();
}

decrementMonth() {
/** Dates are mutable so we're going to copy it over */
const newDate = this.state.currentDate.copy();
const curDate = newDate.date;
Expand All @@ -303,9 +223,7 @@ export class Calendar extends React.Component<CalendarProps, Partial<CalendarSta
this.setState({ currentDate: newDate, detached: true });
}

onNextMonth(event) {
event.preventDefault();

incrementMonth() {
/** Dates are mutable so we're going to copy it over */
const newDate = this.state.currentDate.copy();
const curDate = newDate.date;
Expand All @@ -319,14 +237,77 @@ export class Calendar extends React.Component<CalendarProps, Partial<CalendarSta
this.setState({ currentDate: newDate, detached: true });
}

onKeyDown(e: React.KeyboardEvent<any>) {
const element: HTMLElement = e.currentTarget;
const row = parseInt(element.getAttribute('data-row'), 10);
const col = parseInt(element.getAttribute('data-col'), 10);

if (!isNaN(row) && !isNaN(col)) {
let nextRow = row;
let nextCol = col;
let nextFocus: HTMLElement;
switch (e.keyCode) {
case keyCode.pagedown:
e.preventDefault();
e.stopPropagation();
this.nextFocusCol = nextCol;
this.nextFocusRow = nextRow;
this.incrementMonth();
break;
case keyCode.pageup:
e.preventDefault();
e.stopPropagation();
this.nextFocusCol = nextCol;
this.nextFocusRow = nextRow;
this.decrementMonth();
break;
case keyCode.up:
e.preventDefault();
e.stopPropagation();
nextRow -= 1;
break;
case keyCode.down:
e.preventDefault();
e.stopPropagation();
nextRow += 1;
break;
case keyCode.left:
e.preventDefault();
e.stopPropagation();
nextCol -= 1;
if (nextCol < 0) {
nextCol = 6;
nextRow -= 1;
}
break;
case keyCode.right:
e.preventDefault();
e.stopPropagation();
nextCol += 1;
if (nextCol > 6) {
nextCol = 0;
nextRow += 1;
}
break;
}
nextFocus = this._container.querySelectorAll(`[data-row="${nextRow}"][data-col="${nextCol}"]`)[0] as HTMLElement;
// if we found the next button to focus on, focus it
if (nextFocus != null) {
nextFocus.focus();
}
}
}

setContainerRef(element: HTMLDivElement) {
this._container = element;
}

render() {
const rowClassName = css('calendar-row');
const colClassName = css('disabled');
const tabIndex = this.props.tabIndex;

const curYear = this.state.currentDate.year;
const curMonth = this.state.currentDate.month;
const curDate = this.state.currentDate.date;

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

// TODO aria-label with date

const date = col.date;
const colMonth = col.month;
const key = `${colMonth}-${date}`;
Expand All @@ -376,10 +359,12 @@ export class Calendar extends React.Component<CalendarProps, Partial<CalendarSta
return (
<Attr.button
type='button'
data-row={rowIndex}
data-col={colIndex}
onKeyDown={this.onKeyDown}
className={colClassName}
onClick={onClick}
key={key}
tabIndex={tabIndex}
attr={this.props.attr.dateButton}
>
{date}
Expand All @@ -399,11 +384,12 @@ export class Calendar extends React.Component<CalendarProps, Partial<CalendarSta
return (
<Attr.button
type='button'
data-row={rowIndex}
data-col={colIndex}
onKeyDown={this.onKeyDown}
className={css('selected')}
onClick={onClick}
key={key}
tabIndex={tabIndex}
methodRef={this.dayRef}
onFocus={this.onFocus.bind(this, date)}
attr={this.props.attr.dateButton}
>
Expand All @@ -417,10 +403,11 @@ export class Calendar extends React.Component<CalendarProps, Partial<CalendarSta
return (
<Attr.button
type='button'
data-row={rowIndex}
data-col={colIndex}
onKeyDown={this.onKeyDown}
onClick={onClick}
key={key}
tabIndex={tabIndex}
methodRef={this.dayRef}
onFocus={this.onFocus.bind(this, date)}
attr={this.props.attr.dateButton}
>
Expand All @@ -441,6 +428,7 @@ export class Calendar extends React.Component<CalendarProps, Partial<CalendarSta
});
return (
<Attr.div
methodRef={this.setContainerRef}
className={css('calendar', this.props.className)}
attr={this.props.attr.container}
>
Expand All @@ -456,16 +444,14 @@ export class Calendar extends React.Component<CalendarProps, Partial<CalendarSta
</Attr.div>
<ActionTriggerButton
className={css('calendar-chevron')}
onClick={event => this.onPrevMonth(event)}
tabIndex={tabIndex}
onClick={this.onPrevMonth}
icon='chevronUp'
attr={this.props.attr.prevMonthButton}
/>
<ActionTriggerButton
icon='chevronDown'
className={css('calendar-chevron')}
onClick={event => this.onNextMonth(event)}
tabIndex={tabIndex}
onClick={this.onNextMonth}
attr={this.props.attr.nextMonthButton}
/>
</Attr.div>
Expand Down
Loading

0 comments on commit 7188905

Please sign in to comment.