1 | const _excluded = ["value", "use12HourClock", "padValues", "emptyCharacter", "precision", "noClearButton", "hoursAddon", "minutesAddon", "secondsAddon", "millisecondsAddon", "className", "disabled", "readOnly", "datePart", "onChange", "onBlur", "onFocus"];
|
2 |
|
3 | function _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 |
|
5 | function _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 |
|
7 | import classNames from 'classnames';
|
8 | import qsa from 'dom-helpers/querySelectorAll';
|
9 | import PropTypes from 'prop-types';
|
10 | import React, { useCallback, useRef, useState } from 'react';
|
11 | import { useUncontrolled } from 'uncontrollable';
|
12 | import Button from './Button';
|
13 | import DateTimePartInput from './DateTimePartInput';
|
14 | import { times } from './Icon';
|
15 | import Widget from './Widget';
|
16 | import dates from './dates';
|
17 | import useFocusManager from './useFocusManager';
|
18 |
|
19 | const 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 | };
|
30 |
|
31 |
|
32 | const isEmptyValue = (p, precision) => p.hours == null && p.minutes == null && (precision != 'seconds' && precision !== 'milliseconds' || p.seconds == null) && (precision !== 'milliseconds' || p.milliseconds == null);
|
33 |
|
34 |
|
35 | const isPartialValue = (p, precision) => p.hours == null || p.minutes == null || (precision === 'seconds' || precision === 'milliseconds') && p.seconds == null || precision === 'milliseconds' && p.milliseconds == null;
|
36 |
|
37 | const 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 |
|
63 | const 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 | };
|
70 | const 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 |
|
78 | function testPart(value, part, use12HourClock, tests) {
|
79 | const key = part === 'hours' && use12HourClock ? 'hours12' : part;
|
80 | return tests[key].test(value);
|
81 | }
|
82 |
|
83 | const isValid = (value, part, use12HourClock) => testPart(value, part, use12HourClock, TEST_VALID);
|
84 |
|
85 | const isComplete = (value, part, use12HourClock) => testPart(value, part, use12HourClock, TEST_COMPLETE);
|
86 |
|
87 | const propTypes = {
|
88 | |
89 |
|
90 |
|
91 | value: PropTypes.instanceOf(Date),
|
92 |
|
93 | |
94 |
|
95 |
|
96 | onChange: PropTypes.func,
|
97 |
|
98 | |
99 |
|
100 |
|
101 |
|
102 |
|
103 | datePart: PropTypes.instanceOf(Date),
|
104 |
|
105 | |
106 |
|
107 |
|
108 |
|
109 | use12HourClock: PropTypes.bool,
|
110 |
|
111 |
|
112 | padValues: PropTypes.bool,
|
113 |
|
114 |
|
115 | emptyCharacter: PropTypes.string,
|
116 |
|
117 |
|
118 | noClearButton: PropTypes.bool,
|
119 |
|
120 | |
121 |
|
122 |
|
123 | disabled: PropTypes.bool,
|
124 |
|
125 | |
126 |
|
127 |
|
128 | readOnly: PropTypes.bool,
|
129 |
|
130 |
|
131 | precision: PropTypes.oneOf(['minutes', 'seconds', 'milliseconds']).isRequired,
|
132 |
|
133 | |
134 |
|
135 |
|
136 |
|
137 | hoursAddon: PropTypes.node,
|
138 |
|
139 | |
140 |
|
141 |
|
142 |
|
143 | minutesAddon: PropTypes.node,
|
144 |
|
145 | |
146 |
|
147 |
|
148 |
|
149 | secondsAddon: PropTypes.node,
|
150 |
|
151 | |
152 |
|
153 |
|
154 |
|
155 | millisecondsAddon: PropTypes.node
|
156 | };
|
157 | const defaultProps = {
|
158 | hoursAddon: ':',
|
159 | padValues: true,
|
160 | precision: 'minutes',
|
161 | emptyCharacter: '-'
|
162 | };
|
163 |
|
164 |
|
165 | function 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 |
|
177 |
|
178 | setState({
|
179 | value,
|
180 | use12HourClock,
|
181 | timeParts: getValueParts(value, use12HourClock)
|
182 | });
|
183 | }
|
184 |
|
185 | return [state.timeParts, setTimeParts];
|
186 | }
|
187 |
|
188 | function 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 |
|
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 |
|
271 |
|
272 | if (isValid(rawValue, part, use12HourClock != null ? use12HourClock : false) && !isNaN(+rawValue)) {
|
273 |
|
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 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 | }), 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 && React.createElement("span", null, hoursAddon), 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 && React.createElement("span", null, minsAddon), (precision === 'seconds' || precision === 'milliseconds') && React.createElement(React.Fragment, null, 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 && React.createElement("span", null, secsAddon)), precision === 'milliseconds' && React.createElement(React.Fragment, null, 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 && React.createElement("span", null, millisecondsAddon)), use12HourClock && 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 | }, 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 | }, React.createElement("abbr", null, meridiem))), !noClearButton && 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 |
|
499 | TimeInput.propTypes = propTypes;
|
500 | TimeInput.defaultProps = defaultProps;
|
501 | export default TimeInput; |
\ | No newline at end of file |