aplance-ui 1.3.0 → 1.5.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 (44) hide show
  1. package/package.json +1 -1
  2. package/registry/components/primitives/icon/icon.tsx +4 -6
  3. package/registry/components/primitives/modal/modal.module.css +5 -0
  4. package/registry/components/primitives/modal/modal.tsx +32 -34
  5. package/registry/components/primitives/popover/popover-context.ts +33 -29
  6. package/registry/components/primitives/popover/popover.module.css +4 -2
  7. package/registry/components/primitives/popover/popover.tsx +150 -37
  8. package/registry/components/ui/accordion/accordion.module.css +52 -0
  9. package/registry/components/ui/accordion/accordion.tsx +97 -25
  10. package/registry/components/ui/avatar/avatar.tsx +2 -2
  11. package/registry/components/ui/breadcrumbs/breadcrumbs.module.css +9 -9
  12. package/registry/components/ui/button/button.module.css +43 -5
  13. package/registry/components/ui/button/button.tsx +2 -1
  14. package/registry/components/ui/copy-button/copy-button.module.css +2 -0
  15. package/registry/components/ui/copy-button/copy-button.tsx +5 -0
  16. package/registry/components/ui/data-table/data-table.module.css +786 -710
  17. package/registry/components/ui/data-table/data-table.tsx +1238 -1211
  18. package/registry/components/ui/data-table/filter-modal.tsx +86 -0
  19. package/registry/components/ui/data-table/filters.tsx +21 -0
  20. package/registry/components/ui/data-table/pagination-bar.tsx +31 -17
  21. package/registry/components/ui/data-table/types.ts +19 -1
  22. package/registry/components/ui/data-table/useDataTable.tsx +2 -2
  23. package/registry/components/ui/data-table/utils.ts +30 -4
  24. package/registry/components/ui/input-fields/common/input-field-common.module.css +8 -2
  25. package/registry/components/ui/input-fields/common/picker-popover-common.module.css +4 -0
  26. package/registry/components/ui/input-fields/custom-select/custom-select.module.css +118 -76
  27. package/registry/components/ui/input-fields/custom-select/custom-select.tsx +552 -484
  28. package/registry/components/ui/input-fields/date-picker/date-picker.tsx +936 -933
  29. package/registry/components/ui/input-fields/date-picker/types.ts +2 -0
  30. package/registry/components/ui/input-fields/emoji-picker/emoji-picker.tsx +8 -0
  31. package/registry/components/ui/input-fields/input/input.tsx +348 -348
  32. package/registry/components/ui/input-fields/media-input/media-input.tsx +3 -1
  33. package/registry/components/ui/input-fields/media-input/types.ts +8 -2
  34. package/registry/components/ui/input-fields/media-input/useMediaUpload.tsx +5 -5
  35. package/registry/components/ui/input-fields/media-input/utils.ts +18 -5
  36. package/registry/components/ui/scroll-rail/scroll-rail.module.css +104 -0
  37. package/registry/components/ui/scroll-rail/scroll-rail.tsx +159 -0
  38. package/registry/components/ui/tabs/tabs.module.css +8 -12
  39. package/registry/components/ui/tabs/tabs.tsx +94 -50
  40. package/registry/components/ui/tooltip/tooltip.module.css +8 -3
  41. package/registry/components/ui/tooltip/tooltip.tsx +16 -3
  42. package/registry/hooks/useMediaQuery.ts +38 -0
  43. package/registry/lib/string.ts +3 -3
  44. package/registry/registry.json +54 -4
@@ -1,945 +1,948 @@
1
- import {
2
- useCallback,
3
- useEffect,
4
- useId,
5
- useMemo,
6
- useState,
7
- type ChangeEvent,
8
- type KeyboardEvent,
9
- } from "react";
10
- import clsx from "clsx";
11
- import type { DateRange as RDPDateRange, Matcher } from "react-day-picker";
12
- import { IconCalendar } from "@tabler/icons-react";
13
- import {
14
- Popover,
15
- PopoverMenu,
16
- PopoverTrigger,
17
- } from "../../../primitives/popover/popover";
18
- import { Icon } from "../../../primitives/icon/icon";
19
- import { Input } from "../input/input";
20
- import { Button } from "../../button/button";
21
- import { FieldMessage } from "../common/field-message/field-message";
22
- import sCommon from "../common/input-field-common.module.css";
23
- import sPickerCommon from "../common/picker-popover-common.module.css";
24
- import { SelectButton } from "../common/select-button/select-button";
25
- import { TimePicker } from "../time-picker/time-picker";
26
- import { Calendar } from "./calendar";
27
- import type {
28
- DatePickerProps,
29
- DateRange,
30
- DatePickerMode,
31
- RangeDatePickerProps,
32
- SingleDatePickerProps,
33
- } from "./types";
34
- import {
35
- addMonthOffset,
36
- autoFormatDateText,
37
- coerceToDate,
38
- coerceToRange,
39
- defaultFormatDate,
40
- formatTriggerDisplay,
41
- hasRangeValue,
42
- isRangeValue,
43
- isSameMonth,
44
- mergeDateWithTimeOf,
45
- parseDateTextString,
46
- startOfDay,
47
- toDateInputString,
48
- toMonthStart,
49
- toTimeString,
50
- withTimeString,
51
- type InternalValue,
52
- } from "./utils";
53
- import s from "./date-picker.module.css";
54
-
55
- export { Calendar } from "./calendar";
56
- export type { CalendarProps } from "./calendar";
57
- export * from "./types";
58
-
1
+ import {
2
+ useCallback,
3
+ useId,
4
+ useMemo,
5
+ useState,
6
+ type ChangeEvent,
7
+ type KeyboardEvent,
8
+ } from "react";
9
+ import clsx from "clsx";
10
+ import type { DateRange as RDPDateRange, Matcher } from "react-day-picker";
11
+ import { IconCalendar } from "@tabler/icons-react";
12
+ import {
13
+ Popover,
14
+ PopoverMenu,
15
+ PopoverTrigger,
16
+ } from "../../../primitives/popover/popover";
17
+ import { Icon } from "../../../primitives/icon/icon";
18
+ import { Input } from "../input/input";
19
+ import { Button } from "../../button/button";
20
+ import { FieldMessage } from "../common/field-message/field-message";
21
+ import sCommon from "../common/input-field-common.module.css";
22
+ import sPickerCommon from "../common/picker-popover-common.module.css";
23
+ import { SelectButton } from "../common/select-button/select-button";
24
+ import { TimePicker } from "../time-picker/time-picker";
25
+ import { Calendar } from "./calendar";
26
+ import type {
27
+ DatePickerProps,
28
+ DateRange,
29
+ DatePickerMode,
30
+ RangeDatePickerProps,
31
+ SingleDatePickerProps,
32
+ } from "./types";
33
+ import {
34
+ addMonthOffset,
35
+ autoFormatDateText,
36
+ coerceToDate,
37
+ coerceToRange,
38
+ defaultFormatDate,
39
+ formatTriggerDisplay,
40
+ hasRangeValue,
41
+ isRangeValue,
42
+ isSameMonth,
43
+ mergeDateWithTimeOf,
44
+ parseDateTextString,
45
+ startOfDay,
46
+ toDateInputString,
47
+ toMonthStart,
48
+ toTimeString,
49
+ withTimeString,
50
+ type InternalValue,
51
+ } from "./utils";
52
+ import s from "./date-picker.module.css";
53
+
54
+ export { Calendar } from "./calendar";
55
+ export type { CalendarProps } from "./calendar";
56
+ export * from "./types";
57
+
59
58
  const DEFAULT_START_PLACEHOLDER = "Start date";
60
59
  const DEFAULT_END_PLACEHOLDER = "End date";
61
60
 
62
61
  export const DatePicker = (props: DatePickerProps) => {
63
- const {
64
- label,
65
- className,
66
- style,
67
- id,
68
- info,
69
- errors,
70
- layout = "default",
71
- layoutDirection = "default",
72
-
73
- placeholder,
74
- startPlaceholder = DEFAULT_START_PLACEHOLDER,
75
- endPlaceholder = DEFAULT_END_PLACEHOLDER,
76
-
77
- minDate,
78
- maxDate,
79
- disabledDates,
80
-
81
- disabled = false,
82
- readOnly = false,
83
- required = false,
84
- closeOnSelection,
85
- showManualInput = true,
86
-
87
- showTime = false,
88
- timeStep,
89
- timeFormat = "12h",
90
- timePickerPopover,
91
- showActionFooter = true,
92
-
93
- showClear = true,
94
- onClear,
95
- onBlur,
96
-
97
- numberOfMonths,
98
- weekStartsOn = 1,
99
- captionLayout = "dropdown",
100
- showWeekNumber = false,
101
-
102
- formatDisplay,
103
- formatDate = defaultFormatDate,
104
-
105
- popover,
106
- popoverTrigger,
107
- popoverMenu,
108
- selectButton,
109
- calendar,
110
- } = props;
111
-
112
- const mode: DatePickerMode = props.mode ?? "single";
113
- const isRange = mode === "range";
114
-
115
- const generatedId = useId();
116
- const inputId = id ?? generatedId;
117
-
118
- const isControlled = "value" in props;
119
-
120
- const anchorDateOnly = !showTime;
121
-
122
- const [internalValue, setInternalValue] = useState<InternalValue>(() => {
123
- if (isRange) {
124
- return coerceToRange(
125
- (props as RangeDatePickerProps).defaultValue,
126
- anchorDateOnly,
127
- );
128
- }
129
- return coerceToDate(
130
- (props as SingleDatePickerProps).defaultValue,
131
- anchorDateOnly,
132
- );
133
- });
134
-
135
- const currentValue: InternalValue = isControlled
136
- ? isRange
137
- ? coerceToRange(
138
- (props as RangeDatePickerProps).value,
139
- anchorDateOnly,
140
- )
141
- : coerceToDate(
142
- (props as SingleDatePickerProps).value,
143
- anchorDateOnly,
144
- )
145
- : internalValue;
146
-
147
- const selectedSingle =
148
- !isRange && currentValue instanceof Date ? currentValue : undefined;
149
- const selectedRange =
150
- isRange && isRangeValue(currentValue)
151
- ? (currentValue as DateRange)
152
- : undefined;
153
-
154
- const [isOpen, setIsOpen] = useState(false);
155
- const [singleManualText, setSingleManualText] = useState("");
156
- const [rangeManualText, setRangeManualText] = useState<{
157
- from: string;
158
- to: string;
159
- }>({ from: "", to: "" });
160
-
161
- const resolvedRangeMonthCount = isRange
162
- ? Math.max(1, numberOfMonths ?? calendar?.numberOfMonths ?? 2)
163
- : 1;
164
-
165
- // Controlled month state for live calendar navigation from manual input
166
- const [calendarMonth, setCalendarMonth] = useState<Date | undefined>(() => {
167
- if (isRange) {
168
- const seed = selectedRange?.from ?? selectedRange?.to;
169
- return seed ? toMonthStart(seed) : undefined;
170
- }
171
- return selectedSingle ? toMonthStart(selectedSingle) : undefined;
172
- });
173
-
174
- const [rangeCalendarMonths, setRangeCalendarMonths] = useState<Date[]>(() => {
175
- const fromMonth = selectedRange?.from
176
- ? toMonthStart(selectedRange.from)
177
- : undefined;
178
- const toMonth = selectedRange?.to
179
- ? toMonthStart(selectedRange.to)
180
- : undefined;
181
-
182
- if (fromMonth && toMonth && resolvedRangeMonthCount > 1) {
183
- return [
184
- fromMonth,
185
- ...Array.from({ length: resolvedRangeMonthCount - 2 }, (_, index) =>
186
- addMonthOffset(fromMonth, index + 1),
187
- ),
188
- toMonth,
189
- ];
190
- }
191
-
192
- const seed = fromMonth ?? toMonth ?? toMonthStart(new Date());
193
- return Array.from({ length: resolvedRangeMonthCount }, (_, index) =>
194
- addMonthOffset(seed, index),
195
- );
196
- });
197
-
198
- const hasValue = isRange
199
- ? hasRangeValue(currentValue as DateRange | undefined)
200
- : Boolean(currentValue);
201
-
202
- const shouldCloseOnSelection = closeOnSelection ?? false;
203
-
204
- const minDay = useMemo(
205
- () => (minDate ? startOfDay(minDate) : undefined),
206
- [minDate],
207
- );
208
- const maxDay = useMemo(
209
- () => (maxDate ? startOfDay(maxDate) : undefined),
210
- [maxDate],
211
- );
212
-
213
- const isWithinBounds = useCallback(
214
- (date: Date) => {
215
- const day = startOfDay(date);
216
- if (minDay && day < minDay) return false;
217
- if (maxDay && day > maxDay) return false;
218
- return true;
219
- },
220
- [maxDay, minDay],
221
- );
222
-
223
- const resolvedPlaceholder =
224
- placeholder ?? (isRange ? "Select date range..." : "Select date...");
225
-
226
- const selectedSingleText = selectedSingle
227
- ? toDateInputString(selectedSingle)
228
- : "";
229
- const selectedRangeFromText = selectedRange?.from
230
- ? toDateInputString(selectedRange.from)
231
- : "";
232
- const selectedRangeToText = selectedRange?.to
233
- ? toDateInputString(selectedRange.to)
234
- : "";
235
-
236
- useEffect(() => {
237
- if (!isRange || resolvedRangeMonthCount <= 1) return;
238
-
239
- setRangeCalendarMonths((prev) => {
240
- if (prev.length === resolvedRangeMonthCount) return prev;
241
-
242
- const seed = prev[0] ??
243
- toMonthStart(selectedRange?.from ?? selectedRange?.to ?? new Date());
244
-
245
- return Array.from({ length: resolvedRangeMonthCount }, (_, index) =>
246
- prev[index] ?? addMonthOffset(seed, index),
247
- );
248
- });
249
- }, [
250
- isRange,
251
- resolvedRangeMonthCount,
252
- selectedRange?.from,
253
- selectedRange?.to,
254
- ]);
255
-
256
- useEffect(() => {
257
- if (isRange) {
258
- setRangeManualText({
259
- from: selectedRangeFromText,
260
- to: selectedRangeToText,
261
- });
262
- return;
263
- }
264
-
265
- setSingleManualText(selectedSingleText);
266
- }, [isRange, selectedRangeFromText, selectedRangeToText, selectedSingleText]);
267
-
268
- // Sync visible calendar months when popover opens or value changes externally
269
- useEffect(() => {
270
- if (!isOpen) return;
271
-
272
- if (isRange) {
273
- const fromMonth = selectedRange?.from
274
- ? toMonthStart(selectedRange.from)
275
- : undefined;
276
- const toMonth = selectedRange?.to
277
- ? toMonthStart(selectedRange.to)
278
- : undefined;
279
-
280
- if (resolvedRangeMonthCount > 1) {
281
- setRangeCalendarMonths((prev) => {
282
- const seed = fromMonth ?? toMonth ?? prev[0] ?? toMonthStart(new Date());
283
- const next = Array.from(
284
- { length: resolvedRangeMonthCount },
285
- (_, index) => prev[index] ?? addMonthOffset(seed, index),
286
- );
287
-
288
- if (fromMonth) next[0] = fromMonth;
289
- if (toMonth) next[resolvedRangeMonthCount - 1] = toMonth;
290
-
291
- const unchanged =
292
- prev.length === next.length &&
293
- prev.every((month, index) => isSameMonth(month, next[index]));
294
-
295
- return unchanged ? prev : next;
296
- });
297
- }
298
-
299
- const target = selectedRange?.from ?? selectedRange?.to;
300
- if (target) setCalendarMonth(toMonthStart(target));
301
- return;
302
- }
303
-
304
- if (selectedSingle) {
305
- setCalendarMonth(toMonthStart(selectedSingle));
306
- }
307
- // eslint-disable-next-line react-hooks/exhaustive-deps
308
- }, [isOpen]);
309
-
310
- const displayText = useMemo(() => {
311
- if (formatDisplay) {
312
- return (
313
- formatDisplay(currentValue, { mode, withTime: showTime }) ||
314
- resolvedPlaceholder
315
- );
316
- }
317
- return formatTriggerDisplay(
318
- currentValue,
319
- mode,
320
- showTime,
321
- resolvedPlaceholder,
322
- startPlaceholder,
323
- endPlaceholder,
324
- formatDate,
325
- );
326
- }, [
327
- currentValue,
328
- endPlaceholder,
329
- formatDate,
330
- formatDisplay,
331
- mode,
332
- resolvedPlaceholder,
333
- showTime,
334
- startPlaceholder,
335
- ]);
336
-
337
- const disabledMatcher = useMemo<Matcher | Matcher[] | undefined>(() => {
338
- const matchers: Matcher[] = [];
339
- if (minDate) matchers.push({ before: minDate });
340
- if (maxDate) matchers.push({ after: maxDate });
341
- if (disabledDates) {
342
- if (Array.isArray(disabledDates)) matchers.push(...disabledDates);
343
- else matchers.push(disabledDates);
344
- }
345
- const external = calendar?.disabled;
346
- if (external) {
347
- if (Array.isArray(external)) matchers.push(...external);
348
- else matchers.push(external);
349
- }
350
- return matchers.length ? matchers : undefined;
351
- }, [calendar?.disabled, disabledDates, maxDate, minDate]);
352
-
353
- const onChangeSingle = !isRange
354
- ? (props as SingleDatePickerProps).onChange
355
- : undefined;
356
- const onChangeRange = isRange
357
- ? (props as RangeDatePickerProps).onChange
358
- : undefined;
359
-
360
- const commit = useCallback(
361
- (next: InternalValue, closeMenu = false) => {
362
- if (!isControlled) setInternalValue(next);
363
- if (isRange) {
364
- onChangeRange?.(next as DateRange | undefined);
365
- } else {
366
- onChangeSingle?.(next as Date | undefined);
367
- }
368
- if (closeMenu) setIsOpen(false);
369
- },
370
- [isControlled, isRange, onChangeRange, onChangeSingle],
371
- );
372
-
373
- const resolveDayValue = useCallback(
374
- (date: Date, existingTime: Date | undefined): Date =>
375
- showTime ? mergeDateWithTimeOf(date, existingTime) : startOfDay(date),
376
- [showTime],
377
- );
378
-
379
- const handleSingleSelect = useCallback(
380
- (nextDate: Date | undefined) => {
381
- if (!nextDate) {
382
- commit(undefined);
383
- return;
384
- }
385
-
386
- const existing = currentValue instanceof Date ? currentValue : undefined;
387
- commit(resolveDayValue(nextDate, existing), shouldCloseOnSelection);
388
- },
389
- [commit, currentValue, resolveDayValue, shouldCloseOnSelection],
390
- );
391
-
392
- const handleSingleDateInputChange = useCallback(
393
- (event: ChangeEvent<HTMLInputElement>) => {
394
- const raw = event.target.value;
395
- const formatted = autoFormatDateText(raw, singleManualText);
396
- setSingleManualText(formatted);
397
-
398
- const trimmed = formatted.trim();
399
- if (!trimmed) {
400
- commit(undefined);
401
- return;
402
- }
403
-
404
- const nextDate = parseDateTextString(trimmed);
405
-
406
- if (!nextDate || !isWithinBounds(nextDate)) {
407
- return;
408
- }
409
-
410
- const existing = currentValue instanceof Date ? currentValue : undefined;
411
-
412
- // Live-sync calendar month
413
- setCalendarMonth(toMonthStart(nextDate));
414
- commit(resolveDayValue(nextDate, existing));
415
- },
416
- [commit, currentValue, isWithinBounds, resolveDayValue, singleManualText],
417
- );
418
-
419
- const handleSingleDateInputBlur = useCallback(() => {
420
- setSingleManualText(selectedSingleText);
421
- }, [selectedSingleText]);
422
-
423
- const handleRangeDateInputChange = useCallback(
424
- (key: "from" | "to", event: ChangeEvent<HTMLInputElement>) => {
425
- const raw = event.target.value;
426
- const prev = rangeManualText[key];
427
- const formatted = autoFormatDateText(raw, prev);
428
-
429
- setRangeManualText((prev) => ({
430
- ...prev,
431
- [key]: formatted,
432
- }));
433
-
434
- const trimmed = formatted.trim();
435
- const existing: DateRange = isRangeValue(currentValue)
436
- ? (currentValue as DateRange)
437
- : {};
438
-
439
- if (!trimmed) {
440
- const nextRange =
441
- key === "from"
442
- ? { ...existing, from: undefined }
443
- : { ...existing, to: undefined };
444
-
445
- commit(nextRange.from || nextRange.to ? nextRange : undefined);
446
- return;
447
- }
448
-
449
- const nextDateRaw = parseDateTextString(trimmed);
450
- if (!nextDateRaw || !isWithinBounds(nextDateRaw)) {
451
- return;
452
- }
453
-
454
- const nextDate = resolveDayValue(nextDateRaw, existing[key]);
455
-
456
- const nextRange =
457
- key === "from"
458
- ? { ...existing, from: nextDate }
459
- : { ...existing, to: nextDate };
460
-
461
- if (nextRange.from && nextRange.to && nextRange.from > nextRange.to) {
462
- if (key === "from") {
463
- nextRange.to = nextRange.from;
464
- } else {
465
- nextRange.from = nextRange.to;
466
- }
467
- }
468
-
469
- // Live-sync calendar month
470
- const nextMonth = toMonthStart(nextDateRaw);
471
-
472
- if (resolvedRangeMonthCount > 1) {
473
- const panelIndex = key === "from" ? 0 : resolvedRangeMonthCount - 1;
474
-
475
- setRangeCalendarMonths((prev) => {
476
- const seed = prev[0] ?? nextMonth;
477
- const next = Array.from(
478
- { length: resolvedRangeMonthCount },
479
- (_, index) => prev[index] ?? addMonthOffset(seed, index),
480
- );
481
- next[panelIndex] = nextMonth;
482
- return next;
483
- });
484
- } else {
485
- setCalendarMonth(nextMonth);
486
- }
487
-
488
- commit(nextRange);
489
- },
490
- [
491
- commit,
492
- currentValue,
493
- isWithinBounds,
494
- rangeManualText,
495
- resolveDayValue,
496
- resolvedRangeMonthCount,
497
- ],
498
- );
499
-
500
- const handleRangeDateInputBlur = useCallback(() => {
501
- setRangeManualText({
502
- from: selectedRangeFromText,
503
- to: selectedRangeToText,
504
- });
505
- }, [selectedRangeFromText, selectedRangeToText]);
506
-
507
- const handleManualInputKeyDown = useCallback(
508
- (event: KeyboardEvent<HTMLInputElement>) => {
509
- if (event.key !== "Enter") return;
510
- event.preventDefault();
511
- event.currentTarget.blur();
512
- },
513
- [],
514
- );
515
-
516
- const handleRangeSelect = useCallback(
517
- (next: DateRange | undefined) => {
518
- if (!next) {
519
- commit(undefined);
520
- return;
521
- }
522
-
523
- const existing = isRangeValue(currentValue)
524
- ? (currentValue as DateRange)
525
- : undefined;
526
-
527
- const from = next.from
528
- ? resolveDayValue(next.from, existing?.from)
529
- : undefined;
530
- const to = next.to
531
- ? resolveDayValue(next.to, existing?.to)
532
- : undefined;
533
-
534
- const nextRange: DateRange | undefined =
535
- from || to ? { from, to } : undefined;
536
-
537
- const canClose =
538
- shouldCloseOnSelection && Boolean(nextRange?.from && nextRange?.to);
539
-
540
- commit(nextRange, canClose);
541
- },
542
- [commit, currentValue, resolveDayValue, shouldCloseOnSelection],
543
- );
544
-
545
- const handleRangePanelMonthChange = useCallback(
546
- (index: number, nextMonthRaw: Date) => {
547
- const nextMonth = toMonthStart(nextMonthRaw);
548
-
549
- setRangeCalendarMonths((prev) => {
550
- const seed = prev[0] ?? nextMonth;
551
- const next = Array.from(
552
- { length: resolvedRangeMonthCount },
553
- (_, monthIndex) => prev[monthIndex] ?? addMonthOffset(seed, monthIndex),
554
- );
555
- next[index] = nextMonth;
556
- return next;
557
- });
558
- },
559
- [resolvedRangeMonthCount],
560
- );
561
-
562
- const handleSingleTimeChange = useCallback(
563
- (time: string | undefined) => {
564
- if (!time) return;
565
- if (!(currentValue instanceof Date)) return;
566
- commit(withTimeString(currentValue, time));
567
- },
568
- [commit, currentValue],
569
- );
570
-
571
- const handleRangeTimeChange = useCallback(
572
- (key: "from" | "to", time: string | undefined) => {
573
- if (!time) return;
574
- const range = isRangeValue(currentValue)
575
- ? (currentValue as DateRange)
576
- : undefined;
577
- if (!range?.[key]) return;
578
- commit({ ...range, [key]: withTimeString(range[key]!, time) });
579
- },
580
- [commit, currentValue],
581
- );
582
-
583
- const popoverOnClose = popover?.onClose;
584
- const handlePopoverClose = useCallback(() => {
585
- popoverOnClose?.();
586
- onBlur?.();
587
- }, [onBlur, popoverOnClose]);
588
-
589
- const {
590
- className: popoverClassName,
591
- offset: popoverOffset = 4,
592
- ...popoverRest
593
- } = popover ?? {};
594
-
595
- const {
596
- className: popoverTriggerClassName,
597
- style: popoverTriggerStyle,
598
- ...popoverTriggerRest
599
- } = popoverTrigger ?? {};
600
-
601
- const {
602
- className: popoverMenuClassName,
603
- style: popoverMenuStyle,
604
- ...popoverMenuRest
605
- } = popoverMenu ?? {};
606
-
607
- const timeValueSingle =
608
- selectedSingle && showTime ? toTimeString(selectedSingle) : "";
609
-
610
- const timeValueRangeFrom =
611
- selectedRange?.from && showTime ? toTimeString(selectedRange.from) : "";
612
- const timeValueRangeTo =
613
- selectedRange?.to && showTime ? toTimeString(selectedRange.to) : "";
614
-
615
- const timeMinuteStep = useMemo(() => {
616
- if (!timeStep || !Number.isFinite(timeStep) || timeStep <= 0) return 1;
617
- return Math.max(1, Math.round(timeStep / 60));
618
- }, [timeStep]);
619
-
620
- const resolvedTimePickerPopover = useMemo(
621
- () => ({
622
- renderOnPortal: false,
623
- closeOnPathChange: false,
624
- closeOnClickOutside: true,
625
- position: timePickerPopover?.position ?? "bottom",
626
- align: timePickerPopover?.align ?? "start",
627
- offset: 2,
628
- }),
629
- [timePickerPopover?.align, timePickerPopover?.position],
630
- );
631
-
62
+ const {
63
+ label,
64
+ className,
65
+ style,
66
+ id,
67
+ info,
68
+ errors,
69
+ layout = "default",
70
+ layoutDirection = "default",
71
+
72
+ placeholder,
73
+ startPlaceholder = DEFAULT_START_PLACEHOLDER,
74
+ endPlaceholder = DEFAULT_END_PLACEHOLDER,
75
+
76
+ minDate,
77
+ maxDate,
78
+ disabledDates,
79
+
80
+ disabled = false,
81
+ readOnly = false,
82
+ required = false,
83
+ closeOnSelection,
84
+ showManualInput = true,
85
+
86
+ showTime = false,
87
+ timeStep,
88
+ timeFormat = "12h",
89
+ timePickerPopover,
90
+ showActionFooter = true,
91
+
92
+ showClear = true,
93
+ onClear,
94
+ onBlur,
95
+
96
+ numberOfMonths,
97
+ weekStartsOn = 1,
98
+ captionLayout = "dropdown",
99
+ showWeekNumber = false,
100
+
101
+ formatDisplay,
102
+ formatDate = defaultFormatDate,
103
+
104
+ popover,
105
+ popoverTrigger,
106
+ popoverMenu,
107
+ selectButton,
108
+ calendar,
109
+ } = props;
110
+
111
+ const mode: DatePickerMode = props.mode ?? "single";
112
+ const isRange = mode === "range";
113
+
114
+ const generatedId = useId();
115
+ const inputId = id ?? generatedId;
116
+
117
+ const isControlled = "value" in props;
118
+
119
+ const anchorDateOnly = !showTime;
120
+
121
+ const [internalValue, setInternalValue] = useState<InternalValue>(() => {
122
+ if (isRange) {
123
+ return coerceToRange(
124
+ (props as RangeDatePickerProps).defaultValue,
125
+ anchorDateOnly,
126
+ );
127
+ }
128
+ return coerceToDate(
129
+ (props as SingleDatePickerProps).defaultValue,
130
+ anchorDateOnly,
131
+ );
132
+ });
133
+
134
+ const currentValue: InternalValue = isControlled
135
+ ? isRange
136
+ ? coerceToRange(
137
+ (props as RangeDatePickerProps).value,
138
+ anchorDateOnly,
139
+ )
140
+ : coerceToDate(
141
+ (props as SingleDatePickerProps).value,
142
+ anchorDateOnly,
143
+ )
144
+ : internalValue;
145
+
146
+ const selectedSingle =
147
+ !isRange && currentValue instanceof Date ? currentValue : undefined;
148
+ const selectedRange =
149
+ isRange && isRangeValue(currentValue)
150
+ ? (currentValue as DateRange)
151
+ : undefined;
152
+
153
+ const selectedSingleText = selectedSingle
154
+ ? toDateInputString(selectedSingle)
155
+ : "";
156
+ const selectedRangeFromText = selectedRange?.from
157
+ ? toDateInputString(selectedRange.from)
158
+ : "";
159
+ const selectedRangeToText = selectedRange?.to
160
+ ? toDateInputString(selectedRange.to)
161
+ : "";
162
+
163
+ const [isOpen, setIsOpen] = useState(false);
164
+ const [singleManualText, setSingleManualText] = useState(selectedSingleText);
165
+ const [rangeManualText, setRangeManualText] = useState<{
166
+ from: string;
167
+ to: string;
168
+ }>({ from: selectedRangeFromText, to: selectedRangeToText });
169
+
170
+ // The typed text follows the value whenever it changes, from outside or by
171
+ // a pick, in the render that sees the change.
172
+ const valueTextKey = isRange
173
+ ? `range:${selectedRangeFromText}|${selectedRangeToText}`
174
+ : `single:${selectedSingleText}`;
175
+ const [seenValueTextKey, setSeenValueTextKey] = useState(valueTextKey);
176
+ if (valueTextKey !== seenValueTextKey) {
177
+ setSeenValueTextKey(valueTextKey);
178
+ if (isRange) {
179
+ setRangeManualText({
180
+ from: selectedRangeFromText,
181
+ to: selectedRangeToText,
182
+ });
183
+ } else {
184
+ setSingleManualText(selectedSingleText);
185
+ }
186
+ }
187
+
188
+ const resolvedRangeMonthCount = isRange
189
+ ? Math.max(1, numberOfMonths ?? calendar?.numberOfMonths ?? 2)
190
+ : 1;
191
+
192
+ // Controlled month state for live calendar navigation from manual input
193
+ const [calendarMonth, setCalendarMonth] = useState<Date | undefined>(() => {
194
+ if (isRange) {
195
+ const seed = selectedRange?.from ?? selectedRange?.to;
196
+ return seed ? toMonthStart(seed) : undefined;
197
+ }
198
+ return selectedSingle ? toMonthStart(selectedSingle) : undefined;
199
+ });
200
+
201
+ const [rangeCalendarMonths, setRangeCalendarMonths] = useState<Date[]>(() => {
202
+ const fromMonth = selectedRange?.from
203
+ ? toMonthStart(selectedRange.from)
204
+ : undefined;
205
+ const toMonth = selectedRange?.to
206
+ ? toMonthStart(selectedRange.to)
207
+ : undefined;
208
+
209
+ if (fromMonth && toMonth && resolvedRangeMonthCount > 1) {
210
+ return [
211
+ fromMonth,
212
+ ...Array.from({ length: resolvedRangeMonthCount - 2 }, (_, index) =>
213
+ addMonthOffset(fromMonth, index + 1),
214
+ ),
215
+ toMonth,
216
+ ];
217
+ }
218
+
219
+ const seed = fromMonth ?? toMonth ?? toMonthStart(new Date());
220
+ return Array.from({ length: resolvedRangeMonthCount }, (_, index) =>
221
+ addMonthOffset(seed, index),
222
+ );
223
+ });
224
+
225
+ const hasValue = isRange
226
+ ? hasRangeValue(currentValue as DateRange | undefined)
227
+ : Boolean(currentValue);
228
+
229
+ const shouldCloseOnSelection = closeOnSelection ?? false;
230
+
231
+ const minDay = useMemo(
232
+ () => (minDate ? startOfDay(minDate) : undefined),
233
+ [minDate],
234
+ );
235
+ const maxDay = useMemo(
236
+ () => (maxDate ? startOfDay(maxDate) : undefined),
237
+ [maxDate],
238
+ );
239
+
240
+ const isWithinBounds = useCallback(
241
+ (date: Date) => {
242
+ const day = startOfDay(date);
243
+ if (minDay && day < minDay) return false;
244
+ if (maxDay && day > maxDay) return false;
245
+ return true;
246
+ },
247
+ [maxDay, minDay],
248
+ );
249
+
250
+ const resolvedPlaceholder =
251
+ placeholder ?? (isRange ? "Select date range..." : "Select date...");
252
+
253
+ // One month per calendar shown: when the count changes, the months follow.
254
+ if (
255
+ isRange &&
256
+ resolvedRangeMonthCount > 1 &&
257
+ rangeCalendarMonths.length !== resolvedRangeMonthCount
258
+ ) {
259
+ const seed = rangeCalendarMonths[0] ??
260
+ toMonthStart(selectedRange?.from ?? selectedRange?.to ?? new Date());
261
+ setRangeCalendarMonths(
262
+ Array.from({ length: resolvedRangeMonthCount }, (_, index) =>
263
+ rangeCalendarMonths[index] ?? addMonthOffset(seed, index),
264
+ ),
265
+ );
266
+ }
267
+
268
+ // Opening shows the months of the value, in the render that opens it.
269
+ const [wasOpen, setWasOpen] = useState(isOpen);
270
+ if (isOpen !== wasOpen) {
271
+ setWasOpen(isOpen);
272
+ if (isOpen && isRange) {
273
+ const fromMonth = selectedRange?.from
274
+ ? toMonthStart(selectedRange.from)
275
+ : undefined;
276
+ const toMonth = selectedRange?.to
277
+ ? toMonthStart(selectedRange.to)
278
+ : undefined;
279
+
280
+ if (resolvedRangeMonthCount > 1) {
281
+ setRangeCalendarMonths((prev) => {
282
+ const seed = fromMonth ?? toMonth ?? prev[0] ?? toMonthStart(new Date());
283
+ const next = Array.from(
284
+ { length: resolvedRangeMonthCount },
285
+ (_, index) => prev[index] ?? addMonthOffset(seed, index),
286
+ );
287
+
288
+ if (fromMonth) next[0] = fromMonth;
289
+ if (toMonth) next[resolvedRangeMonthCount - 1] = toMonth;
290
+
291
+ const unchanged =
292
+ prev.length === next.length &&
293
+ prev.every((month, index) => isSameMonth(month, next[index]));
294
+
295
+ return unchanged ? prev : next;
296
+ });
297
+ }
298
+
299
+ const target = selectedRange?.from ?? selectedRange?.to;
300
+ if (target) setCalendarMonth(toMonthStart(target));
301
+ } else if (isOpen && selectedSingle) {
302
+ setCalendarMonth(toMonthStart(selectedSingle));
303
+ }
304
+ }
305
+
306
+ const displayText = useMemo(() => {
307
+ if (formatDisplay) {
308
+ return (
309
+ formatDisplay(currentValue, { mode, withTime: showTime }) ||
310
+ resolvedPlaceholder
311
+ );
312
+ }
313
+ return formatTriggerDisplay(
314
+ currentValue,
315
+ mode,
316
+ showTime,
317
+ resolvedPlaceholder,
318
+ startPlaceholder,
319
+ endPlaceholder,
320
+ formatDate,
321
+ );
322
+ }, [
323
+ currentValue,
324
+ endPlaceholder,
325
+ formatDate,
326
+ formatDisplay,
327
+ mode,
328
+ resolvedPlaceholder,
329
+ showTime,
330
+ startPlaceholder,
331
+ ]);
332
+
333
+ const disabledMatcher = useMemo<Matcher | Matcher[] | undefined>(() => {
334
+ const matchers: Matcher[] = [];
335
+ if (minDate) matchers.push({ before: minDate });
336
+ if (maxDate) matchers.push({ after: maxDate });
337
+ if (disabledDates) {
338
+ if (Array.isArray(disabledDates)) matchers.push(...disabledDates);
339
+ else matchers.push(disabledDates);
340
+ }
341
+ const external = calendar?.disabled;
342
+ if (external) {
343
+ if (Array.isArray(external)) matchers.push(...external);
344
+ else matchers.push(external);
345
+ }
346
+ return matchers.length ? matchers : undefined;
347
+ }, [calendar?.disabled, disabledDates, maxDate, minDate]);
348
+
349
+ const onChangeSingle = !isRange
350
+ ? (props as SingleDatePickerProps).onChange
351
+ : undefined;
352
+ const onChangeRange = isRange
353
+ ? (props as RangeDatePickerProps).onChange
354
+ : undefined;
355
+
356
+ const commit = useCallback(
357
+ (next: InternalValue, closeMenu = false) => {
358
+ if (!isControlled) setInternalValue(next);
359
+ if (isRange) {
360
+ onChangeRange?.(next as DateRange | undefined);
361
+ } else {
362
+ onChangeSingle?.(next as Date | undefined);
363
+ }
364
+ if (closeMenu) setIsOpen(false);
365
+ },
366
+ [isControlled, isRange, onChangeRange, onChangeSingle],
367
+ );
368
+
369
+ const resolveDayValue = useCallback(
370
+ (date: Date, existingTime: Date | undefined): Date =>
371
+ showTime ? mergeDateWithTimeOf(date, existingTime) : startOfDay(date),
372
+ [showTime],
373
+ );
374
+
375
+ const handleSingleSelect = useCallback(
376
+ (nextDate: Date | undefined) => {
377
+ if (!nextDate) {
378
+ commit(undefined);
379
+ return;
380
+ }
381
+
382
+ const existing = currentValue instanceof Date ? currentValue : undefined;
383
+ commit(resolveDayValue(nextDate, existing), shouldCloseOnSelection);
384
+ },
385
+ [commit, currentValue, resolveDayValue, shouldCloseOnSelection],
386
+ );
387
+
388
+ const handleSingleDateInputChange = useCallback(
389
+ (event: ChangeEvent<HTMLInputElement>) => {
390
+ const raw = event.target.value;
391
+ const formatted = autoFormatDateText(raw, singleManualText);
392
+ setSingleManualText(formatted);
393
+
394
+ const trimmed = formatted.trim();
395
+ if (!trimmed) {
396
+ commit(undefined);
397
+ return;
398
+ }
399
+
400
+ const nextDate = parseDateTextString(trimmed);
401
+
402
+ if (!nextDate || !isWithinBounds(nextDate)) {
403
+ return;
404
+ }
405
+
406
+ const existing = currentValue instanceof Date ? currentValue : undefined;
407
+
408
+ // Live-sync calendar month
409
+ setCalendarMonth(toMonthStart(nextDate));
410
+ commit(resolveDayValue(nextDate, existing));
411
+ },
412
+ [commit, currentValue, isWithinBounds, resolveDayValue, singleManualText],
413
+ );
414
+
415
+ const handleSingleDateInputBlur = useCallback(() => {
416
+ setSingleManualText(selectedSingleText);
417
+ }, [selectedSingleText]);
418
+
419
+ const handleRangeDateInputChange = useCallback(
420
+ (key: "from" | "to", event: ChangeEvent<HTMLInputElement>) => {
421
+ const raw = event.target.value;
422
+ const prev = rangeManualText[key];
423
+ const formatted = autoFormatDateText(raw, prev);
424
+
425
+ setRangeManualText((prev) => ({
426
+ ...prev,
427
+ [key]: formatted,
428
+ }));
429
+
430
+ const trimmed = formatted.trim();
431
+ const existing: DateRange = isRangeValue(currentValue)
432
+ ? (currentValue as DateRange)
433
+ : {};
434
+
435
+ if (!trimmed) {
436
+ const nextRange =
437
+ key === "from"
438
+ ? { ...existing, from: undefined }
439
+ : { ...existing, to: undefined };
440
+
441
+ commit(nextRange.from || nextRange.to ? nextRange : undefined);
442
+ return;
443
+ }
444
+
445
+ const nextDateRaw = parseDateTextString(trimmed);
446
+ if (!nextDateRaw || !isWithinBounds(nextDateRaw)) {
447
+ return;
448
+ }
449
+
450
+ const nextDate = resolveDayValue(nextDateRaw, existing[key]);
451
+
452
+ const nextRange =
453
+ key === "from"
454
+ ? { ...existing, from: nextDate }
455
+ : { ...existing, to: nextDate };
456
+
457
+ if (nextRange.from && nextRange.to && nextRange.from > nextRange.to) {
458
+ if (key === "from") {
459
+ nextRange.to = nextRange.from;
460
+ } else {
461
+ nextRange.from = nextRange.to;
462
+ }
463
+ }
464
+
465
+ // Live-sync calendar month
466
+ const nextMonth = toMonthStart(nextDateRaw);
467
+
468
+ if (resolvedRangeMonthCount > 1) {
469
+ const panelIndex = key === "from" ? 0 : resolvedRangeMonthCount - 1;
470
+
471
+ setRangeCalendarMonths((prev) => {
472
+ const seed = prev[0] ?? nextMonth;
473
+ const next = Array.from(
474
+ { length: resolvedRangeMonthCount },
475
+ (_, index) => prev[index] ?? addMonthOffset(seed, index),
476
+ );
477
+ next[panelIndex] = nextMonth;
478
+ return next;
479
+ });
480
+ } else {
481
+ setCalendarMonth(nextMonth);
482
+ }
483
+
484
+ commit(nextRange);
485
+ },
486
+ [
487
+ commit,
488
+ currentValue,
489
+ isWithinBounds,
490
+ rangeManualText,
491
+ resolveDayValue,
492
+ resolvedRangeMonthCount,
493
+ ],
494
+ );
495
+
496
+ const handleRangeDateInputBlur = useCallback(() => {
497
+ setRangeManualText({
498
+ from: selectedRangeFromText,
499
+ to: selectedRangeToText,
500
+ });
501
+ }, [selectedRangeFromText, selectedRangeToText]);
502
+
503
+ const handleManualInputKeyDown = useCallback(
504
+ (event: KeyboardEvent<HTMLInputElement>) => {
505
+ if (event.key !== "Enter") return;
506
+ event.preventDefault();
507
+ event.currentTarget.blur();
508
+ },
509
+ [],
510
+ );
511
+
512
+ const handleRangeSelect = useCallback(
513
+ (next: DateRange | undefined) => {
514
+ if (!next) {
515
+ commit(undefined);
516
+ return;
517
+ }
518
+
519
+ const existing = isRangeValue(currentValue)
520
+ ? (currentValue as DateRange)
521
+ : undefined;
522
+
523
+ const from = next.from
524
+ ? resolveDayValue(next.from, existing?.from)
525
+ : undefined;
526
+ const to = next.to
527
+ ? resolveDayValue(next.to, existing?.to)
528
+ : undefined;
529
+
530
+ const nextRange: DateRange | undefined =
531
+ from || to ? { from, to } : undefined;
532
+
533
+ const canClose =
534
+ shouldCloseOnSelection && Boolean(nextRange?.from && nextRange?.to);
535
+
536
+ commit(nextRange, canClose);
537
+ },
538
+ [commit, currentValue, resolveDayValue, shouldCloseOnSelection],
539
+ );
540
+
541
+ const handleRangePanelMonthChange = useCallback(
542
+ (index: number, nextMonthRaw: Date) => {
543
+ const nextMonth = toMonthStart(nextMonthRaw);
544
+
545
+ setRangeCalendarMonths((prev) => {
546
+ const seed = prev[0] ?? nextMonth;
547
+ const next = Array.from(
548
+ { length: resolvedRangeMonthCount },
549
+ (_, monthIndex) => prev[monthIndex] ?? addMonthOffset(seed, monthIndex),
550
+ );
551
+ next[index] = nextMonth;
552
+ return next;
553
+ });
554
+ },
555
+ [resolvedRangeMonthCount],
556
+ );
557
+
558
+ const handleSingleTimeChange = useCallback(
559
+ (time: string | undefined) => {
560
+ if (!time) return;
561
+ if (!(currentValue instanceof Date)) return;
562
+ commit(withTimeString(currentValue, time));
563
+ },
564
+ [commit, currentValue],
565
+ );
566
+
567
+ const handleRangeTimeChange = useCallback(
568
+ (key: "from" | "to", time: string | undefined) => {
569
+ if (!time) return;
570
+ const range = isRangeValue(currentValue)
571
+ ? (currentValue as DateRange)
572
+ : undefined;
573
+ if (!range?.[key]) return;
574
+ commit({ ...range, [key]: withTimeString(range[key]!, time) });
575
+ },
576
+ [commit, currentValue],
577
+ );
578
+
579
+ const popoverOnClose = popover?.onClose;
580
+ const handlePopoverClose = useCallback(() => {
581
+ popoverOnClose?.();
582
+ onBlur?.();
583
+ }, [onBlur, popoverOnClose]);
584
+
585
+ const {
586
+ className: popoverClassName,
587
+ offset: popoverOffset = 4,
588
+ ...popoverRest
589
+ } = popover ?? {};
590
+
591
+ const {
592
+ className: popoverTriggerClassName,
593
+ style: popoverTriggerStyle,
594
+ ...popoverTriggerRest
595
+ } = popoverTrigger ?? {};
596
+
597
+ const {
598
+ className: popoverMenuClassName,
599
+ style: popoverMenuStyle,
600
+ ...popoverMenuRest
601
+ } = popoverMenu ?? {};
602
+
603
+ const timeValueSingle =
604
+ selectedSingle && showTime ? toTimeString(selectedSingle) : "";
605
+
606
+ const timeValueRangeFrom =
607
+ selectedRange?.from && showTime ? toTimeString(selectedRange.from) : "";
608
+ const timeValueRangeTo =
609
+ selectedRange?.to && showTime ? toTimeString(selectedRange.to) : "";
610
+
611
+ const timeMinuteStep = useMemo(() => {
612
+ if (!timeStep || !Number.isFinite(timeStep) || timeStep <= 0) return 1;
613
+ return Math.max(1, Math.round(timeStep / 60));
614
+ }, [timeStep]);
615
+
616
+ const resolvedTimePickerPopover = useMemo(
617
+ () => ({
618
+ renderOnPortal: false,
619
+ closeOnPathChange: false,
620
+ closeOnClickOutside: true,
621
+ position: timePickerPopover?.position ?? "bottom",
622
+ align: timePickerPopover?.align ?? "start",
623
+ autoPosition: timePickerPopover?.autoPosition,
624
+ collisionPadding: timePickerPopover?.collisionPadding,
625
+ offset: 2,
626
+ }),
627
+ [
628
+ timePickerPopover?.align,
629
+ timePickerPopover?.autoPosition,
630
+ timePickerPopover?.collisionPadding,
631
+ timePickerPopover?.position,
632
+ ],
633
+ );
634
+
632
635
  const isIndependentRangeMonths = isRange && resolvedRangeMonthCount > 1;
633
636
 
634
637
  return (
635
- <div className={clsx(sCommon.container, className)} style={style}>
636
- {label && (
637
- <div
638
- className={clsx(
639
- sCommon.field_container,
640
- layout !== "default" && sCommon[layout],
641
- layout !== "default" &&
642
- layoutDirection !== "default" &&
643
- sCommon[layoutDirection],
644
- )}
645
- >
646
- <label className={sCommon.label} htmlFor={inputId}>
647
- {label}
648
- </label>
649
- </div>
650
- )}
651
-
652
- <Popover
653
- closeOnPathChange={popover?.closeOnPathChange ?? true}
654
- closeOnClickOutside={popover?.closeOnClickOutside ?? true}
655
- animationDuration={popover?.animationDuration ?? 200}
656
- position={popover?.position ?? "bottom"}
657
- align={popover?.align ?? "start"}
658
- isOpen={isOpen}
659
- setIsOpen={setIsOpen}
660
- className={clsx(sPickerCommon.picker_popover, popoverClassName)}
661
- offset={popoverOffset}
662
- {...popoverRest}
663
- onClose={handlePopoverClose}
664
- >
665
- <PopoverTrigger
666
- asChild
667
- className={clsx(
668
- sPickerCommon.picker_popover_trigger,
669
- popoverTriggerClassName,
670
- )}
671
- style={popoverTriggerStyle}
672
- disabled={disabled || readOnly}
673
- {...popoverTriggerRest}
674
- >
675
- <SelectButton
676
- id={inputId}
677
- disabled={disabled || readOnly}
678
- required={required}
679
- className={clsx(
680
- !hasValue && sPickerCommon.picker_value_placeholder,
681
- selectButton?.className,
682
- )}
683
- style={selectButton?.style}
684
- buttonVariant={selectButton?.buttonVariant}
685
- buttonSize={selectButton?.buttonSize}
686
- tooltip={selectButton?.tooltip}
687
- tooltipPosition={selectButton?.tooltipPosition}
688
- showClear={hasValue && showClear && !disabled && !readOnly}
689
- onClear={(e: React.MouseEvent) => {
690
- e.stopPropagation();
691
- e.preventDefault();
692
- commit(undefined);
693
- onClear?.();
694
- }}
695
- showChevron={false}
696
- iconEnd={
697
- <Icon aria-hidden="true" clickable>
698
- <IconCalendar/>
699
- </Icon>
700
- }
701
- >
702
- {displayText}
703
- </SelectButton>
704
- </PopoverTrigger>
705
-
706
- {!disabled && !readOnly && (
707
- <PopoverMenu
708
- className={clsx(
709
- sPickerCommon.picker_popover_menu_base,
710
- s.popover_menu,
711
- popoverMenuClassName,
712
- )}
713
- style={popoverMenuStyle}
714
- {...popoverMenuRest}
715
- >
716
- <div className={s.menu_content}>
717
- {showManualInput && (
718
- <div className={s.manual_panel}>
719
- {isRange ? (
720
- <div className={s.manual_grid}>
721
- <Input
722
- type="text"
723
- value={rangeManualText.from}
724
- placeholder={`${startPlaceholder} (YYYY-MM-DD)`}
725
- required={required}
726
- autoComplete="off"
727
- onChange={(event) =>
728
- handleRangeDateInputChange("from", event)
729
- }
730
- onBlur={handleRangeDateInputBlur}
731
- onKeyDown={handleManualInputKeyDown}
732
- />
733
- <Input
734
- type="text"
735
- value={rangeManualText.to}
736
- placeholder={`${endPlaceholder} (YYYY-MM-DD)`}
737
- required={required}
738
- autoComplete="off"
739
- onChange={(event) =>
740
- handleRangeDateInputChange("to", event)
741
- }
742
- onBlur={handleRangeDateInputBlur}
743
- onKeyDown={handleManualInputKeyDown}
744
- />
745
- </div>
746
- ) : (
747
- <Input
748
- type="text"
749
- value={singleManualText}
750
- placeholder="YYYY-MM-DD"
751
- required={required}
752
- autoComplete="off"
753
- onChange={handleSingleDateInputChange}
754
- onBlur={handleSingleDateInputBlur}
755
- onKeyDown={handleManualInputKeyDown}
756
- />
757
- )}
758
- </div>
759
- )}
760
-
638
+ <div className={clsx(sCommon.container, className)} style={style}>
639
+ {label && (
640
+ <div
641
+ className={clsx(
642
+ sCommon.field_container,
643
+ layout !== "default" && sCommon[layout],
644
+ layout !== "default" &&
645
+ layoutDirection !== "default" &&
646
+ sCommon[layoutDirection],
647
+ )}
648
+ >
649
+ <label className={sCommon.label} htmlFor={inputId}>
650
+ {label}
651
+ </label>
652
+ </div>
653
+ )}
654
+
655
+ <Popover
656
+ closeOnPathChange={popover?.closeOnPathChange ?? true}
657
+ closeOnClickOutside={popover?.closeOnClickOutside ?? true}
658
+ animationDuration={popover?.animationDuration ?? 200}
659
+ position={popover?.position ?? "bottom"}
660
+ align={popover?.align ?? "start"}
661
+ isOpen={isOpen}
662
+ setIsOpen={setIsOpen}
663
+ className={clsx(sPickerCommon.picker_popover, popoverClassName)}
664
+ offset={popoverOffset}
665
+ {...popoverRest}
666
+ onClose={handlePopoverClose}
667
+ >
668
+ <PopoverTrigger
669
+ asChild
670
+ className={clsx(
671
+ sPickerCommon.picker_popover_trigger,
672
+ popoverTriggerClassName,
673
+ )}
674
+ style={popoverTriggerStyle}
675
+ disabled={disabled || readOnly}
676
+ {...popoverTriggerRest}
677
+ >
678
+ <SelectButton
679
+ id={inputId}
680
+ disabled={disabled || readOnly}
681
+ required={required}
682
+ className={clsx(
683
+ !hasValue && sPickerCommon.picker_value_placeholder,
684
+ selectButton?.className,
685
+ )}
686
+ style={selectButton?.style}
687
+ buttonVariant={selectButton?.buttonVariant}
688
+ buttonSize={selectButton?.buttonSize}
689
+ tooltip={selectButton?.tooltip}
690
+ tooltipPosition={selectButton?.tooltipPosition}
691
+ showClear={hasValue && showClear && !disabled && !readOnly}
692
+ onClear={(e: React.MouseEvent) => {
693
+ e.stopPropagation();
694
+ e.preventDefault();
695
+ commit(undefined);
696
+ onClear?.();
697
+ }}
698
+ showChevron={false}
699
+ iconEnd={
700
+ <Icon aria-hidden="true" clickable>
701
+ <IconCalendar/>
702
+ </Icon>
703
+ }
704
+ >
705
+ {displayText}
706
+ </SelectButton>
707
+ </PopoverTrigger>
708
+
709
+ {!disabled && !readOnly && (
710
+ <PopoverMenu
711
+ className={clsx(
712
+ sPickerCommon.picker_popover_menu_base,
713
+ s.popover_menu,
714
+ popoverMenuClassName,
715
+ )}
716
+ style={popoverMenuStyle}
717
+ {...popoverMenuRest}
718
+ >
719
+ <div className={s.menu_content}>
720
+ {showManualInput && (
721
+ <div className={s.manual_panel}>
722
+ {isRange ? (
723
+ <div className={s.manual_grid}>
724
+ <Input
725
+ type="text"
726
+ value={rangeManualText.from}
727
+ placeholder={`${startPlaceholder} (YYYY-MM-DD)`}
728
+ required={required}
729
+ autoComplete="off"
730
+ onChange={(event) =>
731
+ handleRangeDateInputChange("from", event)
732
+ }
733
+ onBlur={handleRangeDateInputBlur}
734
+ onKeyDown={handleManualInputKeyDown}
735
+ />
736
+ <Input
737
+ type="text"
738
+ value={rangeManualText.to}
739
+ placeholder={`${endPlaceholder} (YYYY-MM-DD)`}
740
+ required={required}
741
+ autoComplete="off"
742
+ onChange={(event) =>
743
+ handleRangeDateInputChange("to", event)
744
+ }
745
+ onBlur={handleRangeDateInputBlur}
746
+ onKeyDown={handleManualInputKeyDown}
747
+ />
748
+ </div>
749
+ ) : (
750
+ <Input
751
+ type="text"
752
+ value={singleManualText}
753
+ placeholder="YYYY-MM-DD"
754
+ required={required}
755
+ autoComplete="off"
756
+ onChange={handleSingleDateInputChange}
757
+ onBlur={handleSingleDateInputBlur}
758
+ onKeyDown={handleManualInputKeyDown}
759
+ />
760
+ )}
761
+ </div>
762
+ )}
763
+
761
764
  <div className={s.calendar_wrap}>
762
765
  {isRange ? (
763
766
  isIndependentRangeMonths ? (
764
767
  <div className={s.range_calendars}>
765
768
  {Array.from({ length: resolvedRangeMonthCount }, (_, index) => {
766
- const month =
767
- rangeCalendarMonths[index] ??
768
- addMonthOffset(
769
- rangeCalendarMonths[0] ?? toMonthStart(new Date()),
770
- index,
771
- );
772
-
773
- return (
774
- <Calendar
775
- key={`range-month-panel-${index}`}
776
- {...calendar}
777
- className={clsx(s.calendar, calendar?.className)}
778
- mode="range"
779
- selected={
780
- selectedRange
781
- ? { from: selectedRange.from, to: selectedRange.to }
782
- : undefined
783
- }
784
- onSelect={(next: RDPDateRange | undefined) =>
785
- handleRangeSelect(
786
- next ? { from: next.from, to: next.to } : undefined,
787
- )
788
- }
789
- month={month}
790
- onMonthChange={(nextMonth) =>
791
- handleRangePanelMonthChange(index, nextMonth)
792
- }
793
- numberOfMonths={1}
794
- weekStartsOn={weekStartsOn}
795
- showWeekNumber={
796
- showWeekNumber || calendar?.showWeekNumber
797
- }
798
- captionLayout={calendar?.captionLayout ?? captionLayout}
799
- disabled={disabledMatcher}
800
- required={required as false}
801
- />
802
- );
803
- })}
804
- </div>
805
- ) : (
806
- <Calendar
807
- {...calendar}
808
- className={clsx(s.calendar, calendar?.className)}
809
- mode="range"
810
- selected={
811
- selectedRange
812
- ? { from: selectedRange.from, to: selectedRange.to }
813
- : undefined
814
- }
815
- onSelect={(next: RDPDateRange | undefined) =>
816
- handleRangeSelect(
817
- next ? { from: next.from, to: next.to } : undefined,
818
- )
819
- }
820
- month={calendarMonth}
821
- onMonthChange={(nextMonth) =>
822
- setCalendarMonth(toMonthStart(nextMonth))
823
- }
824
- defaultMonth={
825
- calendar?.defaultMonth ??
826
- (selectedRange?.from ?? selectedRange?.to)
827
- }
828
- numberOfMonths={1}
829
- weekStartsOn={weekStartsOn}
830
- showWeekNumber={showWeekNumber || calendar?.showWeekNumber}
831
- captionLayout={calendar?.captionLayout ?? captionLayout}
832
- disabled={disabledMatcher}
833
- required={required as false}
834
- />
835
- )
836
- ) : (
837
- <Calendar
838
- {...calendar}
839
- className={clsx(s.calendar, calendar?.className)}
840
- mode="single"
841
- selected={selectedSingle}
842
- onSelect={handleSingleSelect}
843
- month={calendarMonth}
844
- onMonthChange={(nextMonth) =>
845
- setCalendarMonth(toMonthStart(nextMonth))
846
- }
847
- defaultMonth={calendar?.defaultMonth ?? selectedSingle}
848
- numberOfMonths={
849
- numberOfMonths ?? calendar?.numberOfMonths ?? 1
850
- }
851
- weekStartsOn={weekStartsOn}
852
- showWeekNumber={showWeekNumber || calendar?.showWeekNumber}
853
- captionLayout={calendar?.captionLayout ?? captionLayout}
854
- disabled={disabledMatcher}
855
- required={required as false}
856
- />
857
- )}
858
- </div>
859
-
860
- {showTime && (
861
- <div className={s.time_panel}>
862
- {isRange ? (
863
- <div className={s.range_time_grid}>
864
- <TimePicker
865
- placeholder={startPlaceholder}
866
- value={timeValueRangeFrom || undefined}
867
- minuteStep={timeMinuteStep}
868
- timeFormat={timeFormat}
869
- required={required}
870
- disabled={!selectedRange?.from}
871
- showClear={false}
872
- showActionFooter={false}
873
- onChange={(value) => handleRangeTimeChange("from", value)}
874
- popover={resolvedTimePickerPopover}
875
- selectButton={{ buttonVariant: "none" }}
876
- />
877
- <TimePicker
878
- placeholder={endPlaceholder}
879
- value={timeValueRangeTo || undefined}
880
- minuteStep={timeMinuteStep}
881
- timeFormat={timeFormat}
882
- required={required}
883
- disabled={!selectedRange?.to}
884
- showClear={false}
885
- showActionFooter={false}
886
- onChange={(value) => handleRangeTimeChange("to", value)}
887
- popover={resolvedTimePickerPopover}
888
- selectButton={{ buttonVariant: "none" }}
889
- />
890
- </div>
891
- ) : (
892
- <TimePicker
893
- placeholder="Time"
894
- value={timeValueSingle || undefined}
895
- minuteStep={timeMinuteStep}
896
- timeFormat={timeFormat}
897
- required={required}
898
- disabled={!selectedSingle}
899
- showClear={false}
900
- showActionFooter={false}
901
- onChange={handleSingleTimeChange}
902
- popover={resolvedTimePickerPopover}
903
- selectButton={{ buttonVariant: "none" }}
904
- />
905
- )}
906
- </div>
907
- )}
908
-
909
- {showActionFooter && (
910
- <div className={sPickerCommon.picker_actions_footer}>
911
- <Button
912
- type="button"
913
- variant="ghost"
914
- size="sm"
915
- disabled={!hasValue}
916
- onClick={() => {
917
- if (disabled || readOnly) return;
918
- commit(undefined);
919
- onClear?.();
920
- }}
921
- >
922
- Clear
923
- </Button>
924
-
925
- <Button
926
- type="button"
927
- variant="primary"
928
- size="sm"
929
- onClick={() => {
930
- setIsOpen(false);
931
- }}
932
- >
933
- Done
934
- </Button>
935
- </div>
936
- )}
937
- </div>
938
- </PopoverMenu>
939
- )}
940
- </Popover>
941
-
942
- <FieldMessage errors={errors} info={info} />
943
- </div>
944
- );
945
- };
769
+ const month =
770
+ rangeCalendarMonths[index] ??
771
+ addMonthOffset(
772
+ rangeCalendarMonths[0] ?? toMonthStart(new Date()),
773
+ index,
774
+ );
775
+
776
+ return (
777
+ <Calendar
778
+ key={`range-month-panel-${index}`}
779
+ {...calendar}
780
+ className={clsx(s.calendar, calendar?.className)}
781
+ mode="range"
782
+ selected={
783
+ selectedRange
784
+ ? { from: selectedRange.from, to: selectedRange.to }
785
+ : undefined
786
+ }
787
+ onSelect={(next: RDPDateRange | undefined) =>
788
+ handleRangeSelect(
789
+ next ? { from: next.from, to: next.to } : undefined,
790
+ )
791
+ }
792
+ month={month}
793
+ onMonthChange={(nextMonth) =>
794
+ handleRangePanelMonthChange(index, nextMonth)
795
+ }
796
+ numberOfMonths={1}
797
+ weekStartsOn={weekStartsOn}
798
+ showWeekNumber={
799
+ showWeekNumber || calendar?.showWeekNumber
800
+ }
801
+ captionLayout={calendar?.captionLayout ?? captionLayout}
802
+ disabled={disabledMatcher}
803
+ required={required as false}
804
+ />
805
+ );
806
+ })}
807
+ </div>
808
+ ) : (
809
+ <Calendar
810
+ {...calendar}
811
+ className={clsx(s.calendar, calendar?.className)}
812
+ mode="range"
813
+ selected={
814
+ selectedRange
815
+ ? { from: selectedRange.from, to: selectedRange.to }
816
+ : undefined
817
+ }
818
+ onSelect={(next: RDPDateRange | undefined) =>
819
+ handleRangeSelect(
820
+ next ? { from: next.from, to: next.to } : undefined,
821
+ )
822
+ }
823
+ month={calendarMonth}
824
+ onMonthChange={(nextMonth) =>
825
+ setCalendarMonth(toMonthStart(nextMonth))
826
+ }
827
+ defaultMonth={
828
+ calendar?.defaultMonth ??
829
+ (selectedRange?.from ?? selectedRange?.to)
830
+ }
831
+ numberOfMonths={1}
832
+ weekStartsOn={weekStartsOn}
833
+ showWeekNumber={showWeekNumber || calendar?.showWeekNumber}
834
+ captionLayout={calendar?.captionLayout ?? captionLayout}
835
+ disabled={disabledMatcher}
836
+ required={required as false}
837
+ />
838
+ )
839
+ ) : (
840
+ <Calendar
841
+ {...calendar}
842
+ className={clsx(s.calendar, calendar?.className)}
843
+ mode="single"
844
+ selected={selectedSingle}
845
+ onSelect={handleSingleSelect}
846
+ month={calendarMonth}
847
+ onMonthChange={(nextMonth) =>
848
+ setCalendarMonth(toMonthStart(nextMonth))
849
+ }
850
+ defaultMonth={calendar?.defaultMonth ?? selectedSingle}
851
+ numberOfMonths={
852
+ numberOfMonths ?? calendar?.numberOfMonths ?? 1
853
+ }
854
+ weekStartsOn={weekStartsOn}
855
+ showWeekNumber={showWeekNumber || calendar?.showWeekNumber}
856
+ captionLayout={calendar?.captionLayout ?? captionLayout}
857
+ disabled={disabledMatcher}
858
+ required={required as false}
859
+ />
860
+ )}
861
+ </div>
862
+
863
+ {showTime && (
864
+ <div className={s.time_panel}>
865
+ {isRange ? (
866
+ <div className={s.range_time_grid}>
867
+ <TimePicker
868
+ placeholder={startPlaceholder}
869
+ value={timeValueRangeFrom || undefined}
870
+ minuteStep={timeMinuteStep}
871
+ timeFormat={timeFormat}
872
+ required={required}
873
+ disabled={!selectedRange?.from}
874
+ showClear={false}
875
+ showActionFooter={false}
876
+ onChange={(value) => handleRangeTimeChange("from", value)}
877
+ popover={resolvedTimePickerPopover}
878
+ selectButton={{ buttonVariant: "none" }}
879
+ />
880
+ <TimePicker
881
+ placeholder={endPlaceholder}
882
+ value={timeValueRangeTo || undefined}
883
+ minuteStep={timeMinuteStep}
884
+ timeFormat={timeFormat}
885
+ required={required}
886
+ disabled={!selectedRange?.to}
887
+ showClear={false}
888
+ showActionFooter={false}
889
+ onChange={(value) => handleRangeTimeChange("to", value)}
890
+ popover={resolvedTimePickerPopover}
891
+ selectButton={{ buttonVariant: "none" }}
892
+ />
893
+ </div>
894
+ ) : (
895
+ <TimePicker
896
+ placeholder="Time"
897
+ value={timeValueSingle || undefined}
898
+ minuteStep={timeMinuteStep}
899
+ timeFormat={timeFormat}
900
+ required={required}
901
+ disabled={!selectedSingle}
902
+ showClear={false}
903
+ showActionFooter={false}
904
+ onChange={handleSingleTimeChange}
905
+ popover={resolvedTimePickerPopover}
906
+ selectButton={{ buttonVariant: "none" }}
907
+ />
908
+ )}
909
+ </div>
910
+ )}
911
+
912
+ {showActionFooter && (
913
+ <div className={sPickerCommon.picker_actions_footer}>
914
+ <Button
915
+ type="button"
916
+ variant="ghost"
917
+ size="sm"
918
+ disabled={!hasValue}
919
+ onClick={() => {
920
+ if (disabled || readOnly) return;
921
+ commit(undefined);
922
+ onClear?.();
923
+ }}
924
+ >
925
+ Clear
926
+ </Button>
927
+
928
+ <Button
929
+ type="button"
930
+ variant="primary"
931
+ size="sm"
932
+ onClick={() => {
933
+ setIsOpen(false);
934
+ }}
935
+ >
936
+ Done
937
+ </Button>
938
+ </div>
939
+ )}
940
+ </div>
941
+ </PopoverMenu>
942
+ )}
943
+ </Popover>
944
+
945
+ <FieldMessage errors={errors} info={info} />
946
+ </div>
947
+ );
948
+ };