Skip to content

Commit

Permalink
Modified aria attributes and focusing behavior so that the keyboard s…
Browse files Browse the repository at this point in the history
…hortcuts panel receives focus, is read appropriately by screen readers, prevents focus from accidentally moving back to the other elements without use of the proper shortcuts, and then returns focus to the appropriate place when closed.
  • Loading branch information
Erin Doyle committed Nov 4, 2017
1 parent ae49b71 commit 33b133f
Show file tree
Hide file tree
Showing 9 changed files with 384 additions and 85 deletions.
7 changes: 2 additions & 5 deletions src/components/DayPicker.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -234,13 +234,10 @@ class DayPicker extends React.Component {
}

componentDidUpdate(prevProps) {
const { isFocused, showKeyboardShortcuts } = this.props;
const { isFocused } = this.props;
const { focusedDate } = this.state;

if (
(!prevProps.isFocused && isFocused && !focusedDate) ||
(!prevProps.showKeyboardShortcuts && showKeyboardShortcuts)
) {
if (!prevProps.isFocused && isFocused && !focusedDate) {
this.container.focus();
}
}
Expand Down
159 changes: 109 additions & 50 deletions src/components/DayPickerKeyboardShortcuts.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,91 @@ const defaultProps = {
phrases: DayPickerKeyboardShortcutsPhrases,
};

function getKeyboardShortcuts(phrases) {
return [{
unicode: '↵',
label: phrases.enterKey,
action: phrases.selectFocusedDate,
},
{
unicode: '←/→',
label: phrases.leftArrowRightArrow,
action: phrases.moveFocusByOneDay,
},
{
unicode: '↑/↓',
label: phrases.upArrowDownArrow,
action: phrases.moveFocusByOneWeek,
},
{
unicode: 'PgUp/PgDn',
label: phrases.pageUpPageDown,
action: phrases.moveFocusByOneMonth,
},
{
unicode: 'Home/End',
label: phrases.homeEnd,
action: phrases.moveFocustoStartAndEndOfWeek,
},
{
unicode: 'Esc',
label: phrases.escape,
action: phrases.returnFocusToInput,
},
{
unicode: '?',
label: phrases.questionMark,
action: phrases.openThisPanel,
},
];
}

class DayPickerKeyboardShortcuts extends React.Component {
constructor(...args) {
super(...args);

this.onClick = this.onClick.bind(this);
this.keyboardShortcuts = getKeyboardShortcuts(this.props.phrases);

this.onShowKeyboardShortcutsButtonClick = this.onShowKeyboardShortcutsButtonClick.bind(this);
this.setShowKeyboardShortcutsButtonRef = this.setShowKeyboardShortcutsButtonRef.bind(this);
this.setHideKeyboardShortcutsButtonRef = this.setHideKeyboardShortcutsButtonRef.bind(this);
this.handleFocus = this.handleFocus.bind(this);
this.onKeyDown = this.onKeyDown.bind(this);
}

componentDidUpdate() {
this.handleFocus();
}

onKeyDown(e) {
const { closeKeyboardShortcutsPanel } = this.props;

e.stopPropagation();

// Because the close button is the only focusable element inside of the panel, this
// amount to a very basic focus trap. The user can exit the panel by "pressing" the
// close button or hitting escape
switch (e.key) {
case 'Space':
case 'Escape':
closeKeyboardShortcutsPanel();
break;

case 'Tab':
case 'Enter':
case 'Home':
case 'End':
case 'PageUp':
case 'PageDown':
e.preventDefault();
break;

default:
break;
}
}

onClick() {
onShowKeyboardShortcutsButtonClick() {
const { openKeyboardShortcutsPanel } = this.props;

// we want to return focus to this button after closing the keyboard shortcuts panel
Expand All @@ -52,6 +128,18 @@ class DayPickerKeyboardShortcuts extends React.Component {
this.showKeyboardShortcutsButton = ref;
}

setHideKeyboardShortcutsButtonRef(ref) {
this.hideKeyboardShortcutsButton = ref;
}

handleFocus() {
if (this.hideKeyboardShortcutsButton) {
// automatically move focus into the dialog by moving
// to the only interactive element, the hide button
this.hideKeyboardShortcutsButton.focus();
}
}

render() {
const {
block,
Expand All @@ -62,43 +150,6 @@ class DayPickerKeyboardShortcuts extends React.Component {
phrases,
} = this.props;

const keyboardShortcuts = [{
unicode: '↵',
label: phrases.enterKey,
action: phrases.selectFocusedDate,
},
{
unicode: '←/→',
label: phrases.leftArrowRightArrow,
action: phrases.moveFocusByOneDay,
},
{
unicode: '↑/↓',
label: phrases.upArrowDownArrow,
action: phrases.moveFocusByOneWeek,
},
{
unicode: 'PgUp/PgDn',
label: phrases.pageUpPageDown,
action: phrases.moveFocusByOneMonth,
},
{
unicode: 'Home/End',
label: phrases.homeEnd,
action: phrases.moveFocustoStartAndEndOfWeek,
},
{
unicode: 'Esc',
label: phrases.escape,
action: phrases.returnFocusToInput,
},
{
unicode: '?',
label: phrases.questionMark,
action: phrases.openThisPanel,
},
];

const toggleButtonText = showKeyboardShortcutsPanel
? phrases.hideKeyboardShortcutsPanel
: phrases.showKeyboardShortcutsPanel;
Expand All @@ -110,6 +161,7 @@ class DayPickerKeyboardShortcuts extends React.Component {
return (
<div>
<button
id="showKeyboardShortcutsButton"
ref={this.setShowKeyboardShortcutsButtonRef}
{...css(
styles.DayPickerKeyboardShortcuts_buttonReset,
Expand All @@ -120,7 +172,14 @@ class DayPickerKeyboardShortcuts extends React.Component {
)}
type="button"
aria-label={toggleButtonText}
onClick={this.onClick}
onClick={this.onShowKeyboardShortcutsButtonClick}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
} else if (e.key === 'Space') {
this.onShowKeyboardShortcutsButtonClick();
}
}}
onMouseUp={(e) => {
e.currentTarget.blur();
}}
Expand All @@ -145,6 +204,7 @@ class DayPickerKeyboardShortcuts extends React.Component {
)}
role="dialog"
aria-labelledby="DayPickerKeyboardShortcuts__title"
aria-describedby="DayPickerKeyboardShortcuts__description"
>
<div
{...css(styles.DayPickerKeyboardShortcuts_title)}
Expand All @@ -154,27 +214,26 @@ class DayPickerKeyboardShortcuts extends React.Component {
</div>

<button
id="hideKeyboardShortcutsButton"
ref={this.setHideKeyboardShortcutsButtonRef}
{...css(
styles.DayPickerKeyboardShortcuts_buttonReset,
styles.DayPickerKeyboardShortcuts_close,
)}
type="button"
tabIndex="0"
aria-label={phrases.hideKeyboardShortcutsPanel}
onClick={closeKeyboardShortcutsPanel}
onKeyDown={(e) => {
// Because the close button is the only focusable element inside of the panel, this
// amount to a very basic focus trap. The user can exit the panel by "pressing" the
// close button or hitting escape
if (e.key === 'Tab') {
e.preventDefault();
}
}}
onKeyDown={this.onKeyDown}
>
<CloseButton {...css(styles.DayPickerKeyboardShortcuts_closeSvg)} />
</button>

<ul {...css(styles.DayPickerKeyboardShortcuts__list)}>
{keyboardShortcuts.map(({ unicode, label, action }) => (
<ul
{...css(styles.DayPickerKeyboardShortcuts__list)}
id="DayPickerKeyboardShortcuts__description"
>
{this.keyboardShortcuts.map(({ unicode, label, action }) => (
<KeyboardShortcutRow
key={label}
unicode={unicode}
Expand Down
2 changes: 1 addition & 1 deletion src/components/KeyboardShortcutRow.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ function KeyboardShortcutRow({
<span
{...css(styles.KeyboardShortcutRow_key)}
role="img"
aria-label={label}
aria-label={`${label} `}
>
{unicode}
</span>
Expand Down
1 change: 1 addition & 0 deletions src/components/SingleDatePicker.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,7 @@ class SingleDatePicker extends React.Component {
renderCalendarInfo={renderCalendarInfo}
isFocused={isDayPickerFocused}
showKeyboardShortcuts={showKeyboardShortcuts}
onBlur={this.onDayPickerBlur}
phrases={phrases}
daySize={daySize}
isRTL={isRTL}
Expand Down
22 changes: 11 additions & 11 deletions src/defaultPhrases.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,25 @@ const closeDatePicker = 'Close';
const focusStartDate = 'Interact with the calendar and add the check-in date for your trip.';
const clearDate = 'Clear Date';
const clearDates = 'Clear Dates';
const jumpToPrevMonth = 'Move backward to switch to the previous month';
const jumpToNextMonth = 'Move forward to switch to the next month';
const jumpToPrevMonth = 'Move backward to switch to the previous month.';
const jumpToNextMonth = 'Move forward to switch to the next month.';
const keyboardShortcuts = 'Keyboard Shortcuts';
const showKeyboardShortcutsPanel = 'Open the keyboard shortcuts panel';
const hideKeyboardShortcutsPanel = 'Close the shortcuts panel';
const openThisPanel = 'Open this panel';
const showKeyboardShortcutsPanel = 'Open the keyboard shortcuts panel.';
const hideKeyboardShortcutsPanel = 'Close the shortcuts panel.';
const openThisPanel = 'Open this panel.';
const enterKey = 'Enter key';
const leftArrowRightArrow = 'Right and left arrow keys';
const upArrowDownArrow = 'up and down arrow keys';
const pageUpPageDown = 'page up and page down keys';
const homeEnd = 'Home and end keys';
const escape = 'Escape key';
const questionMark = 'Question mark';
const selectFocusedDate = 'Select the date in focus';
const moveFocusByOneDay = 'Move backward (left) and forward (right) by one day';
const moveFocusByOneWeek = 'Move backward (up) and forward (down) by one week';
const moveFocusByOneMonth = 'Switch months';
const moveFocustoStartAndEndOfWeek = 'Go to the first or last day of a week';
const returnFocusToInput = 'Return to the date input field';
const selectFocusedDate = 'Select the date in focus.';
const moveFocusByOneDay = 'Move backward (left) and forward (right) by one day.';
const moveFocusByOneWeek = 'Move backward (up) and forward (down) by one week.';
const moveFocusByOneMonth = 'Switch months.';
const moveFocustoStartAndEndOfWeek = 'Go to the first or last day of a week.';
const returnFocusToInput = 'Return to the date input field.';
const keyboardNavigationInstructions = `Press the down arrow key to interact with the calendar and
select a date. Press the question mark key to get the keyboard shortcuts for changing dates.`;

Expand Down
9 changes: 8 additions & 1 deletion test/components/DateRangePicker_spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ describe('DateRangePicker', () => {
expect(wrapper.find(DayPickerRangeController)).to.have.length(1);
});


describe('props.orientation === HORIZONTAL_ORIENTATION', () => {
it('renders <DayPickerRangeController /> with props.numberOfMonths === 2', () => {
const wrapper = shallow((
Expand All @@ -50,6 +49,14 @@ describe('DateRangePicker', () => {
});
});

it('should pass onDayPickerBlur as onBlur to <DayPickerRangeController>', () => {
const wrapper = shallow((
<DateRangePicker {...requiredProps} focusedInput={START_DATE} />
)).dive();
const { onDayPickerBlur } = wrapper.instance();
expect(wrapper.find(DayPickerRangeController).prop('onBlur')).to.equal(onDayPickerBlur);
});

describe('props.withPortal is truthy', () => {
describe('<Portal />', () => {
it('is rendered', () => {
Expand Down
Loading

0 comments on commit 33b133f

Please sign in to comment.