aplance-ui 1.3.0 → 1.4.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.
@@ -1,945 +1,952 @@
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
+ 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
+
59
59
  const DEFAULT_START_PLACEHOLDER = "Start date";
60
60
  const DEFAULT_END_PLACEHOLDER = "End date";
61
61
 
62
62
  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
-
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
+ autoPosition: timePickerPopover?.autoPosition,
628
+ collisionPadding: timePickerPopover?.collisionPadding,
629
+ offset: 2,
630
+ }),
631
+ [
632
+ timePickerPopover?.align,
633
+ timePickerPopover?.autoPosition,
634
+ timePickerPopover?.collisionPadding,
635
+ timePickerPopover?.position,
636
+ ],
637
+ );
638
+
632
639
  const isIndependentRangeMonths = isRange && resolvedRangeMonthCount > 1;
633
640
 
634
641
  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
-
642
+ <div className={clsx(sCommon.container, className)} style={style}>
643
+ {label && (
644
+ <div
645
+ className={clsx(
646
+ sCommon.field_container,
647
+ layout !== "default" && sCommon[layout],
648
+ layout !== "default" &&
649
+ layoutDirection !== "default" &&
650
+ sCommon[layoutDirection],
651
+ )}
652
+ >
653
+ <label className={sCommon.label} htmlFor={inputId}>
654
+ {label}
655
+ </label>
656
+ </div>
657
+ )}
658
+
659
+ <Popover
660
+ closeOnPathChange={popover?.closeOnPathChange ?? true}
661
+ closeOnClickOutside={popover?.closeOnClickOutside ?? true}
662
+ animationDuration={popover?.animationDuration ?? 200}
663
+ position={popover?.position ?? "bottom"}
664
+ align={popover?.align ?? "start"}
665
+ isOpen={isOpen}
666
+ setIsOpen={setIsOpen}
667
+ className={clsx(sPickerCommon.picker_popover, popoverClassName)}
668
+ offset={popoverOffset}
669
+ {...popoverRest}
670
+ onClose={handlePopoverClose}
671
+ >
672
+ <PopoverTrigger
673
+ asChild
674
+ className={clsx(
675
+ sPickerCommon.picker_popover_trigger,
676
+ popoverTriggerClassName,
677
+ )}
678
+ style={popoverTriggerStyle}
679
+ disabled={disabled || readOnly}
680
+ {...popoverTriggerRest}
681
+ >
682
+ <SelectButton
683
+ id={inputId}
684
+ disabled={disabled || readOnly}
685
+ required={required}
686
+ className={clsx(
687
+ !hasValue && sPickerCommon.picker_value_placeholder,
688
+ selectButton?.className,
689
+ )}
690
+ style={selectButton?.style}
691
+ buttonVariant={selectButton?.buttonVariant}
692
+ buttonSize={selectButton?.buttonSize}
693
+ tooltip={selectButton?.tooltip}
694
+ tooltipPosition={selectButton?.tooltipPosition}
695
+ showClear={hasValue && showClear && !disabled && !readOnly}
696
+ onClear={(e: React.MouseEvent) => {
697
+ e.stopPropagation();
698
+ e.preventDefault();
699
+ commit(undefined);
700
+ onClear?.();
701
+ }}
702
+ showChevron={false}
703
+ iconEnd={
704
+ <Icon aria-hidden="true" clickable>
705
+ <IconCalendar/>
706
+ </Icon>
707
+ }
708
+ >
709
+ {displayText}
710
+ </SelectButton>
711
+ </PopoverTrigger>
712
+
713
+ {!disabled && !readOnly && (
714
+ <PopoverMenu
715
+ className={clsx(
716
+ sPickerCommon.picker_popover_menu_base,
717
+ s.popover_menu,
718
+ popoverMenuClassName,
719
+ )}
720
+ style={popoverMenuStyle}
721
+ {...popoverMenuRest}
722
+ >
723
+ <div className={s.menu_content}>
724
+ {showManualInput && (
725
+ <div className={s.manual_panel}>
726
+ {isRange ? (
727
+ <div className={s.manual_grid}>
728
+ <Input
729
+ type="text"
730
+ value={rangeManualText.from}
731
+ placeholder={`${startPlaceholder} (YYYY-MM-DD)`}
732
+ required={required}
733
+ autoComplete="off"
734
+ onChange={(event) =>
735
+ handleRangeDateInputChange("from", event)
736
+ }
737
+ onBlur={handleRangeDateInputBlur}
738
+ onKeyDown={handleManualInputKeyDown}
739
+ />
740
+ <Input
741
+ type="text"
742
+ value={rangeManualText.to}
743
+ placeholder={`${endPlaceholder} (YYYY-MM-DD)`}
744
+ required={required}
745
+ autoComplete="off"
746
+ onChange={(event) =>
747
+ handleRangeDateInputChange("to", event)
748
+ }
749
+ onBlur={handleRangeDateInputBlur}
750
+ onKeyDown={handleManualInputKeyDown}
751
+ />
752
+ </div>
753
+ ) : (
754
+ <Input
755
+ type="text"
756
+ value={singleManualText}
757
+ placeholder="YYYY-MM-DD"
758
+ required={required}
759
+ autoComplete="off"
760
+ onChange={handleSingleDateInputChange}
761
+ onBlur={handleSingleDateInputBlur}
762
+ onKeyDown={handleManualInputKeyDown}
763
+ />
764
+ )}
765
+ </div>
766
+ )}
767
+
761
768
  <div className={s.calendar_wrap}>
762
769
  {isRange ? (
763
770
  isIndependentRangeMonths ? (
764
771
  <div className={s.range_calendars}>
765
772
  {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
- };
773
+ const month =
774
+ rangeCalendarMonths[index] ??
775
+ addMonthOffset(
776
+ rangeCalendarMonths[0] ?? toMonthStart(new Date()),
777
+ index,
778
+ );
779
+
780
+ return (
781
+ <Calendar
782
+ key={`range-month-panel-${index}`}
783
+ {...calendar}
784
+ className={clsx(s.calendar, calendar?.className)}
785
+ mode="range"
786
+ selected={
787
+ selectedRange
788
+ ? { from: selectedRange.from, to: selectedRange.to }
789
+ : undefined
790
+ }
791
+ onSelect={(next: RDPDateRange | undefined) =>
792
+ handleRangeSelect(
793
+ next ? { from: next.from, to: next.to } : undefined,
794
+ )
795
+ }
796
+ month={month}
797
+ onMonthChange={(nextMonth) =>
798
+ handleRangePanelMonthChange(index, nextMonth)
799
+ }
800
+ numberOfMonths={1}
801
+ weekStartsOn={weekStartsOn}
802
+ showWeekNumber={
803
+ showWeekNumber || calendar?.showWeekNumber
804
+ }
805
+ captionLayout={calendar?.captionLayout ?? captionLayout}
806
+ disabled={disabledMatcher}
807
+ required={required as false}
808
+ />
809
+ );
810
+ })}
811
+ </div>
812
+ ) : (
813
+ <Calendar
814
+ {...calendar}
815
+ className={clsx(s.calendar, calendar?.className)}
816
+ mode="range"
817
+ selected={
818
+ selectedRange
819
+ ? { from: selectedRange.from, to: selectedRange.to }
820
+ : undefined
821
+ }
822
+ onSelect={(next: RDPDateRange | undefined) =>
823
+ handleRangeSelect(
824
+ next ? { from: next.from, to: next.to } : undefined,
825
+ )
826
+ }
827
+ month={calendarMonth}
828
+ onMonthChange={(nextMonth) =>
829
+ setCalendarMonth(toMonthStart(nextMonth))
830
+ }
831
+ defaultMonth={
832
+ calendar?.defaultMonth ??
833
+ (selectedRange?.from ?? selectedRange?.to)
834
+ }
835
+ numberOfMonths={1}
836
+ weekStartsOn={weekStartsOn}
837
+ showWeekNumber={showWeekNumber || calendar?.showWeekNumber}
838
+ captionLayout={calendar?.captionLayout ?? captionLayout}
839
+ disabled={disabledMatcher}
840
+ required={required as false}
841
+ />
842
+ )
843
+ ) : (
844
+ <Calendar
845
+ {...calendar}
846
+ className={clsx(s.calendar, calendar?.className)}
847
+ mode="single"
848
+ selected={selectedSingle}
849
+ onSelect={handleSingleSelect}
850
+ month={calendarMonth}
851
+ onMonthChange={(nextMonth) =>
852
+ setCalendarMonth(toMonthStart(nextMonth))
853
+ }
854
+ defaultMonth={calendar?.defaultMonth ?? selectedSingle}
855
+ numberOfMonths={
856
+ numberOfMonths ?? calendar?.numberOfMonths ?? 1
857
+ }
858
+ weekStartsOn={weekStartsOn}
859
+ showWeekNumber={showWeekNumber || calendar?.showWeekNumber}
860
+ captionLayout={calendar?.captionLayout ?? captionLayout}
861
+ disabled={disabledMatcher}
862
+ required={required as false}
863
+ />
864
+ )}
865
+ </div>
866
+
867
+ {showTime && (
868
+ <div className={s.time_panel}>
869
+ {isRange ? (
870
+ <div className={s.range_time_grid}>
871
+ <TimePicker
872
+ placeholder={startPlaceholder}
873
+ value={timeValueRangeFrom || undefined}
874
+ minuteStep={timeMinuteStep}
875
+ timeFormat={timeFormat}
876
+ required={required}
877
+ disabled={!selectedRange?.from}
878
+ showClear={false}
879
+ showActionFooter={false}
880
+ onChange={(value) => handleRangeTimeChange("from", value)}
881
+ popover={resolvedTimePickerPopover}
882
+ selectButton={{ buttonVariant: "none" }}
883
+ />
884
+ <TimePicker
885
+ placeholder={endPlaceholder}
886
+ value={timeValueRangeTo || undefined}
887
+ minuteStep={timeMinuteStep}
888
+ timeFormat={timeFormat}
889
+ required={required}
890
+ disabled={!selectedRange?.to}
891
+ showClear={false}
892
+ showActionFooter={false}
893
+ onChange={(value) => handleRangeTimeChange("to", value)}
894
+ popover={resolvedTimePickerPopover}
895
+ selectButton={{ buttonVariant: "none" }}
896
+ />
897
+ </div>
898
+ ) : (
899
+ <TimePicker
900
+ placeholder="Time"
901
+ value={timeValueSingle || undefined}
902
+ minuteStep={timeMinuteStep}
903
+ timeFormat={timeFormat}
904
+ required={required}
905
+ disabled={!selectedSingle}
906
+ showClear={false}
907
+ showActionFooter={false}
908
+ onChange={handleSingleTimeChange}
909
+ popover={resolvedTimePickerPopover}
910
+ selectButton={{ buttonVariant: "none" }}
911
+ />
912
+ )}
913
+ </div>
914
+ )}
915
+
916
+ {showActionFooter && (
917
+ <div className={sPickerCommon.picker_actions_footer}>
918
+ <Button
919
+ type="button"
920
+ variant="ghost"
921
+ size="sm"
922
+ disabled={!hasValue}
923
+ onClick={() => {
924
+ if (disabled || readOnly) return;
925
+ commit(undefined);
926
+ onClear?.();
927
+ }}
928
+ >
929
+ Clear
930
+ </Button>
931
+
932
+ <Button
933
+ type="button"
934
+ variant="primary"
935
+ size="sm"
936
+ onClick={() => {
937
+ setIsOpen(false);
938
+ }}
939
+ >
940
+ Done
941
+ </Button>
942
+ </div>
943
+ )}
944
+ </div>
945
+ </PopoverMenu>
946
+ )}
947
+ </Popover>
948
+
949
+ <FieldMessage errors={errors} info={info} />
950
+ </div>
951
+ );
952
+ };