UNPKG

11.5 kBJSXView Raw
1import React from 'react';
2import PropTypes from 'prop-types';
3import { forbidExtraProps } from 'airbnb-prop-types';
4import { withStyles, withStylesPropTypes } from 'react-with-styles';
5
6import { DayPickerKeyboardShortcutsPhrases } from '../defaultPhrases';
7import getPhrasePropTypes from '../utils/getPhrasePropTypes';
8
9import KeyboardShortcutRow from './KeyboardShortcutRow';
10import CloseButton from './CloseButton';
11
12export const TOP_LEFT = 'top-left';
13export const TOP_RIGHT = 'top-right';
14export const BOTTOM_RIGHT = 'bottom-right';
15
16const propTypes = forbidExtraProps({
17 ...withStylesPropTypes,
18 block: PropTypes.bool,
19 // TODO: rename button location to be direction-agnostic
20 buttonLocation: PropTypes.oneOf([TOP_LEFT, TOP_RIGHT, BOTTOM_RIGHT]),
21 showKeyboardShortcutsPanel: PropTypes.bool,
22 openKeyboardShortcutsPanel: PropTypes.func,
23 closeKeyboardShortcutsPanel: PropTypes.func,
24 phrases: PropTypes.shape(getPhrasePropTypes(DayPickerKeyboardShortcutsPhrases)),
25 renderKeyboardShortcutsButton: PropTypes.func,
26});
27
28const defaultProps = {
29 block: false,
30 buttonLocation: BOTTOM_RIGHT,
31 showKeyboardShortcutsPanel: false,
32 openKeyboardShortcutsPanel() {},
33 closeKeyboardShortcutsPanel() {},
34 phrases: DayPickerKeyboardShortcutsPhrases,
35 renderKeyboardShortcutsButton: undefined,
36};
37
38function getKeyboardShortcuts(phrases) {
39 return [
40 {
41 unicode: '↵',
42 label: phrases.enterKey,
43 action: phrases.selectFocusedDate,
44 },
45 {
46 unicode: '←/→',
47 label: phrases.leftArrowRightArrow,
48 action: phrases.moveFocusByOneDay,
49 },
50 {
51 unicode: '↑/↓',
52 label: phrases.upArrowDownArrow,
53 action: phrases.moveFocusByOneWeek,
54 },
55 {
56 unicode: 'PgUp/PgDn',
57 label: phrases.pageUpPageDown,
58 action: phrases.moveFocusByOneMonth,
59 },
60 {
61 unicode: 'Home/End',
62 label: phrases.homeEnd,
63 action: phrases.moveFocustoStartAndEndOfWeek,
64 },
65 {
66 unicode: 'Esc',
67 label: phrases.escape,
68 action: phrases.returnFocusToInput,
69 },
70 {
71 unicode: '?',
72 label: phrases.questionMark,
73 action: phrases.openThisPanel,
74 },
75 ];
76}
77
78class DayPickerKeyboardShortcuts extends React.PureComponent {
79 constructor(...args) {
80 super(...args);
81
82 const { phrases } = this.props;
83 this.keyboardShortcuts = getKeyboardShortcuts(phrases);
84
85 this.onShowKeyboardShortcutsButtonClick = this.onShowKeyboardShortcutsButtonClick.bind(this);
86 this.setShowKeyboardShortcutsButtonRef = this.setShowKeyboardShortcutsButtonRef.bind(this);
87 this.setHideKeyboardShortcutsButtonRef = this.setHideKeyboardShortcutsButtonRef.bind(this);
88 this.handleFocus = this.handleFocus.bind(this);
89 this.onKeyDown = this.onKeyDown.bind(this);
90 }
91
92 componentWillReceiveProps(nextProps) {
93 const { phrases } = this.props;
94 if (nextProps.phrases !== phrases) {
95 this.keyboardShortcuts = getKeyboardShortcuts(nextProps.phrases);
96 }
97 }
98
99 componentDidUpdate() {
100 this.handleFocus();
101 }
102
103 onKeyDown(e) {
104 e.stopPropagation();
105
106 const { closeKeyboardShortcutsPanel } = this.props;
107 // Because the close button is the only focusable element inside of the panel, this
108 // amounts to a very basic focus trap. The user can exit the panel by "pressing" the
109 // close button or hitting escape
110 switch (e.key) {
111 case 'Escape':
112 closeKeyboardShortcutsPanel();
113 break;
114
115 // do nothing - this allows the up and down arrows continue their
116 // default behavior of scrolling the content of the Keyboard Shortcuts Panel
117 // which is needed when only a single month is shown for instance.
118 case 'ArrowUp':
119 case 'ArrowDown':
120 break;
121
122 // completely block the rest of the keys that have functionality outside of this panel
123 case 'Tab':
124 case 'Home':
125 case 'End':
126 case 'PageUp':
127 case 'PageDown':
128 case 'ArrowLeft':
129 case 'ArrowRight':
130 e.preventDefault();
131 break;
132
133 default:
134 break;
135 }
136 }
137
138 onShowKeyboardShortcutsButtonClick() {
139 const { openKeyboardShortcutsPanel } = this.props;
140
141 // we want to return focus to this button after closing the keyboard shortcuts panel
142 openKeyboardShortcutsPanel(() => { this.showKeyboardShortcutsButton.focus(); });
143 }
144
145 setShowKeyboardShortcutsButtonRef(ref) {
146 this.showKeyboardShortcutsButton = ref;
147 }
148
149 setHideKeyboardShortcutsButtonRef(ref) {
150 this.hideKeyboardShortcutsButton = ref;
151 }
152
153 handleFocus() {
154 if (this.hideKeyboardShortcutsButton) {
155 // automatically move focus into the dialog by moving
156 // to the only interactive element, the hide button
157 this.hideKeyboardShortcutsButton.focus();
158 }
159 }
160
161 render() {
162 const {
163 block,
164 buttonLocation,
165 css,
166 showKeyboardShortcutsPanel,
167 closeKeyboardShortcutsPanel,
168 styles,
169 phrases,
170 renderKeyboardShortcutsButton,
171 } = this.props;
172
173 const toggleButtonText = showKeyboardShortcutsPanel
174 ? phrases.hideKeyboardShortcutsPanel
175 : phrases.showKeyboardShortcutsPanel;
176
177 const bottomRight = buttonLocation === BOTTOM_RIGHT;
178 const topRight = buttonLocation === TOP_RIGHT;
179 const topLeft = buttonLocation === TOP_LEFT;
180
181 return (
182 <div>
183 {renderKeyboardShortcutsButton
184 && renderKeyboardShortcutsButton({
185 // passing in context-specific props
186 ref: this.setShowKeyboardShortcutsButtonRef,
187 onClick: this.onShowKeyboardShortcutsButtonClick,
188 ariaLabel: toggleButtonText,
189 })}
190 {renderKeyboardShortcutsButton || (
191 <button
192 ref={this.setShowKeyboardShortcutsButtonRef}
193 {...css(
194 styles.DayPickerKeyboardShortcuts_buttonReset,
195 styles.DayPickerKeyboardShortcuts_show,
196 bottomRight && styles.DayPickerKeyboardShortcuts_show__bottomRight,
197 topRight && styles.DayPickerKeyboardShortcuts_show__topRight,
198 topLeft && styles.DayPickerKeyboardShortcuts_show__topLeft,
199 )}
200 type="button"
201 aria-label={toggleButtonText}
202 onClick={this.onShowKeyboardShortcutsButtonClick}
203 onMouseUp={(e) => {
204 e.currentTarget.blur();
205 }}
206 >
207 <span
208 {...css(
209 styles.DayPickerKeyboardShortcuts_showSpan,
210 bottomRight && styles.DayPickerKeyboardShortcuts_showSpan__bottomRight,
211 topRight && styles.DayPickerKeyboardShortcuts_showSpan__topRight,
212 topLeft && styles.DayPickerKeyboardShortcuts_showSpan__topLeft,
213 )}
214 >
215 ?
216 </span>
217 </button>
218 )}
219 {showKeyboardShortcutsPanel && (
220 <div
221 {...css(styles.DayPickerKeyboardShortcuts_panel)}
222 role="dialog"
223 aria-labelledby="DayPickerKeyboardShortcuts_title"
224 aria-describedby="DayPickerKeyboardShortcuts_description"
225 >
226 <div
227 {...css(styles.DayPickerKeyboardShortcuts_title)}
228 id="DayPickerKeyboardShortcuts_title"
229 >
230 {phrases.keyboardShortcuts}
231 </div>
232
233 <button
234 ref={this.setHideKeyboardShortcutsButtonRef}
235 {...css(
236 styles.DayPickerKeyboardShortcuts_buttonReset,
237 styles.DayPickerKeyboardShortcuts_close,
238 )}
239 type="button"
240 tabIndex="0"
241 aria-label={phrases.hideKeyboardShortcutsPanel}
242 onClick={closeKeyboardShortcutsPanel}
243 onKeyDown={this.onKeyDown}
244 >
245 <CloseButton {...css(styles.DayPickerKeyboardShortcuts_closeSvg)} />
246 </button>
247
248 <ul
249 {...css(styles.DayPickerKeyboardShortcuts_list)}
250 id="DayPickerKeyboardShortcuts_description"
251 >
252 {this.keyboardShortcuts.map(({ unicode, label, action }) => (
253 <KeyboardShortcutRow
254 key={label}
255 unicode={unicode}
256 label={label}
257 action={action}
258 block={block}
259 />
260 ))}
261 </ul>
262 </div>
263 )}
264 </div>
265 );
266 }
267}
268
269DayPickerKeyboardShortcuts.propTypes = propTypes;
270DayPickerKeyboardShortcuts.defaultProps = defaultProps;
271
272export default withStyles(({ reactDates: { color, font, zIndex } }) => ({
273 DayPickerKeyboardShortcuts_buttonReset: {
274 background: 'none',
275 border: 0,
276 borderRadius: 0,
277 color: 'inherit',
278 font: 'inherit',
279 lineHeight: 'normal',
280 overflow: 'visible',
281 padding: 0,
282 cursor: 'pointer',
283 fontSize: font.size,
284
285 ':active': {
286 outline: 'none',
287 },
288 },
289
290 DayPickerKeyboardShortcuts_show: {
291 width: 33,
292 height: 26,
293 position: 'absolute',
294 zIndex: zIndex + 2,
295
296 '::before': {
297 content: '""',
298 display: 'block',
299 position: 'absolute',
300 },
301 },
302
303 DayPickerKeyboardShortcuts_show__bottomRight: {
304 bottom: 0,
305 right: 0,
306
307 '::before': {
308 borderTop: '26px solid transparent',
309 borderRight: `33px solid ${color.core.primary}`,
310 bottom: 0,
311 right: 0,
312 },
313
314 ':hover::before': {
315 borderRight: `33px solid ${color.core.primary_dark}`,
316 },
317 },
318
319 DayPickerKeyboardShortcuts_show__topRight: {
320 top: 0,
321 right: 0,
322
323 '::before': {
324 borderBottom: '26px solid transparent',
325 borderRight: `33px solid ${color.core.primary}`,
326 top: 0,
327 right: 0,
328 },
329
330 ':hover::before': {
331 borderRight: `33px solid ${color.core.primary_dark}`,
332 },
333 },
334
335 DayPickerKeyboardShortcuts_show__topLeft: {
336 top: 0,
337 left: 0,
338
339 '::before': {
340 borderBottom: '26px solid transparent',
341 borderLeft: `33px solid ${color.core.primary}`,
342 top: 0,
343 left: 0,
344 },
345
346 ':hover::before': {
347 borderLeft: `33px solid ${color.core.primary_dark}`,
348 },
349 },
350
351 DayPickerKeyboardShortcuts_showSpan: {
352 color: color.core.white,
353 position: 'absolute',
354 },
355
356 DayPickerKeyboardShortcuts_showSpan__bottomRight: {
357 bottom: 0,
358 right: 5,
359 },
360
361 DayPickerKeyboardShortcuts_showSpan__topRight: {
362 top: 1,
363 right: 5,
364 },
365
366 DayPickerKeyboardShortcuts_showSpan__topLeft: {
367 top: 1,
368 left: 5,
369 },
370
371 DayPickerKeyboardShortcuts_panel: {
372 overflow: 'auto',
373 background: color.background,
374 border: `1px solid ${color.core.border}`,
375 borderRadius: 2,
376 position: 'absolute',
377 top: 0,
378 bottom: 0,
379 right: 0,
380 left: 0,
381 zIndex: zIndex + 2,
382 padding: 22,
383 margin: 33,
384 textAlign: 'left', // TODO: investigate use of text-align throughout the library
385 },
386
387 DayPickerKeyboardShortcuts_title: {
388 fontSize: 16,
389 fontWeight: 'bold',
390 margin: 0,
391 },
392
393 DayPickerKeyboardShortcuts_list: {
394 listStyle: 'none',
395 padding: 0,
396 fontSize: font.size,
397 },
398
399 DayPickerKeyboardShortcuts_close: {
400 position: 'absolute',
401 right: 22,
402 top: 22,
403 zIndex: zIndex + 2,
404
405 ':active': {
406 outline: 'none',
407 },
408 },
409
410 DayPickerKeyboardShortcuts_closeSvg: {
411 height: 15,
412 width: 15,
413 fill: color.core.grayLighter,
414
415 ':hover': {
416 fill: color.core.grayLight,
417 },
418
419 ':focus': {
420 fill: color.core.grayLight,
421 },
422 },
423}), { pureComponent: typeof React.PureComponent !== 'undefined' })(DayPickerKeyboardShortcuts);