@refraktor/dates 0.0.4 → 0.0.5

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 (42) hide show
  1. package/build/style.css +2 -2
  2. package/package.json +33 -4
  3. package/.turbo/turbo-build.log +0 -4
  4. package/refraktor-dates-0.0.1-alpha.0.tgz +0 -0
  5. package/src/components/date-input/date-input.tsx +0 -379
  6. package/src/components/date-input/date-input.types.ts +0 -161
  7. package/src/components/date-input/index.ts +0 -13
  8. package/src/components/date-picker/date-picker.tsx +0 -649
  9. package/src/components/date-picker/date-picker.types.ts +0 -145
  10. package/src/components/date-picker/index.ts +0 -15
  11. package/src/components/dates-provider/context.ts +0 -18
  12. package/src/components/dates-provider/dates-provider.tsx +0 -136
  13. package/src/components/dates-provider/index.ts +0 -10
  14. package/src/components/dates-provider/types.ts +0 -33
  15. package/src/components/dates-provider/use-dates.ts +0 -5
  16. package/src/components/index.ts +0 -9
  17. package/src/components/month-input/index.ts +0 -13
  18. package/src/components/month-input/month-input.tsx +0 -366
  19. package/src/components/month-input/month-input.types.ts +0 -139
  20. package/src/components/month-picker/index.ts +0 -14
  21. package/src/components/month-picker/month-picker.tsx +0 -458
  22. package/src/components/month-picker/month-picker.types.ts +0 -117
  23. package/src/components/picker-shared/index.ts +0 -7
  24. package/src/components/picker-shared/picker-header.tsx +0 -178
  25. package/src/components/picker-shared/picker-header.types.ts +0 -49
  26. package/src/components/picker-shared/picker.styles.ts +0 -69
  27. package/src/components/picker-shared/picker.types.ts +0 -4
  28. package/src/components/time-input/index.ts +0 -6
  29. package/src/components/time-input/time-input.tsx +0 -48
  30. package/src/components/time-input/time-input.types.ts +0 -30
  31. package/src/components/time-picker/index.ts +0 -10
  32. package/src/components/time-picker/time-picker.tsx +0 -1088
  33. package/src/components/time-picker/time-picker.types.ts +0 -166
  34. package/src/components/year-input/index.ts +0 -13
  35. package/src/components/year-input/year-input.tsx +0 -350
  36. package/src/components/year-input/year-input.types.ts +0 -118
  37. package/src/components/year-picker/index.ts +0 -15
  38. package/src/components/year-picker/year-picker.tsx +0 -504
  39. package/src/components/year-picker/year-picker.types.ts +0 -108
  40. package/src/index.ts +0 -3
  41. package/src/style.css +0 -1
  42. package/tsconfig.json +0 -13
@@ -1,1088 +0,0 @@
1
- import { useId, useUncontrolled } from "@refraktor/utils";
2
- import {
3
- KeyboardEvent,
4
- ReactNode,
5
- Ref,
6
- useCallback,
7
- useEffect,
8
- useMemo,
9
- useRef,
10
- useState
11
- } from "react";
12
- import {
13
- createClassNamesConfig,
14
- createComponentConfig,
15
- factory,
16
- Input,
17
- useClassNames,
18
- useProps,
19
- useTheme
20
- } from "@refraktor/core";
21
- import {
22
- autoUpdate,
23
- flip,
24
- FloatingFocusManager,
25
- FloatingPortal,
26
- offset,
27
- shift,
28
- useClick,
29
- useDismiss,
30
- useFloating,
31
- useInteractions,
32
- useRole
33
- } from "@floating-ui/react";
34
- import { getPickerSizeStyles } from "../picker-shared";
35
- import {
36
- TimePickerClassNames,
37
- TimePickerFactoryPayload,
38
- TimePickerFormat,
39
- TimePickerProps,
40
- TimePickerValue
41
- } from "./time-picker.types";
42
-
43
- const HOURS_IN_DAY = 24;
44
- const MINUTES_IN_HOUR = 60;
45
- const SECONDS_IN_MINUTE = 60;
46
- const PLACEHOLDER = "--";
47
-
48
- const defaultProps = {
49
- format: "24h" as TimePickerFormat,
50
- withSeconds: false,
51
- withDropdown: false,
52
- clearable: false,
53
- disabled: false,
54
- readOnly: false,
55
- variant: "default" as const,
56
- size: "md" as const,
57
- radius: "default" as const,
58
- hoursStep: 1,
59
- minutesStep: 1,
60
- secondsStep: 1,
61
- amPmLabels: { am: "AM", pm: "PM" }
62
- } satisfies Partial<TimePickerProps>;
63
-
64
- type TimeParts = {
65
- hours: number | null;
66
- minutes: number | null;
67
- seconds: number | null;
68
- amPm: "AM" | "PM" | null;
69
- };
70
-
71
- const pad = (value: number) => String(value).padStart(2, "0");
72
-
73
- const parseValue = (
74
- value: string | undefined,
75
- withSeconds: boolean
76
- ): TimeParts => {
77
- if (!value || value === "") {
78
- return { hours: null, minutes: null, seconds: null, amPm: null };
79
- }
80
-
81
- const segments = value.trim().split(":");
82
- if (segments.length < 2) {
83
- return { hours: null, minutes: null, seconds: null, amPm: null };
84
- }
85
-
86
- const hours = Number.parseInt(segments[0], 10);
87
- const minutes = Number.parseInt(segments[1], 10);
88
- const seconds =
89
- segments.length >= 3 ? Number.parseInt(segments[2], 10) : 0;
90
-
91
- if (
92
- Number.isNaN(hours) ||
93
- Number.isNaN(minutes) ||
94
- Number.isNaN(seconds) ||
95
- hours < 0 ||
96
- hours >= HOURS_IN_DAY ||
97
- minutes < 0 ||
98
- minutes >= MINUTES_IN_HOUR ||
99
- seconds < 0 ||
100
- seconds >= SECONDS_IN_MINUTE
101
- ) {
102
- return { hours: null, minutes: null, seconds: null, amPm: null };
103
- }
104
-
105
- return {
106
- hours,
107
- minutes,
108
- seconds: withSeconds ? seconds : null,
109
- amPm: hours >= 12 ? "PM" : "AM"
110
- };
111
- };
112
-
113
- const formatValue = (parts: TimeParts, withSeconds: boolean): string => {
114
- if (parts.hours === null || parts.minutes === null) {
115
- return "";
116
- }
117
-
118
- const base = `${pad(parts.hours)}:${pad(parts.minutes)}`;
119
-
120
- if (withSeconds) {
121
- return `${base}:${pad(parts.seconds ?? 0)}`;
122
- }
123
-
124
- return base;
125
- };
126
-
127
- const to12Hour = (hours24: number): number => {
128
- if (hours24 === 0 || hours24 === 12) return 12;
129
- return hours24 % 12;
130
- };
131
-
132
- const to24Hour = (hour12: number, amPm: "AM" | "PM"): number => {
133
- if (amPm === "AM") {
134
- return hour12 === 12 ? 0 : hour12;
135
- }
136
- return hour12 === 12 ? 12 : hour12 + 12;
137
- };
138
-
139
- const clampValue = (
140
- value: number,
141
- min: number,
142
- max: number,
143
- step: number
144
- ): number => {
145
- const clamped = Math.max(min, Math.min(max, value));
146
- return Math.round(clamped / step) * step;
147
- };
148
-
149
- const parseMinMax = (
150
- timeStr: string | undefined
151
- ): { hours: number; minutes: number; seconds: number } | null => {
152
- if (!timeStr) return null;
153
- const segments = timeStr.trim().split(":");
154
- if (segments.length < 2) return null;
155
-
156
- const hours = Number.parseInt(segments[0], 10);
157
- const minutes = Number.parseInt(segments[1], 10);
158
- const seconds =
159
- segments.length >= 3 ? Number.parseInt(segments[2], 10) : 0;
160
-
161
- if (Number.isNaN(hours) || Number.isNaN(minutes) || Number.isNaN(seconds))
162
- return null;
163
-
164
- return { hours, minutes, seconds };
165
- };
166
-
167
- const setRef = <T,>(ref: Ref<T> | undefined, node: T | null) => {
168
- if (typeof ref === "function") {
169
- ref(node);
170
- } else if (ref && "current" in ref) {
171
- (ref as React.MutableRefObject<T | null>).current = node;
172
- }
173
- };
174
-
175
- const inputSizes: Record<string, string> = {
176
- xs: "h-5 px-2 text-[8px]",
177
- sm: "h-6 px-2.5 text-[10px]",
178
- md: "h-8 px-3 text-xs",
179
- lg: "h-10 px-3.5 text-sm",
180
- xl: "h-12 px-4 text-base"
181
- };
182
-
183
- const inputVariants: Record<string, string> = {
184
- default:
185
- "bg-[var(--refraktor-bg)] text-[var(--refraktor-text)] border border-[var(--refraktor-border)]",
186
- filled: "bg-[var(--refraktor-bg)] text-[var(--refraktor-text)]",
187
- outline:
188
- "bg-transparent text-[var(--refraktor-text)] border border-[var(--refraktor-border)]"
189
- };
190
-
191
- const segmentWidths: Record<string, string> = {
192
- xs: "w-[1.25rem]",
193
- sm: "w-[1.5rem]",
194
- md: "w-[1.75rem]",
195
- lg: "w-[2rem]",
196
- xl: "w-[2.5rem]"
197
- };
198
-
199
- const amPmWidths: Record<string, string> = {
200
- xs: "w-[1.75rem]",
201
- sm: "w-[2rem]",
202
- md: "w-[2.25rem]",
203
- lg: "w-[2.75rem]",
204
- xl: "w-[3.25rem]"
205
- };
206
-
207
- const separatorSizes: Record<string, string> = {
208
- xs: "text-[8px]",
209
- sm: "text-[10px]",
210
- md: "text-xs",
211
- lg: "text-sm",
212
- xl: "text-base"
213
- };
214
-
215
- const TimePicker = factory<TimePickerFactoryPayload>((_props, ref) => {
216
- const { cx, getRadius } = useTheme();
217
- const {
218
- id,
219
- value,
220
- defaultValue,
221
- onChange,
222
- format,
223
- withSeconds,
224
- withDropdown,
225
- clearable,
226
- min,
227
- max,
228
- hoursStep,
229
- minutesStep,
230
- secondsStep,
231
- amPmLabels,
232
- disabled,
233
- readOnly,
234
- variant,
235
- size,
236
- radius,
237
- label,
238
- description,
239
- error,
240
- required,
241
- withAsterisk,
242
- leftSection,
243
- rightSection,
244
- hoursRef: hoursRefProp,
245
- minutesRef: minutesRefProp,
246
- secondsRef: secondsRefProp,
247
- amPmRef: amPmRefProp,
248
- hoursInputLabel,
249
- minutesInputLabel,
250
- secondsInputLabel,
251
- amPmInputLabel,
252
- popoverProps,
253
- onFocus,
254
- onBlur,
255
- className,
256
- classNames,
257
- ...props
258
- } = useProps("TimePicker", defaultProps, _props);
259
- const classes = useClassNames("TimePicker", classNames);
260
-
261
- const _id = useId(id);
262
- const is12h = format === "12h";
263
-
264
- const hoursRef = useRef<HTMLInputElement>(null);
265
- const minutesRef = useRef<HTMLInputElement>(null);
266
- const secondsRef = useRef<HTMLInputElement>(null);
267
- const amPmRef = useRef<HTMLInputElement>(null);
268
- const wrapperRef = useRef<HTMLDivElement>(null);
269
- const blurTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
270
-
271
- const minParsed = useMemo(() => parseMinMax(min), [min]);
272
- const maxParsed = useMemo(() => parseMinMax(max), [max]);
273
-
274
- const [internalValue, setInternalValue] = useUncontrolled<string>({
275
- value,
276
- defaultValue,
277
- finalValue: "",
278
- onChange
279
- });
280
-
281
- const [parts, setParts] = useState<TimeParts>(() =>
282
- parseValue(internalValue, withSeconds)
283
- );
284
-
285
- useEffect(() => {
286
- const parsed = parseValue(internalValue, withSeconds);
287
- setParts(parsed);
288
- }, [internalValue, withSeconds]);
289
-
290
- const [dropdownOpen, setDropdownOpen] = useState(false);
291
- const isDropdownVisible = withDropdown && dropdownOpen && !disabled;
292
-
293
- const floating = useFloating({
294
- placement: popoverProps?.placement ?? "bottom-start",
295
- open: isDropdownVisible,
296
- onOpenChange: (open) => {
297
- if (!disabled && !readOnly) setDropdownOpen(open);
298
- },
299
- middleware: [
300
- offset(popoverProps?.offset ?? 4),
301
- flip(),
302
- shift()
303
- ],
304
- whileElementsMounted: autoUpdate,
305
- strategy: "fixed"
306
- });
307
-
308
- const click = useClick(floating.context, {
309
- enabled: withDropdown && !disabled && !readOnly
310
- });
311
- const dismiss = useDismiss(floating.context);
312
- const role = useRole(floating.context, { role: "dialog" });
313
- const { getReferenceProps, getFloatingProps } = useInteractions([
314
- click,
315
- dismiss,
316
- role
317
- ]);
318
-
319
- const emitValue = useCallback(
320
- (nextParts: TimeParts) => {
321
- const allFilled =
322
- nextParts.hours !== null &&
323
- nextParts.minutes !== null &&
324
- (!withSeconds || nextParts.seconds !== null) &&
325
- (!is12h || nextParts.amPm !== null);
326
-
327
- const allEmpty =
328
- nextParts.hours === null &&
329
- nextParts.minutes === null &&
330
- (nextParts.seconds === null || !withSeconds);
331
-
332
- if (allFilled) {
333
- let hours = nextParts.hours!;
334
- if (is12h && nextParts.amPm !== null) {
335
- hours = to24Hour(hours, nextParts.amPm);
336
- }
337
- const formatted = formatValue(
338
- { ...nextParts, hours },
339
- withSeconds
340
- );
341
- setInternalValue(formatted);
342
- } else if (allEmpty) {
343
- setInternalValue("");
344
- }
345
- },
346
- [is12h, setInternalValue, withSeconds]
347
- );
348
-
349
- const updateParts = useCallback(
350
- (updater: (prev: TimeParts) => TimeParts) => {
351
- setParts((prev) => {
352
- const next = updater(prev);
353
- emitValue(next);
354
- return next;
355
- });
356
- },
357
- [emitValue]
358
- );
359
-
360
- const getDisplayHours = useCallback(
361
- (hours24: number | null): string => {
362
- if (hours24 === null) return PLACEHOLDER;
363
- if (is12h) return pad(to12Hour(hours24));
364
- return pad(hours24);
365
- },
366
- [is12h]
367
- );
368
-
369
- const handleClear = useCallback(() => {
370
- if (disabled || readOnly) return;
371
- const empty: TimeParts = {
372
- hours: null,
373
- minutes: null,
374
- seconds: null,
375
- amPm: null
376
- };
377
- setParts(empty);
378
- setInternalValue("");
379
- hoursRef.current?.focus();
380
- }, [disabled, readOnly, setInternalValue]);
381
-
382
- const createSegmentKeyHandler = useCallback(
383
- (
384
- segment: "hours" | "minutes" | "seconds",
385
- prevRef: React.RefObject<HTMLInputElement | null>,
386
- nextRef: React.RefObject<HTMLInputElement | null>
387
- ) => {
388
- return (event: KeyboardEvent<HTMLInputElement>) => {
389
- if (disabled || readOnly) return;
390
-
391
- const step =
392
- segment === "hours"
393
- ? hoursStep
394
- : segment === "minutes"
395
- ? minutesStep
396
- : secondsStep;
397
- const maxVal =
398
- segment === "hours"
399
- ? is12h
400
- ? 12
401
- : HOURS_IN_DAY - 1
402
- : segment === "minutes"
403
- ? MINUTES_IN_HOUR - 1
404
- : SECONDS_IN_MINUTE - 1;
405
- const minVal = segment === "hours" && is12h ? 1 : 0;
406
-
407
- if (event.key === "ArrowUp") {
408
- event.preventDefault();
409
- updateParts((prev) => {
410
- const current = prev[segment] ?? minVal;
411
- let next = current + step;
412
- if (next > maxVal) next = minVal;
413
- return { ...prev, [segment]: next };
414
- });
415
- return;
416
- }
417
-
418
- if (event.key === "ArrowDown") {
419
- event.preventDefault();
420
- updateParts((prev) => {
421
- const current = prev[segment] ?? maxVal;
422
- let next = current - step;
423
- if (next < minVal) next = maxVal;
424
- return { ...prev, [segment]: next };
425
- });
426
- return;
427
- }
428
-
429
- if (event.key === "ArrowRight") {
430
- event.preventDefault();
431
- nextRef.current?.focus();
432
- nextRef.current?.select();
433
- return;
434
- }
435
-
436
- if (event.key === "ArrowLeft") {
437
- event.preventDefault();
438
- prevRef.current?.focus();
439
- prevRef.current?.select();
440
- return;
441
- }
442
-
443
- if (event.key === "Home") {
444
- event.preventDefault();
445
- updateParts((prev) => ({ ...prev, [segment]: minVal }));
446
- return;
447
- }
448
-
449
- if (event.key === "End") {
450
- event.preventDefault();
451
- updateParts((prev) => ({ ...prev, [segment]: maxVal }));
452
- return;
453
- }
454
-
455
- if (event.key === "Backspace") {
456
- event.preventDefault();
457
- updateParts((prev) => {
458
- if (prev[segment] === null) {
459
- prevRef.current?.focus();
460
- prevRef.current?.select();
461
- return prev;
462
- }
463
- return { ...prev, [segment]: null };
464
- });
465
- return;
466
- }
467
-
468
- if (event.key === "Tab") {
469
- return;
470
- }
471
-
472
- if (/^\d$/.test(event.key)) {
473
- event.preventDefault();
474
- const digit = Number.parseInt(event.key, 10);
475
- updateParts((prev) => {
476
- const current = prev[segment];
477
- let next: number;
478
-
479
- if (current === null || current >= 10) {
480
- next = digit;
481
- } else {
482
- next = current * 10 + digit;
483
- }
484
-
485
- if (next > maxVal) {
486
- next = digit;
487
- }
488
-
489
- const result = { ...prev, [segment]: next };
490
-
491
- if (next >= (maxVal + 1) / 10 || (current !== null && current < 10)) {
492
- requestAnimationFrame(() => {
493
- nextRef.current?.focus();
494
- nextRef.current?.select();
495
- });
496
- }
497
-
498
- return result;
499
- });
500
- return;
501
- }
502
-
503
- if (!event.ctrlKey && !event.metaKey && !event.altKey) {
504
- event.preventDefault();
505
- }
506
- };
507
- },
508
- [disabled, hoursStep, is12h, minutesStep, readOnly, secondsStep, updateParts]
509
- );
510
-
511
- const handleAmPmKeyDown = useCallback(
512
- (event: KeyboardEvent<HTMLInputElement>) => {
513
- if (disabled || readOnly) return;
514
-
515
- if (event.key === "ArrowUp" || event.key === "ArrowDown") {
516
- event.preventDefault();
517
- updateParts((prev) => ({
518
- ...prev,
519
- amPm: prev.amPm === "AM" ? "PM" : "AM"
520
- }));
521
- return;
522
- }
523
-
524
- if (event.key === "ArrowLeft") {
525
- event.preventDefault();
526
- const prev = withSeconds ? secondsRef : minutesRef;
527
- prev.current?.focus();
528
- prev.current?.select();
529
- return;
530
- }
531
-
532
- if (
533
- event.key.toLowerCase() === "a" ||
534
- event.key.toLowerCase() === "p"
535
- ) {
536
- event.preventDefault();
537
- const nextAmPm = event.key.toLowerCase() === "a" ? "AM" : "PM";
538
- updateParts((prev) => ({ ...prev, amPm: nextAmPm }));
539
- return;
540
- }
541
-
542
- if (event.key === "Backspace") {
543
- event.preventDefault();
544
- updateParts((prev) => {
545
- if (prev.amPm === null) {
546
- const prev2 = withSeconds ? secondsRef : minutesRef;
547
- prev2.current?.focus();
548
- prev2.current?.select();
549
- return prev;
550
- }
551
- return { ...prev, amPm: null };
552
- });
553
- return;
554
- }
555
-
556
- if (event.key === "Tab") {
557
- return;
558
- }
559
-
560
- if (!event.ctrlKey && !event.metaKey && !event.altKey) {
561
- event.preventDefault();
562
- }
563
- },
564
- [disabled, readOnly, updateParts, withSeconds]
565
- );
566
-
567
- const handleWrapperFocus = useCallback(
568
- (event: React.FocusEvent<HTMLDivElement>) => {
569
- if (blurTimeoutRef.current !== null) {
570
- clearTimeout(blurTimeoutRef.current);
571
- blurTimeoutRef.current = null;
572
- }
573
-
574
- if (!wrapperRef.current?.contains(event.relatedTarget as Node)) {
575
- onFocus?.(event);
576
- if (withDropdown && !disabled && !readOnly) {
577
- setDropdownOpen(true);
578
- }
579
- }
580
- },
581
- [disabled, onFocus, readOnly, withDropdown]
582
- );
583
-
584
- const handleWrapperBlur = useCallback(
585
- (event: React.FocusEvent<HTMLDivElement>) => {
586
- blurTimeoutRef.current = setTimeout(() => {
587
- if (
588
- !wrapperRef.current?.contains(document.activeElement) &&
589
- !floating.refs.floating.current?.contains(
590
- document.activeElement
591
- )
592
- ) {
593
- onBlur?.(event);
594
- setDropdownOpen(false);
595
- }
596
- }, 0);
597
- },
598
- [floating.refs.floating, onBlur]
599
- );
600
-
601
- const displayHours = is12h
602
- ? parts.hours !== null
603
- ? pad(to12Hour(parts.hours))
604
- : PLACEHOLDER
605
- : parts.hours !== null
606
- ? pad(parts.hours)
607
- : PLACEHOLDER;
608
- const displayMinutes =
609
- parts.minutes !== null ? pad(parts.minutes) : PLACEHOLDER;
610
- const displaySeconds =
611
- parts.seconds !== null ? pad(parts.seconds) : PLACEHOLDER;
612
- const displayAmPm = parts.amPm ?? PLACEHOLDER;
613
-
614
- const hasValue =
615
- parts.hours !== null ||
616
- parts.minutes !== null ||
617
- (withSeconds && parts.seconds !== null);
618
-
619
- const showClearButton = clearable && hasValue && !disabled && !readOnly;
620
-
621
- const effectiveRightSection = showClearButton ? (
622
- <button
623
- type="button"
624
- tabIndex={-1}
625
- aria-label="Clear time"
626
- className={cx(
627
- "inline-flex items-center justify-center text-[var(--refraktor-text-secondary)] hover:text-[var(--refraktor-text)] transition-colors cursor-pointer"
628
- )}
629
- onClick={handleClear}
630
- onMouseDown={(e) => e.preventDefault()}
631
- >
632
- <svg
633
- xmlns="http://www.w3.org/2000/svg"
634
- viewBox="0 0 16 16"
635
- fill="currentColor"
636
- className="size-3.5"
637
- >
638
- <path d="M3.72 3.72a.75.75 0 0 1 1.06 0L8 6.94l3.22-3.22a.75.75 0 1 1 1.06 1.06L9.06 8l3.22 3.22a.75.75 0 1 1-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 0 1-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 0 1 0-1.06Z" />
639
- </svg>
640
- </button>
641
- ) : (
642
- rightSection
643
- );
644
-
645
- const sizeClass = inputSizes[size] ?? inputSizes.md;
646
- const variantClass = inputVariants[variant] ?? inputVariants.default;
647
- const segmentWidth = segmentWidths[size] ?? segmentWidths.md;
648
- const amPmWidth = amPmWidths[size] ?? amPmWidths.md;
649
- const sepSize = separatorSizes[size] ?? separatorSizes.md;
650
-
651
- const hoursKeyHandler = createSegmentKeyHandler(
652
- "hours",
653
- { current: null },
654
- minutesRef
655
- );
656
- const minutesKeyHandler = createSegmentKeyHandler(
657
- "minutes",
658
- hoursRef,
659
- withSeconds ? secondsRef : is12h ? amPmRef : { current: null }
660
- );
661
- const secondsKeyHandler = createSegmentKeyHandler(
662
- "seconds",
663
- minutesRef,
664
- is12h ? amPmRef : { current: null }
665
- );
666
-
667
- const renderDropdown = () => {
668
- if (!withDropdown) return null;
669
-
670
- const sizeStyles = getPickerSizeStyles(size);
671
- const hoursCount = is12h ? 12 : HOURS_IN_DAY;
672
- const hoursStart = is12h ? 1 : 0;
673
-
674
- const hourOptions: number[] = [];
675
- for (let i = hoursStart; i < hoursStart + hoursCount; i += hoursStep) {
676
- hourOptions.push(i);
677
- }
678
-
679
- const minuteOptions: number[] = [];
680
- for (let i = 0; i < MINUTES_IN_HOUR; i += minutesStep) {
681
- minuteOptions.push(i);
682
- }
683
-
684
- const secondOptions: number[] = [];
685
- for (let i = 0; i < SECONDS_IN_MINUTE; i += secondsStep) {
686
- secondOptions.push(i);
687
- }
688
-
689
- const currentHourDisplay = is12h
690
- ? parts.hours !== null
691
- ? to12Hour(parts.hours)
692
- : null
693
- : parts.hours;
694
-
695
- const renderColumn = (
696
- columnLabel: string,
697
- options: (number | string)[],
698
- selectedValue: number | string | null,
699
- onSelect: (value: number | string) => void,
700
- extraClassName?: string
701
- ) => (
702
- <div
703
- className={cx(
704
- "flex flex-col min-w-0",
705
- classes.dropdownColumn,
706
- extraClassName
707
- )}
708
- >
709
- <div
710
- className={cx(
711
- "py-1.5 text-center font-medium text-[var(--refraktor-text-secondary)] border-b border-[var(--refraktor-border)] bg-[var(--refraktor-bg-subtle)]",
712
- sizeStyles.label,
713
- classes.dropdownColumnLabel
714
- )}
715
- >
716
- {columnLabel}
717
- </div>
718
- <div
719
- className={cx(
720
- "refraktor-scrollbar flex max-h-52 flex-col gap-0.5 p-1 overflow-y-auto"
721
- )}
722
- >
723
- {options.map((opt) => {
724
- const isSelected = opt === selectedValue;
725
- const displayLabel =
726
- typeof opt === "number" ? pad(opt) : opt;
727
-
728
- return (
729
- <button
730
- key={String(opt)}
731
- type="button"
732
- tabIndex={-1}
733
- className={cx(
734
- "inline-flex w-full items-center justify-center font-medium transition-colors",
735
- isSelected
736
- ? "bg-[var(--refraktor-primary)] text-[var(--refraktor-primary-text)]"
737
- : "hover:bg-[var(--refraktor-bg-hover)] text-[var(--refraktor-text)]",
738
- sizeStyles.cell,
739
- getRadius(radius),
740
- classes.dropdownOption,
741
- isSelected && classes.dropdownOptionActive
742
- )}
743
- onMouseDown={(e) => e.preventDefault()}
744
- onClick={() => onSelect(opt)}
745
- >
746
- {displayLabel}
747
- </button>
748
- );
749
- })}
750
- </div>
751
- </div>
752
- );
753
-
754
- const handleDropdownHourSelect = (val: number | string) => {
755
- const hourVal = typeof val === "number" ? val : Number(val);
756
- updateParts((prev) => {
757
- const next = { ...prev, hours: is12h ? to24Hour(hourVal, prev.amPm ?? "AM") : hourVal };
758
- if (next.minutes === null) {
759
- requestAnimationFrame(() => {
760
- minutesRef.current?.focus();
761
- minutesRef.current?.select();
762
- });
763
- }
764
- return next;
765
- });
766
- };
767
-
768
- const handleDropdownMinuteSelect = (val: number | string) => {
769
- const minVal = typeof val === "number" ? val : Number(val);
770
- updateParts((prev) => {
771
- const next = { ...prev, minutes: minVal };
772
- if (withSeconds && next.seconds === null) {
773
- requestAnimationFrame(() => {
774
- secondsRef.current?.focus();
775
- secondsRef.current?.select();
776
- });
777
- }
778
- return next;
779
- });
780
- };
781
-
782
- const handleDropdownSecondSelect = (val: number | string) => {
783
- const secVal = typeof val === "number" ? val : Number(val);
784
- updateParts((prev) => ({ ...prev, seconds: secVal }));
785
- };
786
-
787
- const handleDropdownAmPmSelect = (val: number | string) => {
788
- const amPmVal = val as "AM" | "PM";
789
- updateParts((prev) => {
790
- if (prev.hours === null) return { ...prev, amPm: amPmVal };
791
- const hour12 = to12Hour(prev.hours);
792
- const newHours24 = to24Hour(hour12, amPmVal);
793
- return { ...prev, hours: newHours24, amPm: amPmVal };
794
- });
795
- };
796
-
797
- const columnCount =
798
- (withSeconds ? 4 : 3) + (is12h ? 1 : 0) - (withSeconds ? 0 : 1);
799
- const gridColsClass =
800
- columnCount === 2
801
- ? "grid-cols-2"
802
- : columnCount === 3
803
- ? "grid-cols-3"
804
- : "grid-cols-4";
805
-
806
- return (
807
- <FloatingPortal>
808
- {isDropdownVisible && (
809
- <FloatingFocusManager
810
- context={floating.context}
811
- modal={false}
812
- initialFocus={-1}
813
- returnFocus={false}
814
- >
815
- <div
816
- ref={floating.refs.setFloating}
817
- style={{
818
- ...floating.floatingStyles,
819
- zIndex: 1000
820
- }}
821
- className={cx(
822
- "border border-[var(--refraktor-border)] bg-[var(--refraktor-bg)] shadow-md overflow-hidden",
823
- getRadius(radius),
824
- classes.dropdown
825
- )}
826
- {...getFloatingProps()}
827
- >
828
- <div
829
- className={cx(
830
- "grid divide-x divide-[var(--refraktor-border)]",
831
- gridColsClass
832
- )}
833
- >
834
- {renderColumn(
835
- "Hour",
836
- hourOptions,
837
- currentHourDisplay,
838
- handleDropdownHourSelect
839
- )}
840
- {renderColumn(
841
- "Min",
842
- minuteOptions,
843
- parts.minutes,
844
- handleDropdownMinuteSelect
845
- )}
846
- {withSeconds &&
847
- renderColumn(
848
- "Sec",
849
- secondOptions,
850
- parts.seconds,
851
- handleDropdownSecondSelect
852
- )}
853
- {is12h &&
854
- renderColumn(
855
- amPmLabels.am + "/" + amPmLabels.pm,
856
- [amPmLabels.am, amPmLabels.pm],
857
- parts.amPm ===
858
- "AM"
859
- ? amPmLabels.am
860
- : parts.amPm === "PM"
861
- ? amPmLabels.pm
862
- : null,
863
- (val) => {
864
- const normalized =
865
- val === amPmLabels.am
866
- ? "AM"
867
- : "PM";
868
- handleDropdownAmPmSelect(
869
- normalized
870
- );
871
- }
872
- )}
873
- </div>
874
- </div>
875
- </FloatingFocusManager>
876
- )}
877
- </FloatingPortal>
878
- );
879
- };
880
-
881
- const hasWrapper = label || description || error;
882
- const inputContent = (
883
- <div
884
- ref={(node) => {
885
- if (wrapperRef) (wrapperRef as React.MutableRefObject<HTMLDivElement | null>).current = node;
886
- floating.refs.setReference(node);
887
- }}
888
- className={cx(
889
- "relative w-full inline-flex items-center transition-all",
890
- sizeClass,
891
- variantClass,
892
- getRadius(radius),
893
- "focus-within:border-[var(--refraktor-primary)]",
894
- error && typeof error !== "boolean" && "border-[var(--refraktor-colors-red-6)]",
895
- disabled && "opacity-50 cursor-not-allowed",
896
- classes.fieldsWrapper,
897
- !hasWrapper && className
898
- )}
899
- onFocus={handleWrapperFocus}
900
- onBlur={handleWrapperBlur}
901
- {...(!hasWrapper ? getReferenceProps(props) : getReferenceProps())}
902
- >
903
- {leftSection && (
904
- <div className="flex h-full items-center justify-center text-[var(--refraktor-text-secondary)] shrink-0 select-none">
905
- {leftSection}
906
- </div>
907
- )}
908
-
909
- <div
910
- className={cx(
911
- "flex items-center flex-1 min-w-0 gap-0.5"
912
- )}
913
- >
914
- <input
915
- ref={(node) => {
916
- hoursRef.current = node;
917
- setRef(hoursRefProp, node);
918
- }}
919
- id={`${_id}-hours`}
920
- type="text"
921
- inputMode="numeric"
922
- autoComplete="off"
923
- placeholder={PLACEHOLDER}
924
- value={
925
- parts.hours !== null ? (is12h ? pad(to12Hour(parts.hours)) : pad(parts.hours)) : ""
926
- }
927
- aria-label={hoursInputLabel ?? "Hours"}
928
- readOnly
929
- tabIndex={disabled ? -1 : 0}
930
- disabled={disabled}
931
- className={cx(
932
- "bg-transparent border-none outline-none text-center text-[var(--refraktor-text)] placeholder:text-[var(--refraktor-text-tertiary)] cursor-default select-all p-0",
933
- segmentWidth,
934
- classes.field
935
- )}
936
- onKeyDown={hoursKeyHandler}
937
- onFocus={(e) => e.target.select()}
938
- />
939
-
940
- <span
941
- className={cx(
942
- "text-[var(--refraktor-text-secondary)] select-none leading-none",
943
- sepSize,
944
- classes.separator
945
- )}
946
- >
947
- :
948
- </span>
949
-
950
- <input
951
- ref={(node) => {
952
- minutesRef.current = node;
953
- setRef(minutesRefProp, node);
954
- }}
955
- id={`${_id}-minutes`}
956
- type="text"
957
- inputMode="numeric"
958
- autoComplete="off"
959
- placeholder={PLACEHOLDER}
960
- value={
961
- parts.minutes !== null ? pad(parts.minutes) : ""
962
- }
963
- aria-label={minutesInputLabel ?? "Minutes"}
964
- readOnly
965
- tabIndex={disabled ? -1 : 0}
966
- disabled={disabled}
967
- className={cx(
968
- "bg-transparent border-none outline-none text-center text-[var(--refraktor-text)] placeholder:text-[var(--refraktor-text-tertiary)] cursor-default select-all p-0",
969
- segmentWidth,
970
- classes.field
971
- )}
972
- onKeyDown={minutesKeyHandler}
973
- onFocus={(e) => e.target.select()}
974
- />
975
-
976
- {withSeconds && (
977
- <>
978
- <span
979
- className={cx(
980
- "text-[var(--refraktor-text-secondary)] select-none leading-none",
981
- sepSize,
982
- classes.separator
983
- )}
984
- >
985
- :
986
- </span>
987
- <input
988
- ref={(node) => {
989
- secondsRef.current = node;
990
- setRef(secondsRefProp, node);
991
- }}
992
- id={`${_id}-seconds`}
993
- type="text"
994
- inputMode="numeric"
995
- autoComplete="off"
996
- placeholder={PLACEHOLDER}
997
- value={
998
- parts.seconds !== null
999
- ? pad(parts.seconds)
1000
- : ""
1001
- }
1002
- aria-label={secondsInputLabel ?? "Seconds"}
1003
- readOnly
1004
- tabIndex={disabled ? -1 : 0}
1005
- disabled={disabled}
1006
- className={cx(
1007
- "bg-transparent border-none outline-none text-center text-[var(--refraktor-text)] placeholder:text-[var(--refraktor-text-tertiary)] cursor-default select-all p-0",
1008
- segmentWidth,
1009
- classes.field
1010
- )}
1011
- onKeyDown={secondsKeyHandler}
1012
- onFocus={(e) => e.target.select()}
1013
- />
1014
- </>
1015
- )}
1016
-
1017
- {is12h && (
1018
- <input
1019
- ref={(node) => {
1020
- amPmRef.current = node;
1021
- setRef(amPmRefProp, node);
1022
- }}
1023
- id={`${_id}-ampm`}
1024
- type="text"
1025
- autoComplete="off"
1026
- placeholder={PLACEHOLDER}
1027
- value={
1028
- parts.amPm !== null
1029
- ? parts.amPm === "AM"
1030
- ? amPmLabels.am
1031
- : amPmLabels.pm
1032
- : ""
1033
- }
1034
- aria-label={amPmInputLabel ?? "AM/PM"}
1035
- readOnly
1036
- tabIndex={disabled ? -1 : 0}
1037
- disabled={disabled}
1038
- className={cx(
1039
- "bg-transparent border-none outline-none text-center text-[var(--refraktor-text)] placeholder:text-[var(--refraktor-text-tertiary)] cursor-default select-all p-0 ml-1",
1040
- amPmWidth,
1041
- classes.amPmInput
1042
- )}
1043
- onKeyDown={handleAmPmKeyDown}
1044
- onFocus={(e) => e.target.select()}
1045
- />
1046
- )}
1047
- </div>
1048
-
1049
- {effectiveRightSection && (
1050
- <div className="flex h-full items-center justify-center text-[var(--refraktor-text-secondary)] shrink-0 select-none">
1051
- {effectiveRightSection}
1052
- </div>
1053
- )}
1054
- </div>
1055
- );
1056
-
1057
- const content = hasWrapper ? (
1058
- <Input.Wrapper
1059
- ref={ref}
1060
- label={label}
1061
- description={description}
1062
- error={error}
1063
- required={required}
1064
- withAsterisk={withAsterisk}
1065
- inputId={`${_id}-hours`}
1066
- className={cx(classes.root, className)}
1067
- >
1068
- {inputContent}
1069
- {renderDropdown()}
1070
- </Input.Wrapper>
1071
- ) : (
1072
- <div
1073
- ref={ref}
1074
- className={cx(classes.root, className)}
1075
- >
1076
- {inputContent}
1077
- {renderDropdown()}
1078
- </div>
1079
- );
1080
-
1081
- return content;
1082
- });
1083
-
1084
- TimePicker.displayName = "@refraktor/dates/TimePicker";
1085
- TimePicker.configure = createComponentConfig<TimePickerProps>();
1086
- TimePicker.classNames = createClassNamesConfig<TimePickerClassNames>();
1087
-
1088
- export default TimePicker;