@servicetitan/anvil2 1.42.2 → 1.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/{Combobox-D2aSaDkz.js → Combobox-C7ANSGIu.js} +2 -2
  3. package/dist/{Combobox-D2aSaDkz.js.map → Combobox-C7ANSGIu.js.map} +1 -1
  4. package/dist/Combobox.js +1 -1
  5. package/dist/{DateField-D28_sa7P.js → DateField-_TqCdcYv.js} +4 -3
  6. package/dist/{DateField-D28_sa7P.js.map → DateField-_TqCdcYv.js.map} +1 -1
  7. package/dist/DateField.js +1 -1
  8. package/dist/{DateFieldRange--oSGfjYa.js → DateFieldRange-Dk8WA52F.js} +2 -2
  9. package/dist/{DateFieldRange--oSGfjYa.js.map → DateFieldRange-Dk8WA52F.js.map} +1 -1
  10. package/dist/DateFieldRange.js +1 -1
  11. package/dist/{DateFieldSingle-0a8Bk7Yj.js → DateFieldSingle-D3xD8YZk.js} +2 -2
  12. package/dist/{DateFieldSingle-0a8Bk7Yj.js.map → DateFieldSingle-D3xD8YZk.js.map} +1 -1
  13. package/dist/DateFieldSingle.js +1 -1
  14. package/dist/{DateFieldYearless-DCv9WJdu.js → DateFieldYearless-BxkCSNk5.js} +2 -2
  15. package/dist/{DateFieldYearless-DCv9WJdu.js.map → DateFieldYearless-BxkCSNk5.js.map} +1 -1
  16. package/dist/DateFieldYearless-CAUHW6Ow-EUWxJ0OY.js +1431 -0
  17. package/dist/DateFieldYearless-CAUHW6Ow-EUWxJ0OY.js.map +1 -0
  18. package/dist/DateFieldYearless.js +1 -1
  19. package/dist/{Page-KN0DLtcf.js → Page-CxB5N9dR.js} +36 -36
  20. package/dist/{Page-KN0DLtcf.js.map → Page-CxB5N9dR.js.map} +1 -1
  21. package/dist/Page.css +72 -72
  22. package/dist/Page.js +1 -1
  23. package/dist/{Popover-B1HaUjGI.js → Popover-BPiqdyu1.js} +2 -2
  24. package/dist/{Popover-B1HaUjGI.js.map → Popover-BPiqdyu1.js.map} +1 -1
  25. package/dist/{Popover-CU2cGVD8-FWJOuFRj.js → Popover-CU2cGVD8-Casl3vM1.js} +2 -2
  26. package/dist/{Popover-CU2cGVD8-FWJOuFRj.js.map → Popover-CU2cGVD8-Casl3vM1.js.map} +1 -1
  27. package/dist/Popover.js +1 -1
  28. package/dist/TimeField-DRHLRqN3.js +913 -0
  29. package/dist/TimeField-DRHLRqN3.js.map +1 -0
  30. package/dist/TimeField.css +10 -0
  31. package/dist/TimeField.d.ts +2 -0
  32. package/dist/TimeField.js +2 -0
  33. package/dist/TimeField.js.map +1 -0
  34. package/dist/{Toolbar-BznMJKGJ.js → Toolbar-CSWhVSUM.js} +2 -2
  35. package/dist/{Toolbar-BznMJKGJ.js.map → Toolbar-CSWhVSUM.js.map} +1 -1
  36. package/dist/Toolbar.js +1 -1
  37. package/dist/assets/icons/st/gnav_field_pro_active.svg +1 -1
  38. package/dist/assets/icons/st/gnav_field_pro_inactive.svg +1 -1
  39. package/dist/components/TimeField/TimeField.d.ts +53 -0
  40. package/dist/components/TimeField/index.d.ts +1 -0
  41. package/dist/components/index.d.ts +1 -0
  42. package/dist/event-BEJFimi3.js +6 -0
  43. package/dist/event-BEJFimi3.js.map +1 -0
  44. package/dist/index.js +9 -8
  45. package/dist/index.js.map +1 -1
  46. package/dist/usePopoverCloseDelayWorkaround-BZcjPkvT-BZcjPkvT.js +18 -0
  47. package/dist/usePopoverCloseDelayWorkaround-BZcjPkvT-BZcjPkvT.js.map +1 -0
  48. package/dist/{DateFieldYearless-C3_oGDr3-5meexzZO.js → usePopoverSupport-B9Lsqryr-DhZHMoNb.js} +470 -1429
  49. package/dist/usePopoverSupport-B9Lsqryr-DhZHMoNb.js.map +1 -0
  50. package/package.json +4 -4
  51. package/dist/DateFieldYearless-C3_oGDr3-5meexzZO.js.map +0 -1
  52. package/dist/usePopoverCloseDelayWorkaround-BhhG-xEB-hfJZaXHC.js +0 -21
  53. package/dist/usePopoverCloseDelayWorkaround-BhhG-xEB-hfJZaXHC.js.map +0 -1
@@ -0,0 +1,913 @@
1
+ import { jsx, jsxs } from 'react/jsx-runtime';
2
+ import * as React from 'react';
3
+ import { forwardRef, useState, useRef, useEffect, useMemo, useCallback, useImperativeHandle } from 'react';
4
+ import { useTrackingId } from './useTrackingId.js';
5
+ import { c as childrenToString } from './childrenToString-Bz9MqbHb-Bz9MqbHb.js';
6
+ import { T as TextField } from './TextField-CRTh0gL_-D2CjcYXX.js';
7
+ import { u as usePopoverSupport, a as useMaskito, e as maskitoTimeOptionsGenerator, b as maskitoWithPlaceholder } from './usePopoverSupport-B9Lsqryr-DhZHMoNb.js';
8
+ import { I as Icon } from './Icon-B6HmlQiR-BxQkO3X5.js';
9
+ import { c as cx } from './index-tZvMCc77.js';
10
+ import { u as useMergeRefs } from './useMergeRefs-Bde85AWI-Bde85AWI.js';
11
+ import { P as Popover } from './Popover-CU2cGVD8-Casl3vM1.js';
12
+ import { b as Listbox } from './Listbox-B-WUuj-_-BWWeWtLW.js';
13
+ import { u as useOptionallyControlledState } from './useOptionallyControlledState-DAv5LXXh-DAv5LXXh.js';
14
+ import { u as usePopoverCloseDelayWorkaround } from './usePopoverCloseDelayWorkaround-BZcjPkvT-BZcjPkvT.js';
15
+
16
+ import './TimeField.css';const SvgAccessTime = (props) => /* @__PURE__ */ React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", width: "1em", height: "1em", viewBox: "0 0 24 24", ...props }, /* @__PURE__ */ React.createElement("path", { d: "M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm-.22-13h-.06c-.4 0-.72.32-.72.72v4.72c0 .35.18.68.49.86l4.15 2.49c.34.2.78.1.98-.24a.71.71 0 0 0-.25-.99l-3.87-2.3V7.72c0-.4-.32-.72-.72-.72z" }));
17
+
18
+ const timePlaceholderMask = ({
19
+ format,
20
+ placeholder
21
+ }) => {
22
+ const timeOptions = maskitoTimeOptionsGenerator({
23
+ mode: format === 12 ? "HH:MM AA" : "HH:MM",
24
+ step: 0
25
+ // We handle step logic in your business layer
26
+ });
27
+ const { plugins, removePlaceholder, ...placeholderOptions } = maskitoWithPlaceholder(placeholder);
28
+ const timePlaceholderMask2 = {
29
+ ...timeOptions,
30
+ plugins: plugins.concat(timeOptions.plugins || []),
31
+ preprocessors: [
32
+ ...placeholderOptions.preprocessors,
33
+ ...timeOptions.preprocessors
34
+ ],
35
+ postprocessors: [
36
+ ...timeOptions.postprocessors,
37
+ ...placeholderOptions.postprocessors
38
+ ]
39
+ };
40
+ return { options: timePlaceholderMask2, removePlaceholder };
41
+ };
42
+
43
+ const TIME_CONSTANTS = {
44
+ /** Minutes in a full day (24 hours * 60 minutes) */
45
+ MINUTES_IN_DAY: 1440,
46
+ /** Maximum hour in 24-hour format */
47
+ MAX_HOUR_24: 23,
48
+ /** Maximum minute value */
49
+ MAX_MINUTE: 59,
50
+ /** Timeout in milliseconds for input debouncing */
51
+ INPUT_DEBOUNCE_TIMEOUT: 100
52
+ };
53
+ const TIME_FORMAT_PLACEHOLDERS = {
54
+ 12: "––:–– ––",
55
+ 24: "––:––"
56
+ };
57
+ const CLOCK_ICON = SvgAccessTime;
58
+
59
+ function formatTimeString(hours, minutes, format) {
60
+ if (hours === 24) {
61
+ hours = 0;
62
+ }
63
+ if (format === 12) {
64
+ let displayHours = hours;
65
+ let period = "AM";
66
+ if (hours === 0) {
67
+ displayHours = 12;
68
+ } else if (hours > 12) {
69
+ displayHours = hours - 12;
70
+ period = "PM";
71
+ } else if (hours === 12) {
72
+ period = "PM";
73
+ }
74
+ return `${displayHours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")} ${period}`;
75
+ } else {
76
+ return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`;
77
+ }
78
+ }
79
+
80
+ const MAX_HOUR = TIME_CONSTANTS.MAX_HOUR_24;
81
+ const MAX_MINUTE = TIME_CONSTANTS.MAX_MINUTE;
82
+ function getConstraintMinutes(min, max) {
83
+ const minMinutes = min ? timeToMinutes(convertTimeFormat(min, 24)) : 0;
84
+ const maxMinutes = max ? timeToMinutes(convertTimeFormat(max, 24)) : TIME_CONSTANTS.MINUTES_IN_DAY;
85
+ return { minMinutes, maxMinutes };
86
+ }
87
+ function isWithinConstraints(timeMinutes, minMinutes, maxMinutes) {
88
+ return timeMinutes >= minMinutes && timeMinutes <= maxMinutes;
89
+ }
90
+ const timeToMinutes = (timeStr) => {
91
+ const match = timeStr.match(/^(\d{1,2}):(\d{2})(?:\s*(AM|PM))?$/i);
92
+ if (!match) return 0;
93
+ let hours = parseInt(match[1], 10);
94
+ const minutes = parseInt(match[2], 10);
95
+ const period = match[3]?.toUpperCase();
96
+ if (minutes > MAX_MINUTE) return 0;
97
+ if (period === "PM" && hours !== 12) {
98
+ hours += 12;
99
+ } else if (period === "AM" && hours === 12) {
100
+ hours = 0;
101
+ }
102
+ if (hours > MAX_HOUR) return 0;
103
+ return hours * 60 + minutes;
104
+ };
105
+ function convertTimeFormat(time, targetFormat) {
106
+ if (!time) return time;
107
+ const minutes = timeToMinutes(time);
108
+ if (minutes === 0 && time !== "00:00" && time !== "12:00 AM") {
109
+ return time;
110
+ }
111
+ const hours = Math.floor(minutes / 60);
112
+ const mins = minutes % 60;
113
+ return formatTimeString(hours, mins, targetFormat);
114
+ }
115
+ function normalizePartialInput(value) {
116
+ const cleanValue = value.replace(/[–_:]/g, "").replace(/\s+/g, "");
117
+ if (/^\d{1,2}$/.test(cleanValue)) {
118
+ const result = `${cleanValue}:00`;
119
+ return result;
120
+ } else if (/^\d{1,2}:\d{1}$/.test(cleanValue)) {
121
+ const result = `${cleanValue}0`;
122
+ return result;
123
+ } else if (/^\d{4}$/.test(cleanValue)) {
124
+ const result = `${cleanValue.slice(0, 2)}:${cleanValue.slice(2)}`;
125
+ return result;
126
+ } else if (/^\d{4}(AM|PM)$/i.test(cleanValue)) {
127
+ const digits = cleanValue.slice(0, 4);
128
+ const ampm = cleanValue.slice(4);
129
+ const result = `${digits.slice(0, 2)}:${digits.slice(2)} ${ampm}`;
130
+ return result;
131
+ }
132
+ return value;
133
+ }
134
+ function parse12HourFormat(normalizedValue, min, max) {
135
+ const match = normalizedValue.match(/^(\d{1,2}):(\d{1,2})(?:\s*(AM|PM))?$/i);
136
+ if (!match) return null;
137
+ let hours = parseInt(match[1], 10);
138
+ let minutes = parseInt(match[2], 10);
139
+ const ampm = match[3]?.toUpperCase();
140
+ if (minutes < 10 && normalizedValue.match(/^(\d{1,2}):(\d{1})$/)) {
141
+ minutes *= 10;
142
+ }
143
+ if (ampm === "PM") {
144
+ if (hours !== 12) {
145
+ hours += 12;
146
+ }
147
+ } else if (ampm === "AM") {
148
+ if (hours === 12) {
149
+ hours = 0;
150
+ }
151
+ } else {
152
+ if (hours === 12) {
153
+ hours = 12;
154
+ } else {
155
+ if (min || max) {
156
+ const amHours = hours;
157
+ const pmHours = hours + 12;
158
+ const { minMinutes, maxMinutes } = getConstraintMinutes(min, max);
159
+ const amMinutes = amHours * 60 + minutes;
160
+ const pmMinutes = pmHours * 60 + minutes;
161
+ const amValid = isWithinConstraints(amMinutes, minMinutes, maxMinutes);
162
+ const pmValid = isWithinConstraints(pmMinutes, minMinutes, maxMinutes);
163
+ if (amValid && pmValid) ; else if (pmValid) {
164
+ hours = pmHours;
165
+ } else ;
166
+ }
167
+ }
168
+ }
169
+ return { hours, minutes };
170
+ }
171
+ function parse24HourFormat(normalizedValue) {
172
+ const match = normalizedValue.match(/^(\d{1,2}):(\d{1,2})$/);
173
+ if (!match) {
174
+ return null;
175
+ }
176
+ const hours = parseInt(match[1], 10);
177
+ let minutes = parseInt(match[2], 10);
178
+ if (minutes < 10 && normalizedValue.match(/^(\d{1,2}):(\d{1})$/)) {
179
+ minutes *= 10;
180
+ }
181
+ const result = { hours, minutes };
182
+ return result;
183
+ }
184
+ function validate12HourConstraints(timeString, value, min, max) {
185
+ if (!min && !max) return true;
186
+ const originalHasAMPM = value.toUpperCase().includes("AM") || value.toUpperCase().includes("PM");
187
+ if (!originalHasAMPM) {
188
+ const timePart = timeString.replace(/\s*(AM|PM)$/i, "");
189
+ const amTime = `${timePart} AM`;
190
+ const pmTime = `${timePart} PM`;
191
+ const amMinutes = timeToMinutes(convertTimeFormat(amTime, 24));
192
+ const pmMinutes = timeToMinutes(convertTimeFormat(pmTime, 24));
193
+ const { minMinutes, maxMinutes } = getConstraintMinutes(min, max);
194
+ return isWithinConstraints(amMinutes, minMinutes, maxMinutes) || isWithinConstraints(pmMinutes, minMinutes, maxMinutes);
195
+ } else {
196
+ const timeMinutes = timeToMinutes(convertTimeFormat(timeString, 24));
197
+ const { minMinutes, maxMinutes } = getConstraintMinutes(min, max);
198
+ return isWithinConstraints(timeMinutes, minMinutes, maxMinutes);
199
+ }
200
+ }
201
+ function validate24HourConstraints(timeString, min, max) {
202
+ if (!min && !max) return true;
203
+ const timeMinutes = timeToMinutes(timeString);
204
+ const { minMinutes, maxMinutes } = getConstraintMinutes(min, max);
205
+ if (isWithinConstraints(timeMinutes, minMinutes, maxMinutes)) {
206
+ return true;
207
+ }
208
+ const hours = Math.floor(timeMinutes / 60);
209
+ const minutes = timeMinutes % 60;
210
+ if (minutes === 0) {
211
+ for (let h = minMinutes; h <= maxMinutes; h += 60) {
212
+ const potentialHour = Math.floor(h / 60);
213
+ if (potentialHour.toString().startsWith(hours.toString())) {
214
+ return true;
215
+ }
216
+ }
217
+ }
218
+ if (hours >= Math.floor(minMinutes / 60) && hours <= Math.floor(maxMinutes / 60)) {
219
+ const hourMinutes = hours * 60;
220
+ const hourMinMinutes = Math.max(minMinutes, hourMinutes);
221
+ const hourMaxMinutes = Math.min(maxMinutes, hourMinutes + 59);
222
+ for (let m = hourMinMinutes; m <= hourMaxMinutes; m++) {
223
+ const potentialMinutes = m % 60;
224
+ if (potentialMinutes.toString().startsWith(minutes.toString())) {
225
+ return true;
226
+ }
227
+ }
228
+ }
229
+ return false;
230
+ }
231
+ function parseInputValue(value, format, removePlaceholder, min, max) {
232
+ const valueMinusPlaceholder = removePlaceholder(value);
233
+ if (valueMinusPlaceholder === "") {
234
+ return {
235
+ time: null,
236
+ isInputValid: false,
237
+ isInputEmpty: true
238
+ };
239
+ }
240
+ const normalizedValue = normalizePartialInput(valueMinusPlaceholder);
241
+ const parsed = format === 12 ? parse12HourFormat(normalizedValue, min, max) : parse24HourFormat(normalizedValue);
242
+ if (!parsed) {
243
+ return {
244
+ time: null,
245
+ isInputValid: false,
246
+ isInputEmpty: false
247
+ };
248
+ }
249
+ const { hours, minutes } = parsed;
250
+ if (hours < 0 || hours > MAX_HOUR || minutes < 0 || minutes > MAX_MINUTE) {
251
+ return {
252
+ time: null,
253
+ isInputValid: false,
254
+ isInputEmpty: false
255
+ };
256
+ }
257
+ const timeString = formatTimeString(hours, minutes, format);
258
+ const isValid = format === 12 ? validate12HourConstraints(timeString, value, min, max) : validate24HourConstraints(timeString, min, max);
259
+ if (!isValid) {
260
+ return {
261
+ time: null,
262
+ isInputValid: false,
263
+ isInputEmpty: false
264
+ };
265
+ }
266
+ return {
267
+ time: timeString,
268
+ isInputValid: true,
269
+ isInputEmpty: false
270
+ };
271
+ }
272
+
273
+ function isTimeOnStep(time, step) {
274
+ if (!step || step <= 0) return true;
275
+ const totalMinutes = timeToMinutes(time);
276
+ return totalMinutes % step === 0;
277
+ }
278
+ function isValidTime(time) {
279
+ if (!time) return false;
280
+ return timeToMinutes(time) > 0 || time === "00:00";
281
+ }
282
+ function isPartialInputPotentiallyValid(inputValue, format, min, max) {
283
+ if (!min || !max) return false;
284
+ const digits = inputValue.replace(/[^\d]/g, "");
285
+ if (digits.length === 1) {
286
+ const digit = parseInt(digits, 10);
287
+ const minMinutes = timeToMinutes(min);
288
+ const maxMinutes = timeToMinutes(max);
289
+ for (let h = 0; h <= 23; h++) {
290
+ const hourStr = h.toString().padStart(2, "0");
291
+ const hourStrNoPad = h.toString();
292
+ const startsWithDigit = hourStr.startsWith(digit.toString()) || hourStrNoPad.startsWith(digit.toString());
293
+ if (!startsWithDigit) continue;
294
+ if (format === 24) {
295
+ const hourMinutes = h * 60;
296
+ if (hourMinutes >= minMinutes && hourMinutes <= maxMinutes) {
297
+ return true;
298
+ }
299
+ } else {
300
+ if (h === 0) {
301
+ if (digit === 1) {
302
+ const midnightMinutes = 0;
303
+ if (midnightMinutes >= minMinutes && midnightMinutes <= maxMinutes) {
304
+ return true;
305
+ }
306
+ }
307
+ } else if (h >= 1 && h <= 12) {
308
+ const amHour24 = h === 12 ? 0 : h;
309
+ const amMinutes = amHour24 * 60;
310
+ if (amMinutes >= minMinutes && amMinutes <= maxMinutes) {
311
+ return true;
312
+ }
313
+ const pmHour24 = h === 12 ? 12 : h + 12;
314
+ const pmMinutes = pmHour24 * 60;
315
+ if (pmMinutes >= minMinutes && pmMinutes <= maxMinutes) {
316
+ return true;
317
+ }
318
+ }
319
+ }
320
+ }
321
+ }
322
+ return false;
323
+ }
324
+
325
+ const styles = {
326
+ "time-field": "_time-field_11xzw_2"
327
+ };
328
+
329
+ const MaskedTimeInput = forwardRef(
330
+ ({
331
+ lastValidTime,
332
+ format,
333
+ placeholder,
334
+ min,
335
+ max,
336
+ onInputChange,
337
+ onKeyDown,
338
+ onFocus,
339
+ className,
340
+ ...props
341
+ }, ref) => {
342
+ const [inputValue, setInputValue] = useState(placeholder);
343
+ const { options, removePlaceholder } = timePlaceholderMask({
344
+ format,
345
+ placeholder
346
+ });
347
+ const maskedInputRef = useMaskito({ options });
348
+ const inputRef = useRef(null);
349
+ const combinedRef = useMergeRefs([maskedInputRef, inputRef, ref]);
350
+ const previousTimeRef = useRef(null);
351
+ const isUserTypingRef = useRef(false);
352
+ const inputValueRef = useRef(placeholder);
353
+ const previousPlaceholderRef = useRef(placeholder);
354
+ const currentParsedData = useMemo(() => {
355
+ return parseInputValue(inputValue, format, removePlaceholder, min, max);
356
+ }, [inputValue, format, removePlaceholder, min, max]);
357
+ useEffect(() => {
358
+ inputValueRef.current = inputValue;
359
+ }, [inputValue]);
360
+ useEffect(() => {
361
+ previousPlaceholderRef.current = placeholder;
362
+ }, [placeholder]);
363
+ useEffect(() => {
364
+ if (lastValidTime === void 0) return;
365
+ if (lastValidTime === previousTimeRef.current) return;
366
+ if (isUserTypingRef.current) return;
367
+ const newInputValue = lastValidTime ? convertTimeFormat(lastValidTime, format) : placeholder;
368
+ setInputValue(newInputValue);
369
+ inputValueRef.current = newInputValue;
370
+ previousTimeRef.current = lastValidTime;
371
+ }, [lastValidTime, format, placeholder]);
372
+ useEffect(() => {
373
+ if (placeholder === previousPlaceholderRef.current) return;
374
+ if (lastValidTime && inputValue === lastValidTime) return;
375
+ const isOldPlaceholder = inputValue === TIME_FORMAT_PLACEHOLDERS[12] || inputValue === TIME_FORMAT_PLACEHOLDERS[24];
376
+ if (isOldPlaceholder) {
377
+ setInputValue(placeholder);
378
+ }
379
+ previousPlaceholderRef.current = placeholder;
380
+ }, [placeholder, lastValidTime, inputValue]);
381
+ const handleChange = useCallback(
382
+ (event) => {
383
+ isUserTypingRef.current = true;
384
+ const { time, isInputValid, isInputEmpty } = parseInputValue(
385
+ event.target.value,
386
+ format,
387
+ removePlaceholder,
388
+ min,
389
+ max
390
+ );
391
+ const isPotentiallyValidPartial = !isInputValid && !isInputEmpty && event.target.value.length >= placeholder.length && // User is typing, not deleting
392
+ isPartialInputPotentiallyValid(event.target.value, format, min, max);
393
+ if (isInputValid || isInputEmpty || isPotentiallyValidPartial) {
394
+ setInputValue(event.target.value);
395
+ } else {
396
+ setInputValue(placeholder);
397
+ }
398
+ onInputChange?.({
399
+ event,
400
+ time,
401
+ isInputValid,
402
+ isInputEmpty
403
+ });
404
+ setTimeout(() => {
405
+ isUserTypingRef.current = false;
406
+ }, TIME_CONSTANTS.INPUT_DEBOUNCE_TIMEOUT);
407
+ },
408
+ [format, removePlaceholder, onInputChange, min, max, placeholder]
409
+ );
410
+ const resetTypingFlag = useCallback(() => {
411
+ isUserTypingRef.current = false;
412
+ }, []);
413
+ useImperativeHandle(
414
+ ref,
415
+ () => ({
416
+ ...inputRef.current,
417
+ focus: () => inputRef.current?.focus(),
418
+ resetTypingFlag
419
+ })
420
+ );
421
+ const classNames = useMemo(() => {
422
+ return cx(styles["time-field"], className);
423
+ }, [className]);
424
+ return /* @__PURE__ */ jsx(
425
+ TextField,
426
+ {
427
+ ref: combinedRef,
428
+ "data-input-valid": currentParsedData.isInputValid,
429
+ "data-input-empty": currentParsedData.isInputEmpty,
430
+ "data-time": currentParsedData.time ?? "",
431
+ value: inputValue,
432
+ onChange: handleChange,
433
+ prefix: /* @__PURE__ */ jsx(Icon, { svg: CLOCK_ICON }),
434
+ autoComplete: "off",
435
+ onKeyDown,
436
+ onFocus,
437
+ className: classNames,
438
+ ...props
439
+ }
440
+ );
441
+ }
442
+ );
443
+ MaskedTimeInput.displayName = "MaskedTimeInput";
444
+
445
+ function generateTimeOptions({
446
+ step = 30,
447
+ min,
448
+ max,
449
+ format
450
+ }) {
451
+ const options = [];
452
+ const minMinutes = min ? timeToMinutes(min) : 0;
453
+ const maxMinutes = max ? timeToMinutes(max) : TIME_CONSTANTS.MINUTES_IN_DAY - 1;
454
+ for (let minutes = 0; minutes < TIME_CONSTANTS.MINUTES_IN_DAY; minutes += step) {
455
+ if (minutes < minMinutes || minutes > maxMinutes) {
456
+ continue;
457
+ }
458
+ const hours = Math.floor(minutes / 60);
459
+ const mins = minutes % 60;
460
+ options.push(formatTimeString(hours, mins, format));
461
+ }
462
+ return options;
463
+ }
464
+ function normalizeTimeString(str) {
465
+ return str.replace(/[–_:]/g, "").replace(/\s+/g, "").toLowerCase();
466
+ }
467
+ function filterTimeOptions(options, userInput) {
468
+ if (!userInput || userInput === TIME_FORMAT_PLACEHOLDERS[12] || userInput === TIME_FORMAT_PLACEHOLDERS[24]) {
469
+ return options;
470
+ }
471
+ const normalizedInput = normalizeTimeString(userInput);
472
+ const filtered = options.filter((option) => {
473
+ const normalizedOption = normalizeTimeString(option);
474
+ return normalizedOption.startsWith(normalizedInput);
475
+ });
476
+ return filtered;
477
+ }
478
+
479
+ function getNextFutureStep(time, step, format) {
480
+ if (!step || step <= 0) return time;
481
+ const totalMinutes = timeToMinutes(time);
482
+ const nextStep = Math.ceil(totalMinutes / step) * step;
483
+ const hours = Math.floor(nextStep / 60);
484
+ const minutes = nextStep % 60;
485
+ return formatTimeString(hours, minutes, format);
486
+ }
487
+ function handleAutoRounding(time, options) {
488
+ const { autoround, step, format } = options;
489
+ if (!autoround || !time) return time;
490
+ const stepValue = step ?? 30;
491
+ if (isTimeOnStep(time, stepValue)) {
492
+ return time;
493
+ }
494
+ return getNextFutureStep(time, stepValue, format);
495
+ }
496
+ function generateAmPmVariants(currentTime, step, inputValue) {
497
+ const hasAM = inputValue.toUpperCase().includes("AM");
498
+ const hasPM = inputValue.toUpperCase().includes("PM");
499
+ const getNextStepForTime = (time) => {
500
+ const currentMinutes = timeToMinutes(time);
501
+ const nextStepMinutes = Math.ceil(currentMinutes / step) * step;
502
+ const finalMinutes = nextStepMinutes === currentMinutes ? nextStepMinutes + step : nextStepMinutes;
503
+ const hours = Math.floor(finalMinutes / 60);
504
+ const minutes = finalMinutes % 60;
505
+ return formatTimeString(hours, minutes, 12);
506
+ };
507
+ const nextStep = getNextFutureStep(currentTime, step, 12);
508
+ if (hasAM || hasPM) {
509
+ return [nextStep];
510
+ } else {
511
+ const currentMinutes = timeToMinutes(currentTime);
512
+ const pmMinutes = currentMinutes + 12 * 60;
513
+ const pmHours = Math.floor(pmMinutes / 60);
514
+ const pmMins = pmMinutes % 60;
515
+ const pmVersion = getNextStepForTime(formatTimeString(pmHours, pmMins, 24));
516
+ return [nextStep, pmVersion];
517
+ }
518
+ }
519
+ function generatePmInterpretation(currentTime, step, inputValue) {
520
+ if (inputValue.toUpperCase().includes("AM") || inputValue.toUpperCase().includes("PM")) {
521
+ return [];
522
+ }
523
+ const currentMinutes = timeToMinutes(currentTime);
524
+ const pmMinutes = currentMinutes + 12 * 60;
525
+ const pmHours = Math.floor(pmMinutes / 60);
526
+ const pmMins = pmMinutes % 60;
527
+ const pmTime = formatTimeString(pmHours, pmMins, 12);
528
+ if (isValidTime(pmTime)) {
529
+ const pmNextStep = getNextFutureStep(pmTime, step, 12);
530
+ return [pmNextStep];
531
+ }
532
+ return [];
533
+ }
534
+ function getAutoRoundingOptions(currentTime, step, format, inputValue, min) {
535
+ if (!currentTime) {
536
+ return [];
537
+ }
538
+ if (!isValidTime(currentTime)) {
539
+ if (format === 12) {
540
+ return generatePmInterpretation(currentTime, step, inputValue);
541
+ }
542
+ return [];
543
+ }
544
+ const nextStep = getNextFutureStep(currentTime, step, format);
545
+ if (format === 12 && !min) {
546
+ return generateAmPmVariants(currentTime, step, inputValue);
547
+ }
548
+ return [nextStep];
549
+ }
550
+
551
+ const TimeFieldElement = forwardRef(
552
+ function TimeField2({
553
+ value: valueProp,
554
+ defaultValue,
555
+ min,
556
+ max,
557
+ onChange,
558
+ format = 12,
559
+ step = 30,
560
+ required,
561
+ description,
562
+ error: errorProp,
563
+ disableSuggestions = false,
564
+ autoround = false,
565
+ className,
566
+ ...rest
567
+ }, ref) {
568
+ const [lastValidTime, setLastValidTime] = useOptionallyControlledState({
569
+ controlledValue: valueProp,
570
+ defaultValue: defaultValue ?? null
571
+ });
572
+ const [isDropdownOpen, setIsDropdownOpenRaw] = useState(false);
573
+ const setIsDropdownOpen = (open) => {
574
+ setIsDropdownOpenRaw(open);
575
+ };
576
+ const [selectedOptionIndex, setSelectedOptionIndex] = useState(0);
577
+ const [inputValueForFiltering, setInputValueForFiltering] = useState("");
578
+ const [currentParsedTime, setCurrentParsedTime] = useState(
579
+ null
580
+ );
581
+ const [inputParsedData, setInputParsedData] = useState({ time: null, isInputValid: false, isInputEmpty: true });
582
+ const lastOnChangeCallRef = useRef(null);
583
+ const isDropdownClickingRef = useRef(false);
584
+ const [justSelected, setJustSelected] = useState(false);
585
+ const tabJustPressedRef = useRef(false);
586
+ const inputRef = useRef(null);
587
+ const popoverContentRef = useRef(null);
588
+ const combinedRef = useMergeRefs([inputRef, ref]);
589
+ const popoverSupported = usePopoverSupport();
590
+ const placeholder = TIME_FORMAT_PLACEHOLDERS[format];
591
+ const shouldShowOptionsFromHook = usePopoverCloseDelayWorkaround(isDropdownOpen);
592
+ const shouldShowOptions = tabJustPressedRef.current ? false : shouldShowOptionsFromHook;
593
+ const dropdownTimeoutRef = useRef(null);
594
+ useEffect(() => {
595
+ const dropdownRef = dropdownTimeoutRef.current;
596
+ return () => {
597
+ if (dropdownRef) {
598
+ clearTimeout(dropdownRef);
599
+ }
600
+ };
601
+ }, []);
602
+ const allTimeOptions = useMemo(
603
+ () => generateTimeOptions({ step, min, max, format }),
604
+ [step, min, max, format]
605
+ );
606
+ const baseFilteredOptions = useMemo(() => {
607
+ if (!inputValueForFiltering || inputValueForFiltering.match(/^[–_:]+$/)) {
608
+ return allTimeOptions;
609
+ }
610
+ return filterTimeOptions(allTimeOptions, inputValueForFiltering);
611
+ }, [allTimeOptions, inputValueForFiltering]);
612
+ const autoRoundingOptions = useMemo(() => {
613
+ if (!autoround || baseFilteredOptions.length > 0 || !inputValueForFiltering || inputValueForFiltering.match(/^[–_:]+$/)) {
614
+ return [];
615
+ }
616
+ const currentTime = currentParsedTime || inputParsedData.time;
617
+ const autoRoundedOptions = getAutoRoundingOptions(
618
+ currentTime,
619
+ step ?? 30,
620
+ format,
621
+ inputValueForFiltering,
622
+ min
623
+ );
624
+ return autoRoundedOptions.filter(
625
+ (option) => !baseFilteredOptions.includes(option)
626
+ );
627
+ }, [
628
+ autoround,
629
+ baseFilteredOptions,
630
+ inputValueForFiltering,
631
+ currentParsedTime,
632
+ inputParsedData.time,
633
+ format,
634
+ min,
635
+ step
636
+ ]);
637
+ const filteredOptions = useMemo(() => {
638
+ if (baseFilteredOptions.length === 0 && autoRoundingOptions.length === 0) {
639
+ if (!inputParsedData.isInputValid && inputValueForFiltering && !inputValueForFiltering.match(/^[–_:]+$/)) {
640
+ return allTimeOptions;
641
+ }
642
+ }
643
+ return [...baseFilteredOptions, ...autoRoundingOptions];
644
+ }, [
645
+ baseFilteredOptions,
646
+ autoRoundingOptions,
647
+ allTimeOptions,
648
+ inputValueForFiltering,
649
+ inputParsedData.isInputValid
650
+ ]);
651
+ const handleBlur = useCallback(
652
+ (event) => {
653
+ if (event.relatedTarget && popoverContentRef.current?.contains(event.relatedTarget) && isDropdownClickingRef.current) {
654
+ isDropdownClickingRef.current = false;
655
+ return;
656
+ }
657
+ setIsDropdownOpen(false);
658
+ setSelectedOptionIndex(0);
659
+ const input = event.target;
660
+ const parsedInputTime = input.dataset.time || null;
661
+ const isInputValid = input.dataset.inputValid === "true";
662
+ const isInputEmpty = input.dataset.inputEmpty === "true";
663
+ let finalTime = parsedInputTime;
664
+ if (autoround && finalTime && isInputValid) {
665
+ finalTime = handleAutoRounding(finalTime, {
666
+ autoround,
667
+ step,
668
+ format
669
+ });
670
+ }
671
+ if (finalTime) {
672
+ inputRef.current?.resetTypingFlag?.();
673
+ setLastValidTime(finalTime);
674
+ }
675
+ const currentChange = {
676
+ time: finalTime,
677
+ isInputValid,
678
+ isInputEmpty
679
+ };
680
+ const lastChange = lastOnChangeCallRef.current;
681
+ const isDuplicate = lastChange && lastChange.time === currentChange.time && lastChange.isInputValid === currentChange.isInputValid && lastChange.isInputEmpty === currentChange.isInputEmpty;
682
+ if (!isDuplicate) {
683
+ onChange?.(currentChange);
684
+ lastOnChangeCallRef.current = currentChange;
685
+ }
686
+ },
687
+ // eslint-disable-next-line react-hooks/exhaustive-deps
688
+ [format, step, autoround, onChange]
689
+ );
690
+ const handleFocus = useCallback(() => {
691
+ if (!disableSuggestions && !justSelected) {
692
+ setIsDropdownOpen(true);
693
+ setSelectedOptionIndex(0);
694
+ }
695
+ }, [disableSuggestions, justSelected]);
696
+ useEffect(() => {
697
+ if (filteredOptions.length <= 1 && isDropdownOpen) {
698
+ setIsDropdownOpen(false);
699
+ }
700
+ if (!disableSuggestions && filteredOptions.length > 1 && document.activeElement && document.activeElement.tagName === "INPUT" && !isDropdownOpen && !justSelected && !tabJustPressedRef.current) {
701
+ setIsDropdownOpen(true);
702
+ setSelectedOptionIndex(0);
703
+ }
704
+ }, [filteredOptions, disableSuggestions, isDropdownOpen, justSelected]);
705
+ const handleInputChange = (change) => {
706
+ setInputValueForFiltering(change.event.target.value);
707
+ setCurrentParsedTime(change.time);
708
+ setInputParsedData({
709
+ time: change.time,
710
+ isInputValid: change.isInputValid,
711
+ isInputEmpty: change.isInputEmpty
712
+ });
713
+ setSelectedOptionIndex(0);
714
+ setJustSelected(false);
715
+ };
716
+ const scrollToSelectedOption = useCallback(() => {
717
+ if (!popoverContentRef.current || selectedOptionIndex < 0) return;
718
+ const optionElements = popoverContentRef.current.querySelectorAll('[role="option"]');
719
+ const targetElement = optionElements[selectedOptionIndex];
720
+ if (targetElement) {
721
+ targetElement.scrollIntoView({
722
+ block: "center",
723
+ behavior: "auto"
724
+ });
725
+ }
726
+ }, [selectedOptionIndex]);
727
+ useEffect(() => {
728
+ if (isDropdownOpen && selectedOptionIndex >= 0) {
729
+ scrollToSelectedOption();
730
+ }
731
+ }, [selectedOptionIndex, isDropdownOpen, scrollToSelectedOption]);
732
+ const handleOptionSelect = useCallback(
733
+ (selectedTime) => {
734
+ if (inputRef.current?.resetTypingFlag) {
735
+ inputRef.current.resetTypingFlag();
736
+ }
737
+ setIsDropdownOpen(false);
738
+ setSelectedOptionIndex(0);
739
+ setJustSelected(true);
740
+ setLastValidTime(selectedTime);
741
+ setCurrentParsedTime(selectedTime);
742
+ const changeData = {
743
+ time: selectedTime,
744
+ isInputValid: true,
745
+ isInputEmpty: false
746
+ };
747
+ onChange?.(changeData);
748
+ lastOnChangeCallRef.current = changeData;
749
+ requestAnimationFrame(() => {
750
+ inputRef.current?.focus();
751
+ });
752
+ },
753
+ // eslint-disable-next-line react-hooks/exhaustive-deps
754
+ [onChange]
755
+ );
756
+ const handleKeyDown = useCallback(
757
+ (event) => {
758
+ if (!isDropdownOpen) {
759
+ return;
760
+ }
761
+ switch (event.key) {
762
+ case "ArrowDown":
763
+ event.preventDefault();
764
+ setSelectedOptionIndex(
765
+ (prev) => prev < filteredOptions.length - 1 ? prev + 1 : 0
766
+ );
767
+ break;
768
+ case "ArrowUp":
769
+ event.preventDefault();
770
+ setSelectedOptionIndex(
771
+ (prev) => prev > 0 ? prev - 1 : filteredOptions.length - 1
772
+ );
773
+ break;
774
+ case "Enter":
775
+ event.preventDefault();
776
+ if (filteredOptions[selectedOptionIndex]) {
777
+ handleOptionSelect(filteredOptions[selectedOptionIndex]);
778
+ }
779
+ break;
780
+ case "Tab":
781
+ tabJustPressedRef.current = true;
782
+ setIsDropdownOpen(false);
783
+ setSelectedOptionIndex(0);
784
+ setTimeout(() => {
785
+ tabJustPressedRef.current = false;
786
+ }, 50);
787
+ break;
788
+ }
789
+ },
790
+ [
791
+ isDropdownOpen,
792
+ selectedOptionIndex,
793
+ filteredOptions,
794
+ handleOptionSelect
795
+ ]
796
+ );
797
+ const popoverDisabled = disableSuggestions || !popoverSupported;
798
+ const justTheInput = /* @__PURE__ */ jsx(
799
+ MaskedTimeInput,
800
+ {
801
+ ref: combinedRef,
802
+ lastValidTime,
803
+ format,
804
+ placeholder,
805
+ min,
806
+ max,
807
+ onInputChange: handleInputChange,
808
+ onKeyDown: handleKeyDown,
809
+ onBlur: handleBlur,
810
+ onFocus: handleFocus,
811
+ description,
812
+ error: errorProp,
813
+ className: popoverDisabled ? className : void 0,
814
+ ...rest,
815
+ "data-anv": "time-field"
816
+ }
817
+ );
818
+ if (popoverDisabled) {
819
+ return justTheInput;
820
+ }
821
+ return /* @__PURE__ */ jsxs(
822
+ Popover,
823
+ {
824
+ open: isDropdownOpen,
825
+ noPadding: true,
826
+ disableCaret: true,
827
+ placement: "bottom-start",
828
+ matchReferenceWidth: true,
829
+ modal: false,
830
+ onClickOutside: () => {
831
+ setIsDropdownOpen(false);
832
+ },
833
+ onClose: () => {
834
+ setIsDropdownOpen(false);
835
+ },
836
+ fitScreen: true,
837
+ children: [
838
+ /* @__PURE__ */ jsx(Popover.Trigger, { children: ({ ref: popoverRef }) => /* @__PURE__ */ jsx(
839
+ "div",
840
+ {
841
+ ref: popoverRef,
842
+ className,
843
+ children: justTheInput
844
+ }
845
+ ) }),
846
+ /* @__PURE__ */ jsx(
847
+ Popover.Content,
848
+ {
849
+ ref: popoverContentRef,
850
+ "data-open": isDropdownOpen,
851
+ "data-disabled": disableSuggestions,
852
+ onMouseDown: () => {
853
+ isDropdownClickingRef.current = true;
854
+ },
855
+ onTouchStart: () => {
856
+ isDropdownClickingRef.current = true;
857
+ },
858
+ children: shouldShowOptions && /* @__PURE__ */ jsx(
859
+ Listbox,
860
+ {
861
+ selected: filteredOptions[selectedOptionIndex],
862
+ onSelectionChange: (selected) => {
863
+ if (selected) {
864
+ handleOptionSelect(selected);
865
+ } else {
866
+ const currentOption = filteredOptions[selectedOptionIndex];
867
+ if (currentOption) {
868
+ handleOptionSelect(currentOption);
869
+ }
870
+ }
871
+ },
872
+ "aria-label": "Time suggestions",
873
+ children: filteredOptions.map((option) => /* @__PURE__ */ jsx(Listbox.Option, { value: option, label: option, children: option }, option))
874
+ }
875
+ )
876
+ }
877
+ )
878
+ ]
879
+ }
880
+ );
881
+ }
882
+ );
883
+ const TimeField$1 = Object.assign(TimeFieldElement, {});
884
+ TimeField$1.displayName = "TimeField";
885
+
886
+ const TimeField = forwardRef(
887
+ (props, ref) => {
888
+ const data = {
889
+ label: childrenToString(props.label),
890
+ labelProps: props.labelProps,
891
+ prefix: childrenToString(props.prefix),
892
+ hint: childrenToString(props.hint),
893
+ description: childrenToString(props.description),
894
+ size: props.size,
895
+ format: props.format,
896
+ step: props.step,
897
+ min: props.min,
898
+ max: props.max,
899
+ autoround: props.autoround,
900
+ disableSuggestions: props.disableSuggestions
901
+ };
902
+ const trackingId = useTrackingId({
903
+ name: "TimeField",
904
+ data,
905
+ hasOverride: !!props["data-tracking-id"]
906
+ });
907
+ return /* @__PURE__ */ jsx(TimeField$1, { ref, "data-tracking-id": trackingId, ...props });
908
+ }
909
+ );
910
+ TimeField.displayName = TimeField$1.displayName;
911
+
912
+ export { TimeField as T };
913
+ //# sourceMappingURL=TimeField-DRHLRqN3.js.map