UNPKG

15 kBJavaScriptView Raw
1const _excluded = ["value", "use12HourClock", "padValues", "emptyCharacter", "precision", "noClearButton", "hoursAddon", "minutesAddon", "secondsAddon", "millisecondsAddon", "className", "disabled", "readOnly", "datePart", "onChange", "onBlur", "onFocus"];
2
3function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
4
5function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }
6
7import classNames from 'classnames';
8import qsa from 'dom-helpers/querySelectorAll';
9import PropTypes from 'prop-types';
10import React, { useCallback, useRef, useState } from 'react';
11import { useUncontrolled } from 'uncontrollable';
12import Button from './Button';
13import DateTimePartInput from './DateTimePartInput';
14import { times } from './Icon';
15import Widget from './Widget';
16import dates from './dates';
17import useFocusManager from './useFocusManager';
18
19const selectTextRange = el => {
20 if (el instanceof HTMLInputElement) return el.select();
21 const range = document.createRange();
22 range.selectNodeContents(el);
23 const selection = window.getSelection();
24
25 if (selection) {
26 selection.removeAllRanges();
27 selection.addRange(range);
28 }
29}; // prettier-ignore
30
31
32const isEmptyValue = (p, precision) => p.hours == null && p.minutes == null && (precision != 'seconds' && precision !== 'milliseconds' || p.seconds == null) && (precision !== 'milliseconds' || p.milliseconds == null); // prettier-ignore
33
34
35const isPartialValue = (p, precision) => p.hours == null || p.minutes == null || (precision === 'seconds' || precision === 'milliseconds') && p.seconds == null || precision === 'milliseconds' && p.milliseconds == null;
36
37const getValueParts = (value, use12HourClock) => {
38 let hours, minutes, seconds, milliseconds;
39 let meridiem = 'AM';
40
41 if (value) {
42 hours = value.getHours();
43
44 if (use12HourClock) {
45 meridiem = hours < 12 ? 'AM' : 'PM';
46 hours = hours % 12 || 12;
47 }
48
49 minutes = value.getMinutes();
50 seconds = value.getSeconds();
51 milliseconds = value.getMilliseconds();
52 }
53
54 return {
55 hours,
56 minutes,
57 seconds,
58 milliseconds,
59 meridiem
60 };
61};
62
63const TEST_VALID = {
64 hours: /^([1]?[0-9]|2[0-3])$/,
65 hours12: /^(1[0-2]|0?[1-9])$/,
66 minutes: /^([0-5]?\d)$/,
67 seconds: /^([0-5]?\d)$/,
68 milliseconds: /^(\d{1,3})$/
69};
70const TEST_COMPLETE = {
71 hours: /^([3-9]|\d{2})$/,
72 hours12: /^(\d{2}|[2-9])$/,
73 minutes: /^(d{2}|[6-9])$/,
74 seconds: /^(d{2}|[6-9])$/,
75 milliseconds: /^(\d{3})$/
76};
77
78function testPart(value, part, use12HourClock, tests) {
79 const key = part === 'hours' && use12HourClock ? 'hours12' : part;
80 return tests[key].test(value);
81}
82
83const isValid = (value, part, use12HourClock) => testPart(value, part, use12HourClock, TEST_VALID);
84
85const isComplete = (value, part, use12HourClock) => testPart(value, part, use12HourClock, TEST_COMPLETE);
86
87const propTypes = {
88 /**
89 * @example ['valuePicker', [ ['new Date()'] ]]
90 */
91 value: PropTypes.instanceOf(Date),
92
93 /**
94 * @example ['onChangePicker', [ ['new Date()'] ]]
95 */
96 onChange: PropTypes.func,
97
98 /**
99 * The default date used to construct a new time when the `value` is empty
100 *
101 * @default new Date()
102 **/
103 datePart: PropTypes.instanceOf(Date),
104
105 /**
106 * Use a 12 hour clock (with AM/PM) instead of 24 hour one.
107 * The configured localizer may provide a default value .
108 **/
109 use12HourClock: PropTypes.bool,
110
111 /** Time part values will be padded by `0` */
112 padValues: PropTypes.bool,
113
114 /** The string character used to pad empty, or cleared values */
115 emptyCharacter: PropTypes.string,
116
117 /** Hide the input clear button */
118 noClearButton: PropTypes.bool,
119
120 /**
121 * @example ['disabled', ['new Date()']]
122 */
123 disabled: PropTypes.bool,
124
125 /**
126 * @example ['readOnly', ['new Date()']]
127 */
128 readOnly: PropTypes.bool,
129
130 /** Controls how precise of a time can be input **/
131 precision: PropTypes.oneOf(['minutes', 'seconds', 'milliseconds']).isRequired,
132
133 /**
134 * The seperator between hours and minutes
135 * @default ':'
136 */
137 hoursAddon: PropTypes.node,
138
139 /**
140 * The seperator between hours and minutes
141 * @default ':'
142 */
143 minutesAddon: PropTypes.node,
144
145 /**
146 * The seperator between hours and minutes
147 * @default ':'
148 */
149 secondsAddon: PropTypes.node,
150
151 /**
152 * The seperator between hours and minutes
153 * @default '.'
154 */
155 millisecondsAddon: PropTypes.node
156};
157const defaultProps = {
158 hoursAddon: ':',
159 padValues: true,
160 precision: 'minutes',
161 emptyCharacter: '-'
162};
163
164// let count = 0
165function useTimePartState(value, use12HourClock) {
166 const [state, setState] = useState(() => ({
167 value,
168 use12HourClock,
169 timeParts: getValueParts(value, use12HourClock)
170 }));
171 const setTimeParts = useCallback(timeParts => setState(s => Object.assign({}, s, {
172 timeParts
173 })), [setState]);
174
175 if (state.value !== value || state.use12HourClock !== use12HourClock) {
176 // count++
177 // if (count < 100)
178 setState({
179 value,
180 use12HourClock,
181 timeParts: getValueParts(value, use12HourClock)
182 });
183 }
184
185 return [state.timeParts, setTimeParts];
186}
187
188function TimeInput(uncontrolledProps) {
189 const _useUncontrolled = useUncontrolled(uncontrolledProps, {
190 value: 'onChange'
191 }),
192 {
193 value,
194 use12HourClock,
195 padValues: pad,
196 emptyCharacter,
197 precision,
198 noClearButton,
199 hoursAddon,
200 minutesAddon,
201 secondsAddon,
202 millisecondsAddon,
203 className,
204 disabled,
205 readOnly,
206 datePart,
207 onChange,
208 onBlur,
209 onFocus
210 } = _useUncontrolled,
211 props = _objectWithoutPropertiesLoose(_useUncontrolled, _excluded);
212
213 let minsAddon = minutesAddon !== undefined ? minutesAddon : precision === 'seconds' || precision === 'milliseconds' ? ':' : '';
214 let secsAddon = secondsAddon !== undefined ? secondsAddon : precision === 'milliseconds' ? ':' : '';
215 const ref = useRef(null);
216 const hourRef = useRef(null);
217 const [focusEvents, focused] = useFocusManager(ref, {
218 disabled,
219 onBlur,
220 onFocus
221 }, {
222 didHandle: (focused, e) => {
223 var _hourRef$current;
224
225 if (!focused) return;
226 if (!e.target.dataset.focusable) (_hourRef$current = hourRef.current) == null ? void 0 : _hourRef$current.focus();else select(e.target);
227 }
228 });
229 const [timeParts, setTimeParts] = useTimePartState(value != null ? value : null, use12HourClock != null ? use12HourClock : false);
230
231 function getDatePart() {
232 return dates.startOf(datePart || new Date(), 'day');
233 }
234
235 const getMin = part => part === 'hours' ? 1 : 0;
236
237 const getMax = part => {
238 if (part === 'hours') return use12HourClock ? 12 : 23;
239 if (part === 'milliseconds') return 999;
240 return 59;
241 };
242
243 function select(target = document.activeElement) {
244 window.Promise.resolve().then(() => {
245 if (focused) selectTextRange(target);
246 });
247 }
248 /**
249 * Handlers
250 */
251
252
253 const handleClear = () => {
254 var _hourRef$current2;
255
256 (_hourRef$current2 = hourRef.current) == null ? void 0 : _hourRef$current2.focus();
257 if (value) onChange(null);else setTimeParts(getValueParts(null));
258 };
259
260 const handleChange = (part, event) => {
261 const currentValue = timeParts[part];
262 const {
263 target
264 } = event;
265 const rawValue = target.value;
266 let strValue = `${currentValue || ''}${rawValue}`;
267 let numValue = +strValue;
268
269 if (isNaN(numValue) || strValue && !isValid(strValue, part, use12HourClock != null ? use12HourClock : false)) {
270 // the combined value is now past the max or invalid so try the single
271 // digit and "start over" filling the value
272 if (isValid(rawValue, part, use12HourClock != null ? use12HourClock : false) && !isNaN(+rawValue)) {
273 // change the effective current value
274 strValue = rawValue;
275 numValue = +rawValue;
276 } else {
277 return event.preventDefault();
278 }
279 }
280
281 const nextValue = target.value ? numValue : null;
282 notifyChange({
283 [part]: nextValue
284 });
285
286 if (nextValue != null && isComplete(strValue, part, use12HourClock != null ? use12HourClock : false)) {
287 focusNext(event.currentTarget, +1);
288 } else {
289 select(target);
290 }
291 };
292
293 const handleSelect = ({
294 target
295 }) => {
296 select(target);
297 };
298
299 const handleKeyDown = (part, event) => {
300 const {
301 key
302 } = event;
303 const input = event.currentTarget;
304 const {
305 selectionStart: start,
306 selectionEnd: end
307 } = input;
308 const isRTL = getComputedStyle(input).getPropertyValue('direction') === 'rtl';
309 const isMeridiem = part === 'meridiem';
310 const isNext = key === (isRTL ? 'ArrowLeft' : 'ArrowRight');
311 const isPrev = key === (isRTL ? 'ArrowRight' : 'ArrowLeft');
312
313 if (key === 'ArrowUp') {
314 event.preventDefault();
315 increment(part, 1);
316 }
317
318 if (key === 'ArrowDown') {
319 event.preventDefault();
320 increment(part, -1);
321 }
322
323 if (isPrev && (isMeridiem || start - 1 < 0)) {
324 event.preventDefault();
325 focusNext(input, -1);
326 }
327
328 if (isNext && (isMeridiem || input.value.length <= end + 1)) {
329 event.preventDefault();
330 focusNext(input, +1);
331 }
332
333 if (readOnly && key !== 'Tab') {
334 event.preventDefault();
335 }
336
337 if (isMeridiem) {
338 if (key === 'a' || key === 'A') notifyChange({
339 meridiem: 'AM'
340 });
341 if (key === 'p' || key === 'P') notifyChange({
342 meridiem: 'PM'
343 });
344 }
345 };
346
347 const increment = (part, inc) => {
348 let nextPart = timeParts[part];
349
350 if (part === 'meridiem') {
351 nextPart = nextPart === 'AM' ? 'PM' : 'AM';
352 } else {
353 nextPart = (nextPart || 0) + inc;
354 if (!isValid(String(nextPart), part, use12HourClock != null ? use12HourClock : false)) return;
355 }
356
357 notifyChange({
358 [part]: nextPart
359 });
360 select();
361 };
362
363 function notifyChange(updates) {
364 const nextTimeParts = Object.assign({}, timeParts, updates);
365
366 if (value && isEmptyValue(nextTimeParts, precision)) {
367 return onChange(null);
368 }
369
370 if (isPartialValue(nextTimeParts, precision)) return setTimeParts(nextTimeParts);
371 let {
372 hours,
373 minutes,
374 seconds,
375 milliseconds,
376 meridiem
377 } = nextTimeParts;
378 let nextDate = new Date(value || getDatePart());
379
380 if (use12HourClock) {
381 if (hours === 12) hours = 0;
382 hours += meridiem === 'PM' ? 12 : 0;
383 }
384
385 nextDate.setHours(hours);
386 nextDate.setMinutes(minutes);
387 if (seconds != null) nextDate.setSeconds(seconds);
388 if (milliseconds != null) nextDate.setMilliseconds(milliseconds);
389 onChange(nextDate, {
390 lastValue: value,
391 timeParts
392 });
393 }
394
395 function focusNext(input, delta) {
396 let nodes = qsa(ref.current, '* [data-focusable]');
397 let next = nodes[nodes.indexOf(input) + delta];
398 next == null ? void 0 : next.focus();
399 select(next);
400 }
401
402 const {
403 hours,
404 minutes,
405 seconds,
406 milliseconds,
407 meridiem
408 } = timeParts;
409 const showClear = !isEmptyValue(timeParts, precision);
410 return /*#__PURE__*/React.createElement(Widget, _extends({}, props, {
411 role: "group",
412 ref: ref
413 }, focusEvents, {
414 focused: focused,
415 disabled: disabled,
416 readOnly: readOnly,
417 className: classNames(className, 'rw-time-input rw-widget-input')
418 }), /*#__PURE__*/React.createElement(DateTimePartInput, {
419 size: 2,
420 pad: pad ? 2 : undefined,
421 value: hours,
422 disabled: disabled,
423 readOnly: readOnly,
424 "aria-label": "hours",
425 min: getMin('hours'),
426 max: getMax('hours'),
427 ref: hourRef,
428 emptyChar: emptyCharacter,
429 onSelect: handleSelect,
430 onChange: e => handleChange('hours', e),
431 onKeyDown: e => handleKeyDown('hours', e)
432 }), hoursAddon && /*#__PURE__*/React.createElement("span", null, hoursAddon), /*#__PURE__*/React.createElement(DateTimePartInput, {
433 size: 2,
434 pad: pad ? 2 : undefined,
435 value: minutes,
436 disabled: disabled,
437 readOnly: readOnly,
438 "aria-label": "minutes",
439 min: getMin('minutes'),
440 max: getMax('minutes'),
441 emptyChar: emptyCharacter,
442 onSelect: handleSelect,
443 onChange: e => handleChange('minutes', e),
444 onKeyDown: e => handleKeyDown('minutes', e)
445 }), minsAddon && /*#__PURE__*/React.createElement("span", null, minsAddon), (precision === 'seconds' || precision === 'milliseconds') && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(DateTimePartInput, {
446 size: 2,
447 pad: pad ? 2 : undefined,
448 value: seconds,
449 disabled: disabled,
450 readOnly: readOnly,
451 "aria-label": "seconds",
452 min: getMin('seconds'),
453 max: getMax('seconds'),
454 emptyChar: emptyCharacter,
455 onSelect: handleSelect,
456 onChange: e => handleChange('seconds', e),
457 onKeyDown: e => handleKeyDown('seconds', e)
458 }), secsAddon && /*#__PURE__*/React.createElement("span", null, secsAddon)), precision === 'milliseconds' && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(DateTimePartInput, {
459 size: 3,
460 pad: pad ? 3 : undefined,
461 value: milliseconds,
462 disabled: disabled,
463 readOnly: readOnly,
464 "aria-label": "milliseconds",
465 min: getMin('milliseconds'),
466 max: getMax('milliseconds'),
467 emptyChar: emptyCharacter,
468 onSelect: handleSelect,
469 onChange: e => handleChange('milliseconds', e),
470 onKeyDown: e => handleKeyDown('milliseconds', e)
471 }), millisecondsAddon && /*#__PURE__*/React.createElement("span", null, millisecondsAddon)), use12HourClock && /*#__PURE__*/React.createElement("div", {
472 role: "listbox",
473 "aria-label": "AM/PM",
474 "aria-disabled": disabled,
475 "aria-readonly": readOnly,
476 className: "rw-time-part-meridiem"
477 }, /*#__PURE__*/React.createElement("div", {
478 "data-focusable": true,
479 role: "option",
480 "aria-atomic": true,
481 "aria-selected": true,
482 "aria-setsize": 2,
483 "aria-live": "assertive",
484 "aria-disabled": disabled,
485 "aria-readonly": readOnly,
486 "aria-posinset": meridiem === 'AM' ? 1 : 2,
487 tabIndex: !disabled ? 0 : void 0,
488 onFocus: handleSelect,
489 onSelect: handleSelect,
490 onKeyDown: e => handleKeyDown('meridiem', e)
491 }, /*#__PURE__*/React.createElement("abbr", null, meridiem))), !noClearButton && /*#__PURE__*/React.createElement(Button, {
492 label: 'clear input',
493 onClick: handleClear,
494 disabled: disabled || readOnly,
495 className: classNames('rw-time-input-clear', showClear && 'rw-show')
496 }, times));
497}
498
499TimeInput.propTypes = propTypes;
500TimeInput.defaultProps = defaultProps;
501export default TimeInput;
\No newline at end of file