react-dates
Version:
A responsive and accessible date range picker component built with React
412 lines (357 loc) • 11 kB
JSX
import React from 'react';
import PropTypes from 'prop-types';
import { forbidExtraProps } from 'airbnb-prop-types';
import { css, withStyles, withStylesPropTypes } from 'react-with-styles';
import { DayPickerKeyboardShortcutsPhrases } from '../defaultPhrases';
import getPhrasePropTypes from '../utils/getPhrasePropTypes';
import KeyboardShortcutRow from './KeyboardShortcutRow';
import CloseButton from './CloseButton';
export const TOP_LEFT = 'top-left';
export const TOP_RIGHT = 'top-right';
export const BOTTOM_RIGHT = 'bottom-right';
const propTypes = forbidExtraProps({
...withStylesPropTypes,
block: PropTypes.bool,
// TODO: rename button location to be direction-agnostic
buttonLocation: PropTypes.oneOf([TOP_LEFT, TOP_RIGHT, BOTTOM_RIGHT]),
showKeyboardShortcutsPanel: PropTypes.bool,
openKeyboardShortcutsPanel: PropTypes.func,
closeKeyboardShortcutsPanel: PropTypes.func,
phrases: PropTypes.shape(getPhrasePropTypes(DayPickerKeyboardShortcutsPhrases)),
});
const defaultProps = {
block: false,
buttonLocation: BOTTOM_RIGHT,
showKeyboardShortcutsPanel: false,
openKeyboardShortcutsPanel() {},
closeKeyboardShortcutsPanel() {},
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.PureComponent {
constructor(...args) {
super(...args);
const { phrases } = this.props;
this.keyboardShortcuts = getKeyboardShortcuts(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);
}
componentWillReceiveProps(nextProps) {
const { phrases } = this.props;
if (nextProps.phrases !== phrases) {
this.keyboardShortcuts = getKeyboardShortcuts(nextProps.phrases);
}
}
componentDidUpdate() {
this.handleFocus();
}
onKeyDown(e) {
e.stopPropagation();
const { closeKeyboardShortcutsPanel } = this.props;
// Because the close button is the only focusable element inside of the panel, this
// amounts to a very basic focus trap. The user can exit the panel by "pressing" the
// close button or hitting escape
switch (e.key) {
case 'Escape':
closeKeyboardShortcutsPanel();
break;
// do nothing - this allows the up and down arrows continue their
// default behavior of scrolling the content of the Keyboard Shortcuts Panel
// which is needed when only a single month is shown for instance.
case 'ArrowUp':
case 'ArrowDown':
break;
// completely block the rest of the keys that have functionality outside of this panel
case 'Tab':
case 'Home':
case 'End':
case 'PageUp':
case 'PageDown':
case 'ArrowLeft':
case 'ArrowRight':
e.preventDefault();
break;
default:
break;
}
}
onShowKeyboardShortcutsButtonClick() {
const { openKeyboardShortcutsPanel } = this.props;
// we want to return focus to this button after closing the keyboard shortcuts panel
openKeyboardShortcutsPanel(() => { this.showKeyboardShortcutsButton.focus(); });
}
setShowKeyboardShortcutsButtonRef(ref) {
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,
buttonLocation,
showKeyboardShortcutsPanel,
closeKeyboardShortcutsPanel,
styles,
phrases,
} = this.props;
const toggleButtonText = showKeyboardShortcutsPanel
? phrases.hideKeyboardShortcutsPanel
: phrases.showKeyboardShortcutsPanel;
const bottomRight = buttonLocation === BOTTOM_RIGHT;
const topRight = buttonLocation === TOP_RIGHT;
const topLeft = buttonLocation === TOP_LEFT;
return (
<div>
<button
ref={this.setShowKeyboardShortcutsButtonRef}
{...css(
styles.DayPickerKeyboardShortcuts_buttonReset,
styles.DayPickerKeyboardShortcuts_show,
bottomRight && styles.DayPickerKeyboardShortcuts_show__bottomRight,
topRight && styles.DayPickerKeyboardShortcuts_show__topRight,
topLeft && styles.DayPickerKeyboardShortcuts_show__topLeft,
)}
type="button"
aria-label={toggleButtonText}
onClick={this.onShowKeyboardShortcutsButtonClick}
onMouseUp={(e) => {
e.currentTarget.blur();
}}
>
<span
{...css(
styles.DayPickerKeyboardShortcuts_showSpan,
bottomRight && styles.DayPickerKeyboardShortcuts_showSpan__bottomRight,
topRight && styles.DayPickerKeyboardShortcuts_showSpan__topRight,
topLeft && styles.DayPickerKeyboardShortcuts_showSpan__topLeft,
)}
>
?
</span>
</button>
{showKeyboardShortcutsPanel && (
<div
{...css(styles.DayPickerKeyboardShortcuts_panel)}
role="dialog"
aria-labelledby="DayPickerKeyboardShortcuts_title"
aria-describedby="DayPickerKeyboardShortcuts_description"
>
<div
{...css(styles.DayPickerKeyboardShortcuts_title)}
id="DayPickerKeyboardShortcuts_title"
>
{phrases.keyboardShortcuts}
</div>
<button
ref={this.setHideKeyboardShortcutsButtonRef}
{...css(
styles.DayPickerKeyboardShortcuts_buttonReset,
styles.DayPickerKeyboardShortcuts_close,
)}
type="button"
tabIndex="0"
aria-label={phrases.hideKeyboardShortcutsPanel}
onClick={closeKeyboardShortcutsPanel}
onKeyDown={this.onKeyDown}
>
<CloseButton {...css(styles.DayPickerKeyboardShortcuts_closeSvg)} />
</button>
<ul
{...css(styles.DayPickerKeyboardShortcuts_list)}
id="DayPickerKeyboardShortcuts_description"
>
{this.keyboardShortcuts.map(({ unicode, label, action }) => (
<KeyboardShortcutRow
key={label}
unicode={unicode}
label={label}
action={action}
block={block}
/>
))}
</ul>
</div>
)}
</div>
);
}
}
DayPickerKeyboardShortcuts.propTypes = propTypes;
DayPickerKeyboardShortcuts.defaultProps = defaultProps;
export default withStyles(({ reactDates: { color, font, zIndex } }) => ({
DayPickerKeyboardShortcuts_buttonReset: {
background: 'none',
border: 0,
borderRadius: 0,
color: 'inherit',
font: 'inherit',
lineHeight: 'normal',
overflow: 'visible',
padding: 0,
cursor: 'pointer',
fontSize: font.size,
':active': {
outline: 'none',
},
},
DayPickerKeyboardShortcuts_show: {
width: 33,
height: 26,
position: 'absolute',
zIndex: zIndex + 2,
'::before': {
content: '""',
display: 'block',
position: 'absolute',
},
},
DayPickerKeyboardShortcuts_show__bottomRight: {
bottom: 0,
right: 0,
'::before': {
borderTop: '26px solid transparent',
borderRight: `33px solid ${color.core.primary}`,
bottom: 0,
right: 0,
},
':hover::before': {
borderRight: `33px solid ${color.core.primary_dark}`,
},
},
DayPickerKeyboardShortcuts_show__topRight: {
top: 0,
right: 0,
'::before': {
borderBottom: '26px solid transparent',
borderRight: `33px solid ${color.core.primary}`,
top: 0,
right: 0,
},
':hover::before': {
borderRight: `33px solid ${color.core.primary_dark}`,
},
},
DayPickerKeyboardShortcuts_show__topLeft: {
top: 0,
left: 0,
'::before': {
borderBottom: '26px solid transparent',
borderLeft: `33px solid ${color.core.primary}`,
top: 0,
left: 0,
},
':hover::before': {
borderLeft: `33px solid ${color.core.primary_dark}`,
},
},
DayPickerKeyboardShortcuts_showSpan: {
color: color.core.white,
position: 'absolute',
},
DayPickerKeyboardShortcuts_showSpan__bottomRight: {
bottom: 0,
right: 5,
},
DayPickerKeyboardShortcuts_showSpan__topRight: {
top: 1,
right: 5,
},
DayPickerKeyboardShortcuts_showSpan__topLeft: {
top: 1,
left: 5,
},
DayPickerKeyboardShortcuts_panel: {
overflow: 'auto',
background: color.background,
border: `1px solid ${color.core.border}`,
borderRadius: 2,
position: 'absolute',
top: 0,
bottom: 0,
right: 0,
left: 0,
zIndex: zIndex + 2,
padding: 22,
margin: 33,
textAlign: 'left', // TODO: investigate use of text-align throughout the library
},
DayPickerKeyboardShortcuts_title: {
fontSize: 16,
fontWeight: 'bold',
margin: 0,
},
DayPickerKeyboardShortcuts_list: {
listStyle: 'none',
padding: 0,
fontSize: font.size,
},
DayPickerKeyboardShortcuts_close: {
position: 'absolute',
right: 22,
top: 22,
zIndex: zIndex + 2,
':active': {
outline: 'none',
},
},
DayPickerKeyboardShortcuts_closeSvg: {
height: 15,
width: 15,
fill: color.core.grayLighter,
':hover': {
fill: color.core.grayLight,
},
':focus': {
fill: color.core.grayLight,
},
},
}), { pureComponent: typeof React.PureComponent !== 'undefined' })(DayPickerKeyboardShortcuts);