Skip to content

Commit

Permalink
Makes the DateRangePicker and SingleDatePicker keyboard accessible
Browse files Browse the repository at this point in the history
- Adds screen reader text to the input to describe how to enter the calendar
- Adds arrow navigation to the calendar itself
- Adds a keyboard shortcuts panel that pops up on top of the calendar
- Adds screen reader text to each CalendarDay
  • Loading branch information
Maja Wichrowska committed Apr 5, 2017
1 parent 5ba9930 commit 769f049
Show file tree
Hide file tree
Showing 33 changed files with 2,077 additions and 149 deletions.
1 change: 1 addition & 0 deletions constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ module.exports = {
ANCHOR_RIGHT: 'right',

DAY_SIZE: 39,
BLOCKED_MODIFIER: 'blocked',
};
20 changes: 19 additions & 1 deletion css/CalendarDay.scss
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
@import "variables";

// This order is important.
.CalendarDay {
border: 1px solid lighten($react-dates-color-border-light, 3);
padding: 0;
Expand All @@ -9,12 +8,31 @@
cursor: pointer;
width: 39px;
height: 38px;
}

.CalendarDay__button {
position: relative;
height: 100%;
width: 100%;
text-align: center;
background: none;
border: 0;
margin: 0;
padding: 0;
color: inherit;
font: inherit;
line-height: normal;
overflow: visible;
cursor: pointer;
box-sizing: border-box;

&:active {
background: darken($react-dates-color-white, 5%);
outline: 0;
}
}

// This order is important.
.CalendarDay--highlighted-calendar {
background: $react-dates-color-highlighted;
color: $react-dates-color-gray;
Expand Down
4 changes: 2 additions & 2 deletions css/DateRangePicker.scss
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@
}

.DateRangePicker__picker--direction-left {
left: 0;
left: 0;
}

.DateRangePicker__picker--direction-right {
right: 0;
right: 0;
}

.DateRangePicker__picker--portal {
Expand Down
106 changes: 106 additions & 0 deletions css/DayPickerKeyboardShortcuts.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
.DayPickerKeyboardShortcuts__show,
.DayPickerKeyboardShortcuts__close {
background: none;
border: 0;
color: inherit;
font: inherit;
line-height: normal;
overflow: visible;
padding: 0;
cursor: pointer;

&:active {
outline: none;
}
}

.DayPickerKeyboardShortcuts__show {
border-top: 26px solid $react-dates-color-white;
border-right: 33px solid $react-dates-color-primary;
width: 22px;
position: absolute;
right: 0;
bottom: 0;

&:hover {
border-right: 33px solid #008489;
}
}

.DayPickerKeyboardShortcuts__show_span {
color: $react-dates-color-white;
position: absolute;
bottom: 0;
right: -28px;
}

.DayPickerKeyboardShortcuts__panel {
background: $react-dates-color-white;
border: 1px solid $react-dates-color-border;
border-radius: 2px;
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 0;
z-index: 2;
padding: 22px;
margin: 33px;
}

.DayPickerKeyboardShortcuts__title {
font-size: 16px;
font-weight: bold;
margin: 0;
}

.DayPickerKeyboardShortcuts__list {
list-style: none;
padding: 0;

}

.DayPickerKeyboardShortcuts__close {
position: absolute;
right: 22px;
top: 22px;
z-index: 2;

svg {
height: 15px;
width: 15px;
fill: $react-dates-color-gray-lighter;

&:hover,
&:focus {
fill: $react-dates-color-gray-light;
}
}

&:active {
outline: none;
}
}

.KeyboardShortcutRow {
margin: 6px 0;
}

.KeyboardShortcutRow__key-container {
display: inline-block;
width: 15%;
text-align: right;
margin-right: 6px;
}

.KeyboardShortcutRow__key {
font-family: monospace;
font-size: 12px;
text-transform: uppercase;
background: $react-dates-color-gray-lightest;
padding: 2px 6px;
}

.KeyboardShortcutRow__action {
display: inline-block;
}
1 change: 1 addition & 0 deletions css/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
@import 'CalendarMonthGrid';
@import 'DayPicker';
@import 'DayPickerNavigation';
@import 'DayPickerKeyboardShortcuts';
@import 'DateInput';
@import 'DateRangePicker';
@import 'DateRangePickerInput';
Expand Down
2 changes: 2 additions & 0 deletions css/variables.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ $react-dates-spacing-vertical-picker: 72px !default;

$react-dates-color-primary: #00a699 !default;
$react-dates-color-primary-dark: #00514a !default;
$react-date-color-primary-dark-1: #008489 !default;
$react-dates-color-primary-shade-1: #33dacd !default;
$react-dates-color-primary-shade-2: #66e2da !default;
$react-dates-color-primary-shade-3: #80e8e0 !default;
Expand All @@ -15,6 +16,7 @@ $react-dates-color-gray: #565a5c !default;
$react-dates-color-gray-dark: darken($react-dates-color-gray, 10.5%) !default;
$react-dates-color-gray-light: lighten($react-dates-color-gray, 17.8%) !default; // #82888a
$react-dates-color-gray-lighter: lighten($react-dates-color-gray, 45%) !default; // #cacccd
$react-dates-color-gray-lightest: lighten($react-dates-color-gray, 60%) !default;
$react-dates-color-highlighted: #ffe8bc !default;

$react-dates-color-border: #dbdbdb !default;
Expand Down
9 changes: 5 additions & 4 deletions examples/DateRangePickerWrapper.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import omit from 'lodash.omit';

import DateRangePicker from '../src/components/DateRangePicker';

import { DateRangePickerPhrases } from '../src/defaultPhrases';
import DateRangePickerShape from '../src/shapes/DateRangePickerShape';
import { START_DATE, END_DATE, HORIZONTAL_ORIENTATION, ANCHOR_LEFT } from '../constants';
import isInclusivelyAfterDay from '../src/utils/isInclusivelyAfterDay';
Expand Down Expand Up @@ -74,10 +75,7 @@ const defaultProps = {
// internationalization
displayFormat: () => moment.localeData().longDateFormat('L'),
monthFormat: 'MMMM YYYY',
phrases: {
closeDatePicker: 'Close',
clearDates: 'Clear Dates',
},
phrases: DateRangePickerPhrases,
};

class DateRangePickerWrapper extends React.Component {
Expand Down Expand Up @@ -112,6 +110,9 @@ class DateRangePickerWrapper extends React.Component {
render() {
const { focusedInput, startDate, endDate } = this.state;

// autoFocus, autoFocusEndDate, initialStartDate and initialEndDate are helper props for the
// example wrapper but are not props on the SingleDatePicker itself and
// thus, have to be omitted.
const props = omit(this.props, [
'autoFocus',
'autoFocusEndDate',
Expand Down
16 changes: 11 additions & 5 deletions examples/SingleDatePickerWrapper.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import omit from 'lodash.omit';

import SingleDatePicker from '../src/components/SingleDatePicker';

import { SingleDatePickerPhrases } from '../src/defaultPhrases';
import SingleDatePickerShape from '../src/shapes/SingleDatePickerShape';
import { HORIZONTAL_ORIENTATION, ANCHOR_LEFT } from '../constants';
import isInclusivelyAfterDay from '../src/utils/isInclusivelyAfterDay';
Expand All @@ -13,6 +14,7 @@ const propTypes = {
// example props for the demo
autoFocus: PropTypes.bool,
initialDate: momentPropTypes.momentObj,

...omit(SingleDatePickerShape, [
'date',
'onDateChange',
Expand Down Expand Up @@ -61,10 +63,7 @@ const defaultProps = {
// internationalization props
displayFormat: () => moment.localeData().longDateFormat('L'),
monthFormat: 'MMMM YYYY',
phrases: {
closeDatePicker: 'Close',
clearDate: 'Clear Date',
},
phrases: SingleDatePickerPhrases,
};

class SingleDatePickerWrapper extends React.Component {
Expand All @@ -90,9 +89,16 @@ class SingleDatePickerWrapper extends React.Component {
render() {
const { focused, date } = this.state;

// autoFocus and initialDate are helper props for the example wrapper but are not
// props on the SingleDatePicker itself and thus, have to be omitted.
const props = omit(this.props, [
'autoFocus',
'initialDate',
]);

return (
<SingleDatePicker
{...this.props}
{...props}
id="date_input"
date={date}
focused={focused}
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@
"array-includes": "^3.0.2",
"classnames": "^2.2.5",
"consolidated-events": "^1.0.1",
"lodash.throttle": "^4.1.1",
"react-moment-proptypes": "^1.3.0",
"react-portal": "^3.0.0"
},
Expand Down
64 changes: 50 additions & 14 deletions src/components/CalendarDay.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,39 @@ import { forbidExtraProps, nonNegativeInteger } from 'airbnb-prop-types';
import moment from 'moment';
import cx from 'classnames';

import { DAY_SIZE } from '../../constants';
import { CalendarDayPhrases } from '../defaultPhrases';
import getPhrasePropTypes from '../utils/getPhrasePropTypes';

import { BLOCKED_MODIFIER, DAY_SIZE } from '../../constants';

const propTypes = forbidExtraProps({
day: momentPropTypes.momentObj,
daySize: nonNegativeInteger,
isOutsideDay: PropTypes.bool,
modifiers: PropTypes.object,
isFocused: PropTypes.bool,
onDayClick: PropTypes.func,
onDayMouseEnter: PropTypes.func,
onDayMouseLeave: PropTypes.func,
renderDay: PropTypes.func,

// internationalization
phrases: PropTypes.shape(getPhrasePropTypes(CalendarDayPhrases)),
});

const defaultProps = {
day: moment(),
daySize: DAY_SIZE,
isOutsideDay: false,
modifiers: {},
isFocused: false,
onDayClick() {},
onDayMouseEnter() {},
onDayMouseLeave() {},
renderDay: null,

// internationalization
phrases: CalendarDayPhrases,
};

export function getModifiersForDay(modifiers, day) {
Expand All @@ -38,6 +49,13 @@ export default class CalendarDay extends React.Component {
return shallowCompare(this, nextProps, nextState);
}

componentDidUpdate() {
const { isFocused } = this.props;
if (isFocused) {
this.buttonRef.focus();
}
}

onDayClick(day, e) {
const { onDayClick } = this.props;
onDayClick(day, e);
Expand All @@ -60,29 +78,47 @@ export default class CalendarDay extends React.Component {
isOutsideDay,
modifiers,
renderDay,
isFocused,
phrases: { unavailable, available },
} = this.props;

if (!day) return <td />;

const modifiersForDay = getModifiersForDay(modifiers, day);

const className = cx('CalendarDay', {
'CalendarDay--outside': !day || isOutsideDay,
}, getModifiersForDay(modifiers, day).map(mod => `CalendarDay--${mod}`));
'CalendarDay--outside': isOutsideDay,
}, modifiersForDay.map(mod => `CalendarDay--${mod}`));


let availabilityText = '';
if (BLOCKED_MODIFIER in modifiers) {
availabilityText = modifiers[BLOCKED_MODIFIER](day) ? unavailable : available;
}

const ariaLabel = `${availabilityText} ${day.format('dddd')}. ${day.format('LL')}`;
const daySizeStyles = {
width: daySize,
height: daySize - 1,
};

return (day ?
<td
className={className}
style={daySizeStyles}
onMouseEnter={e => this.onDayMouseEnter(day, e)}
onMouseLeave={e => this.onDayMouseLeave(day, e)}
onClick={e => this.onDayClick(day, e)}
>
{renderDay ? renderDay(day) : day.format('D')}
return (
<td className={className}>
<button
type="button"
ref={(ref) => { this.buttonRef = ref; }}
className="CalendarDay__button"
aria-label={ariaLabel}
style={daySizeStyles}
onMouseEnter={(e) => { this.onDayMouseEnter(day, e); }}
onMouseLeave={(e) => { this.onDayMouseLeave(day, e); }}
onMouseUp={(e) => { e.currentTarget.blur(); }}
onClick={(e) => { this.onDayClick(day, e); }}
tabIndex={isFocused ? 0 : -1}
>
{renderDay ? renderDay(day) : day.format('D')}
</button>
</td>
:
<td />
);
}
}
Expand Down

0 comments on commit 769f049

Please sign in to comment.