UNPKG

10.6 kBJSXView Raw
1import React from 'react';
2import PropTypes from 'prop-types';
3import { forbidExtraProps, nonNegativeInteger } from 'airbnb-prop-types';
4import { withStyles, withStylesPropTypes } from 'react-with-styles';
5import throttle from 'lodash/throttle';
6import isTouchDevice from 'is-touch-device';
7
8import noflip from '../utils/noflip';
9import getInputHeight from '../utils/getInputHeight';
10import openDirectionShape from '../shapes/OpenDirectionShape';
11
12import {
13 OPEN_DOWN,
14 OPEN_UP,
15 FANG_HEIGHT_PX,
16 FANG_WIDTH_PX,
17 DEFAULT_VERTICAL_SPACING,
18 MODIFIER_KEY_NAMES,
19} from '../constants';
20
21const FANG_PATH_TOP = `M0,${FANG_HEIGHT_PX} ${FANG_WIDTH_PX},${FANG_HEIGHT_PX} ${FANG_WIDTH_PX / 2},0z`;
22const FANG_STROKE_TOP = `M0,${FANG_HEIGHT_PX} ${FANG_WIDTH_PX / 2},0 ${FANG_WIDTH_PX},${FANG_HEIGHT_PX}`;
23const FANG_PATH_BOTTOM = `M0,0 ${FANG_WIDTH_PX},0 ${FANG_WIDTH_PX / 2},${FANG_HEIGHT_PX}z`;
24const FANG_STROKE_BOTTOM = `M0,0 ${FANG_WIDTH_PX / 2},${FANG_HEIGHT_PX} ${FANG_WIDTH_PX},0`;
25
26const propTypes = forbidExtraProps({
27 ...withStylesPropTypes,
28 id: PropTypes.string.isRequired,
29 placeholder: PropTypes.string,
30 displayValue: PropTypes.string,
31 ariaLabel: PropTypes.string,
32 screenReaderMessage: PropTypes.string,
33 focused: PropTypes.bool,
34 disabled: PropTypes.bool,
35 required: PropTypes.bool,
36 readOnly: PropTypes.bool,
37 openDirection: openDirectionShape,
38 showCaret: PropTypes.bool,
39 verticalSpacing: nonNegativeInteger,
40 small: PropTypes.bool,
41 block: PropTypes.bool,
42 regular: PropTypes.bool,
43
44 onChange: PropTypes.func,
45 onFocus: PropTypes.func,
46 onKeyDownShiftTab: PropTypes.func,
47 onKeyDownTab: PropTypes.func,
48
49 onKeyDownArrowDown: PropTypes.func,
50 onKeyDownQuestionMark: PropTypes.func,
51
52 // accessibility
53 isFocused: PropTypes.bool, // describes actual DOM focus
54});
55
56const defaultProps = {
57 placeholder: 'Select Date',
58 displayValue: '',
59 ariaLabel: undefined,
60 screenReaderMessage: '',
61 focused: false,
62 disabled: false,
63 required: false,
64 readOnly: null,
65 openDirection: OPEN_DOWN,
66 showCaret: false,
67 verticalSpacing: DEFAULT_VERTICAL_SPACING,
68 small: false,
69 block: false,
70 regular: false,
71
72 onChange() {},
73 onFocus() {},
74 onKeyDownShiftTab() {},
75 onKeyDownTab() {},
76
77 onKeyDownArrowDown() {},
78 onKeyDownQuestionMark() {},
79
80 // accessibility
81 isFocused: false,
82};
83
84class DateInput extends React.PureComponent {
85 constructor(props) {
86 super(props);
87
88 this.state = {
89 dateString: '',
90 isTouchDevice: false,
91 };
92
93 this.onChange = this.onChange.bind(this);
94 this.onKeyDown = this.onKeyDown.bind(this);
95 this.setInputRef = this.setInputRef.bind(this);
96 this.throttledKeyDown = throttle(this.onFinalKeyDown, 300, { trailing: false });
97 }
98
99 componentDidMount() {
100 this.setState({ isTouchDevice: isTouchDevice() });
101 }
102
103 componentWillReceiveProps(nextProps) {
104 const { dateString } = this.state;
105 if (dateString && nextProps.displayValue) {
106 this.setState({
107 dateString: '',
108 });
109 }
110 }
111
112 componentDidUpdate(prevProps) {
113 const { focused, isFocused } = this.props;
114 if (prevProps.focused === focused && prevProps.isFocused === isFocused) return;
115
116 if (focused && isFocused) {
117 this.inputRef.focus();
118 }
119 }
120
121 onChange(e) {
122 const { onChange, onKeyDownQuestionMark } = this.props;
123 const dateString = e.target.value;
124
125 // In Safari, onKeyDown does not consistently fire ahead of onChange. As a result, we need to
126 // special case the `?` key so that it always triggers the appropriate callback, instead of
127 // modifying the input value
128 if (dateString[dateString.length - 1] === '?') {
129 onKeyDownQuestionMark(e);
130 } else {
131 this.setState({ dateString }, () => onChange(dateString));
132 }
133 }
134
135 onKeyDown(e) {
136 e.stopPropagation();
137 if (!MODIFIER_KEY_NAMES.has(e.key)) {
138 this.throttledKeyDown(e);
139 }
140 }
141
142 onFinalKeyDown(e) {
143 const {
144 onKeyDownShiftTab,
145 onKeyDownTab,
146 onKeyDownArrowDown,
147 onKeyDownQuestionMark,
148 } = this.props;
149 const { key } = e;
150
151 if (key === 'Tab') {
152 if (e.shiftKey) {
153 onKeyDownShiftTab(e);
154 } else {
155 onKeyDownTab(e);
156 }
157 } else if (key === 'ArrowDown') {
158 onKeyDownArrowDown(e);
159 } else if (key === '?') {
160 e.preventDefault();
161 onKeyDownQuestionMark(e);
162 }
163 }
164
165 setInputRef(ref) {
166 this.inputRef = ref;
167 }
168
169 render() {
170 const {
171 dateString,
172 isTouchDevice: isTouch,
173 } = this.state;
174 const {
175 id,
176 placeholder,
177 ariaLabel,
178 displayValue,
179 screenReaderMessage,
180 focused,
181 showCaret,
182 onFocus,
183 disabled,
184 required,
185 readOnly,
186 openDirection,
187 verticalSpacing,
188 small,
189 regular,
190 block,
191 styles,
192 theme: { reactDates },
193 css,
194 } = this.props;
195
196 const value = dateString || displayValue || '';
197 const screenReaderMessageId = `DateInput__screen-reader-message-${id}`;
198
199 const withFang = showCaret && focused;
200
201 const inputHeight = getInputHeight(reactDates, small);
202
203 return (
204 <div
205 {...css(
206 styles.DateInput,
207 small && styles.DateInput__small,
208 block && styles.DateInput__block,
209 withFang && styles.DateInput__withFang,
210 disabled && styles.DateInput__disabled,
211 withFang && openDirection === OPEN_DOWN && styles.DateInput__openDown,
212 withFang && openDirection === OPEN_UP && styles.DateInput__openUp,
213 )}
214 >
215 <input
216 {...css(
217 styles.DateInput_input,
218 small && styles.DateInput_input__small,
219 regular && styles.DateInput_input__regular,
220 readOnly && styles.DateInput_input__readOnly,
221 focused && styles.DateInput_input__focused,
222 disabled && styles.DateInput_input__disabled,
223 )}
224 aria-label={ariaLabel === undefined ? placeholder : ariaLabel}
225 type="text"
226 id={id}
227 name={id}
228 ref={this.setInputRef}
229 value={value}
230 onChange={this.onChange}
231 onKeyDown={this.onKeyDown}
232 onFocus={onFocus}
233 placeholder={placeholder}
234 autoComplete="off"
235 disabled={disabled}
236 readOnly={typeof readOnly === 'boolean' ? readOnly : isTouch}
237 required={required}
238 aria-describedby={screenReaderMessage && screenReaderMessageId}
239 />
240
241 {withFang && (
242 <svg
243 role="presentation"
244 focusable="false"
245 {...css(
246 styles.DateInput_fang,
247 openDirection === OPEN_DOWN && {
248 top: inputHeight + verticalSpacing - FANG_HEIGHT_PX - 1,
249 },
250 openDirection === OPEN_UP && {
251 bottom: inputHeight + verticalSpacing - FANG_HEIGHT_PX - 1,
252 },
253 )}
254 >
255 <path
256 {...css(styles.DateInput_fangShape)}
257 d={openDirection === OPEN_DOWN ? FANG_PATH_TOP : FANG_PATH_BOTTOM}
258 />
259 <path
260 {...css(styles.DateInput_fangStroke)}
261 d={openDirection === OPEN_DOWN ? FANG_STROKE_TOP : FANG_STROKE_BOTTOM}
262 />
263 </svg>
264 )}
265
266 {screenReaderMessage && (
267 <p {...css(styles.DateInput_screenReaderMessage)} id={screenReaderMessageId}>
268 {screenReaderMessage}
269 </p>
270 )}
271 </div>
272 );
273 }
274}
275
276DateInput.propTypes = propTypes;
277DateInput.defaultProps = defaultProps;
278
279export default withStyles(({
280 reactDates: {
281 border, color, sizing, spacing, font, zIndex,
282 },
283}) => ({
284 DateInput: {
285 margin: 0,
286 padding: spacing.inputPadding,
287 background: color.background,
288 position: 'relative',
289 display: 'inline-block',
290 width: sizing.inputWidth,
291 verticalAlign: 'middle',
292 },
293
294 DateInput__small: {
295 width: sizing.inputWidth_small,
296 },
297
298 DateInput__block: {
299 width: '100%',
300 },
301
302 DateInput__disabled: {
303 background: color.disabled,
304 color: color.textDisabled,
305 },
306
307 DateInput_input: {
308 fontWeight: 200,
309 fontSize: font.input.size,
310 lineHeight: font.input.lineHeight,
311 color: color.text,
312 backgroundColor: color.background,
313 width: '100%',
314 padding: `${spacing.displayTextPaddingVertical}px ${spacing.displayTextPaddingHorizontal}px`,
315 paddingTop: spacing.displayTextPaddingTop,
316 paddingBottom: spacing.displayTextPaddingBottom,
317 paddingLeft: noflip(spacing.displayTextPaddingLeft),
318 paddingRight: noflip(spacing.displayTextPaddingRight),
319 border: border.input.border,
320 borderTop: border.input.borderTop,
321 borderRight: noflip(border.input.borderRight),
322 borderBottom: border.input.borderBottom,
323 borderLeft: noflip(border.input.borderLeft),
324 borderRadius: border.input.borderRadius,
325 },
326
327 DateInput_input__small: {
328 fontSize: font.input.size_small,
329 lineHeight: font.input.lineHeight_small,
330 letterSpacing: font.input.letterSpacing_small,
331 padding: `${spacing.displayTextPaddingVertical_small}px ${spacing.displayTextPaddingHorizontal_small}px`,
332 paddingTop: spacing.displayTextPaddingTop_small,
333 paddingBottom: spacing.displayTextPaddingBottom_small,
334 paddingLeft: noflip(spacing.displayTextPaddingLeft_small),
335 paddingRight: noflip(spacing.displayTextPaddingRight_small),
336 },
337
338 DateInput_input__regular: {
339 fontWeight: 'auto',
340 },
341
342 DateInput_input__readOnly: {
343 userSelect: 'none',
344 },
345
346 DateInput_input__focused: {
347 outline: border.input.outlineFocused,
348 background: color.backgroundFocused,
349 border: border.input.borderFocused,
350 borderTop: border.input.borderTopFocused,
351 borderRight: noflip(border.input.borderRightFocused),
352 borderBottom: border.input.borderBottomFocused,
353 borderLeft: noflip(border.input.borderLeftFocused),
354 },
355
356 DateInput_input__disabled: {
357 background: color.disabled,
358 fontStyle: font.input.styleDisabled,
359 },
360
361 DateInput_screenReaderMessage: {
362 border: 0,
363 clip: 'rect(0, 0, 0, 0)',
364 height: 1,
365 margin: -1,
366 overflow: 'hidden',
367 padding: 0,
368 position: 'absolute',
369 width: 1,
370 },
371
372 DateInput_fang: {
373 position: 'absolute',
374 width: FANG_WIDTH_PX,
375 height: FANG_HEIGHT_PX,
376 left: 22, // TODO: should be noflip wrapped and handled by an isRTL prop
377 zIndex: zIndex + 2,
378 },
379
380 DateInput_fangShape: {
381 fill: color.background,
382 },
383
384 DateInput_fangStroke: {
385 stroke: color.core.border,
386 fill: 'transparent',
387 },
388}), { pureComponent: typeof React.PureComponent !== 'undefined' })(DateInput);