1 | import React from 'react';
|
2 | import PropTypes from 'prop-types';
|
3 | import { forbidExtraProps, nonNegativeInteger } from 'airbnb-prop-types';
|
4 | import { withStyles, withStylesPropTypes } from 'react-with-styles';
|
5 | import throttle from 'lodash/throttle';
|
6 | import isTouchDevice from 'is-touch-device';
|
7 |
|
8 | import noflip from '../utils/noflip';
|
9 | import getInputHeight from '../utils/getInputHeight';
|
10 | import openDirectionShape from '../shapes/OpenDirectionShape';
|
11 |
|
12 | import {
|
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 |
|
21 | const FANG_PATH_TOP = `M0,${FANG_HEIGHT_PX} ${FANG_WIDTH_PX},${FANG_HEIGHT_PX} ${FANG_WIDTH_PX / 2},0z`;
|
22 | const FANG_STROKE_TOP = `M0,${FANG_HEIGHT_PX} ${FANG_WIDTH_PX / 2},0 ${FANG_WIDTH_PX},${FANG_HEIGHT_PX}`;
|
23 | const FANG_PATH_BOTTOM = `M0,0 ${FANG_WIDTH_PX},0 ${FANG_WIDTH_PX / 2},${FANG_HEIGHT_PX}z`;
|
24 | const FANG_STROKE_BOTTOM = `M0,0 ${FANG_WIDTH_PX / 2},${FANG_HEIGHT_PX} ${FANG_WIDTH_PX},0`;
|
25 |
|
26 | const 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 |
|
53 | isFocused: PropTypes.bool,
|
54 | });
|
55 |
|
56 | const 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 |
|
81 | isFocused: false,
|
82 | };
|
83 |
|
84 | class 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 |
|
126 |
|
127 |
|
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 |
|
276 | DateInput.propTypes = propTypes;
|
277 | DateInput.defaultProps = defaultProps;
|
278 |
|
279 | export 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,
|
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);
|