@xsolla/xui-date-picker 0.184.0 → 0.185.1

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.
package/web/index.mjs CHANGED
@@ -8,7 +8,14 @@ import {
8
8
  } from "@xsolla/xui-calendar";
9
9
 
10
10
  // src/DatePicker.tsx
11
- import { forwardRef, useCallback, useEffect, useRef, useState } from "react";
11
+ import {
12
+ forwardRef,
13
+ useCallback,
14
+ useEffect,
15
+ useMemo,
16
+ useRef,
17
+ useState
18
+ } from "react";
12
19
 
13
20
  // ../../foundation/primitives-web/src/Box.tsx
14
21
  import React2 from "react";
@@ -267,9 +274,257 @@ var isNative = false;
267
274
 
268
275
  // src/DatePicker.tsx
269
276
  import { Input } from "@xsolla/xui-input";
270
- import { format } from "date-fns";
277
+ import { parse, isValid } from "date-fns";
271
278
  import { Calendar, DualCalendar } from "@xsolla/xui-calendar";
279
+
280
+ // src/utils.ts
281
+ import { format } from "date-fns";
282
+ import * as locales from "date-fns/locale";
283
+ var defaultLocale = "enUS";
284
+ function formatDate(date, formatStr, locale = defaultLocale) {
285
+ const localeObj = locales[locale] || locales[defaultLocale];
286
+ return format(date, formatStr, {
287
+ locale: localeObj
288
+ });
289
+ }
290
+ var SEGMENT_WIDTH = {
291
+ day: 2,
292
+ month: 2,
293
+ year: 4
294
+ };
295
+ var SEGMENT_TOKEN = {
296
+ day: "dd",
297
+ month: "mm",
298
+ year: "yyyy"
299
+ };
300
+ function parseDateFormat(format2) {
301
+ const tokens = [];
302
+ let i = 0;
303
+ while (i < format2.length) {
304
+ const c = format2[i];
305
+ if (c === "d" || c === "M" || c === "y") {
306
+ while (i < format2.length && format2[i] === c) i++;
307
+ tokens.push({
308
+ kind: "segment",
309
+ type: c === "d" ? "day" : c === "M" ? "month" : "year"
310
+ });
311
+ } else {
312
+ let text = "";
313
+ while (i < format2.length && !"dMy".includes(format2[i])) {
314
+ text += format2[i];
315
+ i++;
316
+ }
317
+ tokens.push({ kind: "literal", text });
318
+ }
319
+ }
320
+ return tokens;
321
+ }
322
+ var EMPTY_PARTS = { day: null, month: null, year: null };
323
+ function datePartsFromDate(date) {
324
+ if (!date) return { ...EMPTY_PARTS };
325
+ return {
326
+ day: date.getDate(),
327
+ month: date.getMonth() + 1,
328
+ year: date.getFullYear()
329
+ };
330
+ }
331
+ function datePartsToDate(parts) {
332
+ const { day, month, year } = parts;
333
+ if (day == null || month == null || year == null) return null;
334
+ const date = new Date(year, month - 1, day);
335
+ if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {
336
+ return null;
337
+ }
338
+ return date;
339
+ }
340
+ function isDatePartsComplete(parts) {
341
+ return parts.day != null && parts.month != null && parts.year != null;
342
+ }
343
+ function clampDatePart(type, value, parts) {
344
+ if (type === "month") return Math.min(12, Math.max(1, value));
345
+ if (type === "year") return Math.max(1, value);
346
+ const year = parts.year ?? (/* @__PURE__ */ new Date()).getFullYear();
347
+ const monthIndex = (parts.month ?? (/* @__PURE__ */ new Date()).getMonth() + 1) - 1;
348
+ return Math.min(daysInMonth(year, monthIndex), Math.max(1, value));
349
+ }
350
+ function stepDatePart(type, current, delta, parts) {
351
+ if (type === "month") return ((current - 1 + delta) % 12 + 12) % 12 + 1;
352
+ if (type === "year") return Math.max(1, current + delta);
353
+ const year = parts.year ?? (/* @__PURE__ */ new Date()).getFullYear();
354
+ const monthIndex = (parts.month ?? (/* @__PURE__ */ new Date()).getMonth() + 1) - 1;
355
+ const dim = daysInMonth(year, monthIndex);
356
+ return ((current - 1 + delta) % dim + dim) % dim + 1;
357
+ }
358
+ function buildSegmentedValue(format2, parts, active) {
359
+ const tokens = parseDateFormat(format2);
360
+ let text = "";
361
+ const ranges = [];
362
+ for (const token of tokens) {
363
+ if (token.kind === "literal") {
364
+ text += token.text;
365
+ continue;
366
+ }
367
+ const { type } = token;
368
+ const width = SEGMENT_WIDTH[type];
369
+ const value = parts[type];
370
+ let rendered;
371
+ let filled;
372
+ if (active && active.type === type && active.buffer.length > 0) {
373
+ rendered = active.buffer;
374
+ filled = false;
375
+ } else if (value != null) {
376
+ rendered = String(value).padStart(width, "0");
377
+ filled = true;
378
+ } else {
379
+ rendered = SEGMENT_TOKEN[type];
380
+ filled = false;
381
+ }
382
+ const start = text.length;
383
+ text += rendered;
384
+ ranges.push({ type, start, end: text.length, filled });
385
+ }
386
+ return { text, ranges };
387
+ }
388
+ function isSegmentComplete(type, buffer) {
389
+ const width = SEGMENT_WIDTH[type];
390
+ if (buffer.length >= width) return true;
391
+ if (buffer.length === 1 && type !== "year") {
392
+ const first = Number(buffer);
393
+ return type === "month" ? first > 1 : first > 3;
394
+ }
395
+ return false;
396
+ }
397
+ function daysInMonth(year, monthIndex) {
398
+ return new Date(year, monthIndex + 1, 0).getDate();
399
+ }
400
+ function getDateSegments(format2, value) {
401
+ const segments = [];
402
+ let fi = 0;
403
+ let vi = 0;
404
+ while (fi < format2.length && vi < value.length) {
405
+ const fc = format2[fi];
406
+ if (fc === "d" || fc === "M" || fc === "y") {
407
+ while (fi < format2.length && format2[fi] === fc) fi++;
408
+ const start = vi;
409
+ while (vi < value.length && value[vi] >= "0" && value[vi] <= "9") vi++;
410
+ if (vi > start) {
411
+ segments.push({
412
+ type: fc === "d" ? "day" : fc === "M" ? "month" : "year",
413
+ start,
414
+ end: vi
415
+ });
416
+ }
417
+ } else {
418
+ fi++;
419
+ vi++;
420
+ }
421
+ }
422
+ return segments;
423
+ }
424
+ function stepDateSegment(date, type, delta) {
425
+ const year = date.getFullYear();
426
+ const month = date.getMonth();
427
+ const day = date.getDate();
428
+ if (type === "year") {
429
+ const nextYear = year + delta;
430
+ return new Date(
431
+ nextYear,
432
+ month,
433
+ Math.min(day, daysInMonth(nextYear, month))
434
+ );
435
+ }
436
+ if (type === "month") {
437
+ const nextMonth = ((month + delta) % 12 + 12) % 12;
438
+ return new Date(
439
+ year,
440
+ nextMonth,
441
+ Math.min(day, daysInMonth(year, nextMonth))
442
+ );
443
+ }
444
+ const dim = daysInMonth(year, month);
445
+ const nextDay = ((day - 1 + delta) % dim + dim) % dim + 1;
446
+ return new Date(year, month, nextDay);
447
+ }
448
+
449
+ // src/DatePicker.tsx
272
450
  import { jsx as jsx2, jsxs } from "react/jsx-runtime";
451
+ var PLACEMENT_OFFSET = 4;
452
+ var DEFAULT_DATE_FORMAT = "MM/dd/yyyy";
453
+ var RANGE_SEPARATOR = /\s*[-–—]\s*/;
454
+ var RANGE_DISPLAY_SEP = " - ";
455
+ var CalendarFieldIcon = () => /* @__PURE__ */ jsx2(
456
+ "svg",
457
+ {
458
+ viewBox: "0 0 24 24",
459
+ width: "100%",
460
+ height: "100%",
461
+ style: { fill: "currentColor", stroke: "none" },
462
+ children: /* @__PURE__ */ jsx2(
463
+ "path",
464
+ {
465
+ fillOpacity: 0.6,
466
+ d: "M8 4H16V2H18V4H21.5C21.7761 4 22 4.22386 22 4.5V19.5C22 19.7761 21.7761 20 21.5 20H2.5C2.22386 20 2 19.7761 2 19.5V4.5C2 4.22386 2.22386 4 2.5 4H6V2H8V4ZM4 18H20V10H4V18ZM4 8H20V6H4V8Z"
467
+ }
468
+ )
469
+ }
470
+ );
471
+ var FALLBACK_PANEL_SIZE = { width: 320, height: 360 };
472
+ var resolveAutoPlacement = (container, panel, viewport, offset) => {
473
+ const spaceBelow = viewport.height - container.bottom;
474
+ const spaceAbove = container.top;
475
+ const spaceRight = viewport.width - container.right;
476
+ const spaceLeft = container.left;
477
+ const neededVertical = panel.height + offset;
478
+ const neededHorizontal = panel.width + offset;
479
+ if (spaceBelow >= neededVertical) return "bottom";
480
+ if (spaceAbove >= neededVertical) return "top";
481
+ if (spaceLeft >= neededHorizontal) return "left";
482
+ if (spaceRight >= neededHorizontal) return "right";
483
+ const gaps = [
484
+ { side: "bottom", space: spaceBelow },
485
+ { side: "top", space: spaceAbove },
486
+ { side: "left", space: spaceLeft },
487
+ { side: "right", space: spaceRight }
488
+ ];
489
+ return gaps.reduce(
490
+ (best, current) => current.space > best.space ? current : best
491
+ ).side;
492
+ };
493
+ var parsePlacement = (placement) => {
494
+ const [side, align] = placement.split("-");
495
+ return { side, align: align ?? null };
496
+ };
497
+ var alignFromDropdownPosition = (dropdownPosition) => dropdownPosition === "right" ? "end" : "start";
498
+ var resolveForcedLayout = (placement, dropdownPosition) => {
499
+ const { side, align } = parsePlacement(placement);
500
+ if (align) return { side, align };
501
+ if (side === "top" || side === "bottom")
502
+ return { side, align: alignFromDropdownPosition(dropdownPosition) };
503
+ return { side, align: "start" };
504
+ };
505
+ var resolveAlignForSide = (side, container, panel, viewport, preferred) => {
506
+ if (preferred === "center") return "center";
507
+ const isVerticalSide = side === "left" || side === "right";
508
+ const fits = (align) => {
509
+ if (isVerticalSide) {
510
+ return align === "start" ? container.top + panel.height <= viewport.height : container.bottom - panel.height >= 0;
511
+ }
512
+ return align === "start" ? container.left + panel.width <= viewport.width : container.right - panel.width >= 0;
513
+ };
514
+ if (fits(preferred)) return preferred;
515
+ const opposite = preferred === "start" ? "end" : "start";
516
+ return fits(opposite) ? opposite : preferred;
517
+ };
518
+ var getPlacementStyle = (side, align, offset) => {
519
+ if (side === "top" || side === "bottom") {
520
+ const vertical2 = side === "bottom" ? { top: "100%", bottom: "auto", marginTop: offset, marginBottom: 0 } : { top: "auto", bottom: "100%", marginTop: 0, marginBottom: offset };
521
+ const horizontal2 = align === "center" ? { left: "50%", right: "auto", transform: "translateX(-50%)" } : align === "end" ? { left: "auto", right: 0 } : { left: 0, right: "auto" };
522
+ return { marginLeft: 0, marginRight: 0, ...vertical2, ...horizontal2 };
523
+ }
524
+ const horizontal = side === "right" ? { left: "100%", right: "auto", marginLeft: offset, marginRight: 0 } : { left: "auto", right: "100%", marginLeft: 0, marginRight: offset };
525
+ const vertical = align === "center" ? { top: "50%", bottom: "auto", transform: "translateY(-50%)" } : align === "end" ? { top: "auto", bottom: 0 } : { top: 0, bottom: "auto" };
526
+ return { marginTop: 0, marginBottom: 0, ...horizontal, ...vertical };
527
+ };
273
528
  var DatePicker = forwardRef(
274
529
  ({
275
530
  onChange,
@@ -280,63 +535,126 @@ var DatePicker = forwardRef(
280
535
  startDate,
281
536
  endDate,
282
537
  selectedDate,
538
+ minDate,
539
+ maxDate,
283
540
  placeholder,
541
+ dateFormat = DEFAULT_DATE_FORMAT,
284
542
  backgroundColor,
285
543
  testID,
286
544
  chips,
287
545
  activeChip,
288
546
  onChipSelect,
547
+ showChipLabel = true,
289
548
  dropdownPosition = "left",
549
+ placement,
290
550
  themeMode,
291
551
  themeProductContext,
292
552
  overlayThemeMode,
293
553
  overlayThemeProductContext,
554
+ iconRight = /* @__PURE__ */ jsx2(CalendarFieldIcon, {}),
555
+ iconLeft,
556
+ error,
557
+ errorMessage,
558
+ readOnly,
559
+ onFocus: onFocusProp,
560
+ onBlur: onBlurProp,
561
+ onClick: onClickProp,
562
+ onKeyDown: onKeyDownProp,
563
+ onPaste: onPasteProp,
294
564
  ...rest
295
565
  }, ref) => {
566
+ const editable = !readOnly && !rest.disabled;
296
567
  const isDual = variant === "dual";
297
568
  const isRange = isDual || selectsRange;
569
+ const propGroups = useCallback(
570
+ () => isRange ? [datePartsFromDate(startDate), datePartsFromDate(endDate)] : [datePartsFromDate(selectedDate)],
571
+ [isRange, startDate, endDate, selectedDate]
572
+ );
298
573
  const [open, setOpen] = useState(false);
299
- const [inputValue, setInputValue] = useState("");
574
+ const [groups, setGroups] = useState(propGroups);
575
+ const [edit, setEdit] = useState(null);
576
+ const [focused, setFocused] = useState(false);
577
+ const [activeSegment, setActiveSegment] = useState(null);
300
578
  const [internalChip, setInternalChip] = useState(null);
579
+ const [parseError, setParseError] = useState(null);
580
+ const [layout, setLayout] = useState(
581
+ () => placement ? resolveForcedLayout(placement, dropdownPosition) : { side: "bottom", align: alignFromDropdownPosition(dropdownPosition) }
582
+ );
301
583
  const containerRef = useRef(null);
584
+ const calendarRef = useRef(null);
585
+ const inputRef = useRef(null);
302
586
  const chipSelectionRef = useRef(false);
587
+ const suppressReopenRef = useRef(false);
303
588
  const activeChipValue = activeChip !== void 0 ? activeChip : internalChip;
304
- const formatDateForDisplay = (date) => {
305
- if (!date) return "";
306
- try {
307
- return format(date, "MM/dd/yyyy");
308
- } catch {
309
- return "";
589
+ const chipLabel = showChipLabel && activeChipValue && chips ? chips.find((c) => c.value === activeChipValue)?.label : void 0;
590
+ const isWithinBounds = (date) => {
591
+ const day = new Date(
592
+ date.getFullYear(),
593
+ date.getMonth(),
594
+ date.getDate()
595
+ ).getTime();
596
+ if (minDate) {
597
+ const min = new Date(
598
+ minDate.getFullYear(),
599
+ minDate.getMonth(),
600
+ minDate.getDate()
601
+ ).getTime();
602
+ if (day < min) return false;
310
603
  }
311
- };
312
- const getDisplayValue = useCallback(() => {
313
- if (activeChipValue && chips) {
314
- const chip = chips.find((c) => c.value === activeChipValue);
315
- if (chip) return chip.label;
604
+ if (maxDate) {
605
+ const max = new Date(
606
+ maxDate.getFullYear(),
607
+ maxDate.getMonth(),
608
+ maxDate.getDate()
609
+ ).getTime();
610
+ if (day > max) return false;
316
611
  }
317
- if (isRange) {
318
- if (startDate && endDate) {
319
- return `${formatDateForDisplay(startDate)} - ${formatDateForDisplay(endDate)}`;
320
- } else if (startDate) {
321
- return formatDateForDisplay(startDate);
322
- }
323
- return "";
324
- } else {
325
- return formatDateForDisplay(selectedDate);
612
+ return true;
613
+ };
614
+ const composeField = () => {
615
+ const build = (group) => buildSegmentedValue(
616
+ dateFormat,
617
+ groups[group] ?? { day: null, month: null, year: null },
618
+ edit && edit.group === group ? { type: edit.type, buffer: edit.buffer } : void 0
619
+ );
620
+ if (!isRange) {
621
+ const { text: text2, ranges } = build(0);
622
+ return { text: text2, segments: ranges.map((r) => ({ ...r, group: 0 })) };
326
623
  }
327
- }, [isRange, startDate, endDate, selectedDate, activeChipValue, chips]);
328
- useEffect(() => {
329
- setInputValue(getDisplayValue());
330
- }, [getDisplayValue]);
331
- const handleInputChange = (text) => {
332
- setInputValue(text);
624
+ const a = build(0);
625
+ const b = build(1);
626
+ const text = `${a.text}${RANGE_DISPLAY_SEP}${b.text}`;
627
+ const offset = a.text.length + RANGE_DISPLAY_SEP.length;
628
+ return {
629
+ text,
630
+ segments: [
631
+ ...a.ranges.map((r) => ({ ...r, group: 0 })),
632
+ ...b.ranges.map((r) => ({
633
+ ...r,
634
+ group: 1,
635
+ start: r.start + offset,
636
+ end: r.end + offset
637
+ }))
638
+ ]
639
+ };
333
640
  };
334
- const handleChipSelect = (value) => {
335
- chipSelectionRef.current = true;
336
- setInternalChip(value);
337
- onChipSelect?.(value);
641
+ const composed = composeField();
642
+ const hasAnyFilled = groups.some(
643
+ (g) => g.day != null || g.month != null || g.year != null
644
+ );
645
+ const showMask = hasAnyFilled || focused && isWeb;
646
+ const inputValue = chipLabel ?? (showMask ? composed.text : "");
647
+ const orderedSegments = () => composeField().segments;
648
+ const firstSegmentKey = () => {
649
+ const first = orderedSegments()[0];
650
+ return first ? { group: first.group, type: first.type } : null;
338
651
  };
339
- const handleDateChange = (date) => {
652
+ const segmentKeyAtCaret = (caret) => {
653
+ const segments = orderedSegments();
654
+ const seg = segments.find((s) => caret >= s.start && caret <= s.end) ?? segments.find((s) => caret < s.start) ?? segments[segments.length - 1];
655
+ return seg ? { group: seg.group, type: seg.type } : null;
656
+ };
657
+ const applySelection = (date, closeOnComplete = true) => {
340
658
  if (!chipSelectionRef.current) {
341
659
  setInternalChip(null);
342
660
  onChipSelect?.(null);
@@ -344,13 +662,278 @@ var DatePicker = forwardRef(
344
662
  chipSelectionRef.current = false;
345
663
  if (!isRange) {
346
664
  onChange?.(date);
347
- setOpen(false);
665
+ if (closeOnComplete) setOpen(false);
348
666
  } else {
349
667
  const range = date;
350
668
  onChange?.(range);
351
- if (range[0] && range[1]) setOpen(false);
669
+ if (closeOnComplete && range[0] && range[1]) setOpen(false);
670
+ }
671
+ };
672
+ const emit = (next) => {
673
+ if (!isRange) {
674
+ const date = datePartsToDate(next[0]);
675
+ if (date && isWithinBounds(date)) applySelection(date, false);
676
+ return;
677
+ }
678
+ const start = datePartsToDate(next[0]);
679
+ const end = datePartsToDate(next[1]);
680
+ if (start && !isWithinBounds(start)) return;
681
+ if (end && !isWithinBounds(end)) return;
682
+ if (start && end && start.getTime() > end.getTime()) {
683
+ setParseError("End date must be after start date");
684
+ return;
685
+ }
686
+ if (start || end) applySelection([start, end], false);
687
+ };
688
+ const commitPart = (group, type, rawValue) => {
689
+ const clamped = clampDatePart(type, rawValue, groups[group]);
690
+ const next = groups.map((g, i) => {
691
+ if (i !== group) return g;
692
+ const updated = { ...g, [type]: clamped };
693
+ if (type !== "day" && updated.day != null) {
694
+ updated.day = clampDatePart("day", updated.day, updated);
695
+ }
696
+ return updated;
697
+ });
698
+ const assembled = datePartsToDate(next[group]);
699
+ if (assembled && !isWithinBounds(assembled)) {
700
+ setParseError("Date must be within the allowed range");
701
+ return null;
702
+ }
703
+ setParseError(null);
704
+ setGroups(next);
705
+ emit(next);
706
+ return clamped;
707
+ };
708
+ const commitBuffer = () => {
709
+ if (!edit) return;
710
+ commitPart(edit.group, edit.type, Number(edit.buffer));
711
+ setEdit(null);
712
+ };
713
+ const defaultPart = (type) => {
714
+ const now = /* @__PURE__ */ new Date();
715
+ if (type === "day") return now.getDate();
716
+ if (type === "month") return now.getMonth() + 1;
717
+ return now.getFullYear();
718
+ };
719
+ const advance = (active) => {
720
+ const segments = orderedSegments();
721
+ const index2 = segments.findIndex(
722
+ (s) => s.group === active.group && s.type === active.type
723
+ );
724
+ const next = segments[index2 + 1];
725
+ setActiveSegment(next ? { group: next.group, type: next.type } : active);
726
+ };
727
+ const handleDigit = (digit) => {
728
+ const active = activeSegment ?? firstSegmentKey();
729
+ if (!active) return;
730
+ const sameActive = edit && edit.group === active.group && edit.type === active.type;
731
+ const buffer = (sameActive ? edit.buffer : "") + digit;
732
+ if (isSegmentComplete(active.type, buffer)) {
733
+ const committed = commitPart(active.group, active.type, Number(buffer));
734
+ setEdit(null);
735
+ if (committed !== null) advance(active);
736
+ } else {
737
+ setEdit({ group: active.group, type: active.type, buffer });
738
+ }
739
+ };
740
+ const handleArrowStep = (e, delta) => {
741
+ e.preventDefault();
742
+ const active = activeSegment ?? firstSegmentKey();
743
+ if (!active) return;
744
+ const parts = groups[active.group];
745
+ const current = parts[active.type];
746
+ const value = current == null ? defaultPart(active.type) : stepDatePart(active.type, current, delta, parts);
747
+ commitPart(active.group, active.type, value);
748
+ setEdit(null);
749
+ if (!activeSegment) setActiveSegment(active);
750
+ };
751
+ const handleArrowMove = (e, direction) => {
752
+ const segments = orderedSegments();
753
+ if (!segments.length) return;
754
+ const active = activeSegment ?? firstSegmentKey();
755
+ if (!active) return;
756
+ let index2 = segments.findIndex(
757
+ (s) => s.group === active.group && s.type === active.type
758
+ );
759
+ if (index2 === -1) index2 = direction > 0 ? -1 : segments.length;
760
+ const next = index2 + direction;
761
+ if (next < 0 || next >= segments.length) return;
762
+ e.preventDefault();
763
+ if (edit) {
764
+ commitPart(edit.group, edit.type, Number(edit.buffer));
765
+ setEdit(null);
766
+ }
767
+ const target = segments[next];
768
+ setActiveSegment({ group: target.group, type: target.type });
769
+ };
770
+ const handleBackspace = (e) => {
771
+ e.preventDefault();
772
+ const active = activeSegment ?? firstSegmentKey();
773
+ if (!active) return;
774
+ const sameActive = edit && edit.group === active.group && edit.type === active.type;
775
+ if (sameActive && edit.buffer.length > 0) {
776
+ const buffer = edit.buffer.slice(0, -1);
777
+ setEdit(buffer ? { ...edit, buffer } : null);
778
+ return;
779
+ }
780
+ const next = groups.map(
781
+ (g, i) => i === active.group ? { ...g, [active.type]: null } : g
782
+ );
783
+ setGroups(next);
784
+ setEdit(null);
785
+ setParseError(null);
786
+ emit(next);
787
+ const segments = orderedSegments();
788
+ const index2 = segments.findIndex(
789
+ (s) => s.group === active.group && s.type === active.type
790
+ );
791
+ const prev = segments[index2 - 1];
792
+ if (prev) setActiveSegment({ group: prev.group, type: prev.type });
793
+ };
794
+ const handleKeyDown = (e) => {
795
+ onKeyDownProp?.(e);
796
+ if (e.defaultPrevented) return;
797
+ if (e.key === "Escape") {
798
+ setOpen(false);
799
+ return;
352
800
  }
801
+ if (!isWeb || !editable) return;
802
+ if (e.key === "Enter") {
803
+ commitBuffer();
804
+ setOpen(false);
805
+ return;
806
+ }
807
+ if (e.key === "ArrowUp") return handleArrowStep(e, 1);
808
+ if (e.key === "ArrowDown") return handleArrowStep(e, -1);
809
+ if (e.key === "ArrowLeft") return handleArrowMove(e, -1);
810
+ if (e.key === "ArrowRight") return handleArrowMove(e, 1);
811
+ if (e.key === "Backspace") return handleBackspace(e);
812
+ if (/^[0-9]$/.test(e.key)) {
813
+ e.preventDefault();
814
+ handleDigit(e.key);
815
+ return;
816
+ }
817
+ if (e.key.length === 1 && !e.metaKey && !e.ctrlKey) e.preventDefault();
818
+ };
819
+ const handleFieldFocus = (e) => {
820
+ onFocusProp?.(e);
821
+ if (suppressReopenRef.current) {
822
+ suppressReopenRef.current = false;
823
+ return;
824
+ }
825
+ setFocused(true);
826
+ setOpen(true);
827
+ if (isWeb && editable) setActiveSegment(firstSegmentKey());
828
+ };
829
+ const handleFieldClick = (e) => {
830
+ onClickProp?.(e);
831
+ if (!isWeb || !editable || !hasAnyFilled) return;
832
+ const input = inputRef.current;
833
+ if (!input) return;
834
+ const key = segmentKeyAtCaret(input.selectionStart ?? 0);
835
+ if (key) setActiveSegment(key);
836
+ };
837
+ const handleFieldBlur = (e) => {
838
+ onBlurProp?.(e);
839
+ setFocused(false);
840
+ setActiveSegment(null);
841
+ if (!editable) return;
842
+ let next = groups;
843
+ if (edit) {
844
+ const clamped = clampDatePart(
845
+ edit.type,
846
+ Number(edit.buffer),
847
+ groups[edit.group]
848
+ );
849
+ next = groups.map(
850
+ (g, i) => i === edit.group ? { ...g, [edit.type]: clamped } : g
851
+ );
852
+ }
853
+ const props = propGroups();
854
+ next = next.map((g, i) => isDatePartsComplete(g) ? g : props[i]);
855
+ setEdit(null);
856
+ setParseError(null);
857
+ setGroups(next);
858
+ emit(next);
859
+ };
860
+ const parseOne = (token) => {
861
+ const trimmed = token.trim();
862
+ if (!trimmed) return null;
863
+ const parsed = parse(trimmed, dateFormat, /* @__PURE__ */ new Date());
864
+ return isValid(parsed) ? parsed : null;
353
865
  };
866
+ const handlePaste = (e) => {
867
+ onPasteProp?.(e);
868
+ if (e.defaultPrevented) return;
869
+ if (!isWeb || !editable) return;
870
+ const text = e.clipboardData?.getData("text") ?? "";
871
+ if (!text.trim()) return;
872
+ e.preventDefault();
873
+ let next;
874
+ if (isRange) {
875
+ const [startText, endText] = text.split(RANGE_SEPARATOR);
876
+ const start = parseOne(startText ?? "");
877
+ const end = parseOne(endText ?? "");
878
+ if (!start) {
879
+ setParseError(
880
+ `Enter a valid date range (${dateFormat} - ${dateFormat})`
881
+ );
882
+ return;
883
+ }
884
+ next = [datePartsFromDate(start), datePartsFromDate(end)];
885
+ } else {
886
+ const date = parseOne(text);
887
+ if (!date) {
888
+ setParseError(`Enter a valid date (${dateFormat})`);
889
+ return;
890
+ }
891
+ next = [datePartsFromDate(date)];
892
+ }
893
+ const outOfBounds = next.map(datePartsToDate).some((d) => d instanceof Date && !isWithinBounds(d));
894
+ if (outOfBounds) {
895
+ setParseError("Date must be within the allowed range");
896
+ return;
897
+ }
898
+ setParseError(null);
899
+ setEdit(null);
900
+ setGroups(next);
901
+ emit(next);
902
+ };
903
+ const handleChipSelect = (value) => {
904
+ chipSelectionRef.current = true;
905
+ setInternalChip(value);
906
+ onChipSelect?.(value);
907
+ };
908
+ const handleDateChange = (date) => applySelection(date);
909
+ const propKey = `${selectedDate?.getTime() ?? ""}|${startDate?.getTime() ?? ""}|${endDate?.getTime() ?? ""}|${isRange}`;
910
+ useEffect(() => {
911
+ if (focused) return;
912
+ setGroups(propGroups());
913
+ setEdit(null);
914
+ setParseError(null);
915
+ }, [propKey]);
916
+ useEffect(() => {
917
+ if (!isWeb || !activeSegment) return;
918
+ const input = inputRef.current;
919
+ if (!input) return;
920
+ const segment = composeField().segments.find(
921
+ (s) => s.group === activeSegment.group && s.type === activeSegment.type
922
+ );
923
+ if (!segment) return;
924
+ let rafId = 0;
925
+ let attempts = 0;
926
+ const apply = () => {
927
+ if (document.activeElement !== input) return;
928
+ if (input.value === inputValue) {
929
+ input.setSelectionRange(segment.start, segment.end);
930
+ } else if (attempts++ < 5) {
931
+ rafId = window.requestAnimationFrame(apply);
932
+ }
933
+ };
934
+ rafId = window.requestAnimationFrame(apply);
935
+ return () => window.cancelAnimationFrame(rafId);
936
+ }, [activeSegment, inputValue]);
354
937
  useEffect(() => {
355
938
  if (isNative) return;
356
939
  const handleClickOutside = (event) => {
@@ -361,16 +944,86 @@ var DatePicker = forwardRef(
361
944
  document.addEventListener("mousedown", handleClickOutside);
362
945
  return () => document.removeEventListener("mousedown", handleClickOutside);
363
946
  }, []);
947
+ useEffect(() => {
948
+ if (!open || !isWeb) return;
949
+ const handleEscape = (event) => {
950
+ if (event.key !== "Escape") return;
951
+ setOpen(false);
952
+ const input = inputRef.current;
953
+ if (input && document.activeElement !== input) {
954
+ suppressReopenRef.current = true;
955
+ input.focus?.();
956
+ }
957
+ };
958
+ document.addEventListener("keydown", handleEscape);
959
+ return () => document.removeEventListener("keydown", handleEscape);
960
+ }, [open]);
961
+ useEffect(() => {
962
+ if (!open || !isWeb) return;
963
+ if (placement) {
964
+ const next = resolveForcedLayout(placement, dropdownPosition);
965
+ setLayout(
966
+ (prev) => prev.side === next.side && prev.align === next.align ? prev : next
967
+ );
968
+ return;
969
+ }
970
+ const compute = () => {
971
+ const container = containerRef.current;
972
+ if (!container) return;
973
+ const containerRect = container.getBoundingClientRect();
974
+ const panelRect = calendarRef.current ? calendarRef.current.getBoundingClientRect() : FALLBACK_PANEL_SIZE;
975
+ const panel = { width: panelRect.width, height: panelRect.height };
976
+ const viewport = {
977
+ width: window.innerWidth,
978
+ height: window.innerHeight
979
+ };
980
+ const side = resolveAutoPlacement(
981
+ containerRect,
982
+ panel,
983
+ viewport,
984
+ PLACEMENT_OFFSET
985
+ );
986
+ const preferred = side === "top" || side === "bottom" ? alignFromDropdownPosition(dropdownPosition) : "start";
987
+ const align = resolveAlignForSide(
988
+ side,
989
+ containerRect,
990
+ panel,
991
+ viewport,
992
+ preferred
993
+ );
994
+ setLayout(
995
+ (prev) => prev.side === side && prev.align === align ? prev : { side, align }
996
+ );
997
+ };
998
+ const rafId = window.requestAnimationFrame(compute);
999
+ const handleResize = () => compute();
1000
+ window.addEventListener("resize", handleResize);
1001
+ return () => {
1002
+ window.cancelAnimationFrame(rafId);
1003
+ window.removeEventListener("resize", handleResize);
1004
+ };
1005
+ }, [open, placement, dropdownPosition]);
364
1006
  const overlayThemeProps = {
365
1007
  themeMode: overlayThemeMode ?? themeMode,
366
1008
  themeProductContext: overlayThemeProductContext ?? themeProductContext
367
1009
  };
1010
+ const activeParts = activeSegment ? groups[activeSegment.group] : null;
1011
+ const editingAnchor = activeParts && activeParts.month != null && activeParts.year != null ? new Date(activeParts.year, activeParts.month - 1, 1) : null;
1012
+ const viewAnchor = editingAnchor ?? selectedDate ?? startDate ?? endDate ?? null;
1013
+ const viewMonth = useMemo(
1014
+ () => viewAnchor ? new Date(viewAnchor.getFullYear(), viewAnchor.getMonth(), 1) : void 0,
1015
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1016
+ [viewAnchor?.getFullYear(), viewAnchor?.getMonth()]
1017
+ );
368
1018
  const renderCalendar = () => isDual ? /* @__PURE__ */ jsx2(
369
1019
  DualCalendar,
370
1020
  {
371
1021
  locale,
372
1022
  startDate,
373
1023
  endDate,
1024
+ minDate,
1025
+ maxDate,
1026
+ month: viewMonth,
374
1027
  onChange: handleDateChange,
375
1028
  chips,
376
1029
  activeChip: activeChipValue,
@@ -385,6 +1038,9 @@ var DatePicker = forwardRef(
385
1038
  startDate,
386
1039
  endDate,
387
1040
  selectedDate,
1041
+ minDate,
1042
+ maxDate,
1043
+ month: viewMonth,
388
1044
  onChange: handleDateChange,
389
1045
  selectsRange,
390
1046
  chips,
@@ -404,16 +1060,26 @@ var DatePicker = forwardRef(
404
1060
  },
405
1061
  position: "relative",
406
1062
  width: "100%",
1063
+ ...isWeb ? { onClick: () => setOpen(true) } : {},
407
1064
  children: [
408
1065
  /* @__PURE__ */ jsx2(
409
1066
  Input,
410
1067
  {
411
1068
  ...rest,
1069
+ ref: inputRef,
412
1070
  size,
413
- placeholder: placeholder || (isRange ? "MM/DD/YYYY - MM/DD/YYYY" : "MM/DD/YYYY"),
1071
+ placeholder: placeholder || (isRange ? "Enter a date range" : "Choose a date"),
414
1072
  value: inputValue,
415
- onChangeText: handleInputChange,
416
- onFocus: () => setOpen(true),
1073
+ readOnly,
1074
+ onFocus: handleFieldFocus,
1075
+ onBlur: handleFieldBlur,
1076
+ onKeyDown: handleKeyDown,
1077
+ onClick: handleFieldClick,
1078
+ onPaste: handlePaste,
1079
+ iconRight,
1080
+ iconLeft,
1081
+ error: error || !!parseError,
1082
+ errorMessage: parseError ?? errorMessage,
417
1083
  backgroundColor,
418
1084
  testID,
419
1085
  themeMode,
@@ -423,12 +1089,18 @@ var DatePicker = forwardRef(
423
1089
  open && (isWeb ? /* @__PURE__ */ jsx2(
424
1090
  Box,
425
1091
  {
1092
+ ref: calendarRef,
1093
+ role: "dialog",
1094
+ "aria-modal": true,
1095
+ "aria-label": isRange ? "Choose a date range" : "Choose a date",
1096
+ onClick: (e) => e.stopPropagation(),
426
1097
  position: "absolute",
427
- top: "100%",
428
- left: dropdownPosition === "left" ? 0 : "auto",
429
- right: dropdownPosition === "right" ? 0 : "auto",
430
- marginTop: 4,
431
1098
  zIndex: 1e3,
1099
+ style: getPlacementStyle(
1100
+ layout.side,
1101
+ layout.align,
1102
+ PLACEMENT_OFFSET
1103
+ ),
432
1104
  children: renderCalendar()
433
1105
  }
434
1106
  ) : /* @__PURE__ */ jsx2(Box, { marginTop: 4, children: renderCalendar() }))
@@ -438,17 +1110,6 @@ var DatePicker = forwardRef(
438
1110
  }
439
1111
  );
440
1112
  DatePicker.displayName = "DatePicker";
441
-
442
- // src/utils.ts
443
- import { format as format2 } from "date-fns";
444
- import * as locales from "date-fns/locale";
445
- var defaultLocale = "enUS";
446
- function formatDate(date, formatStr, locale = defaultLocale) {
447
- const localeObj = locales[locale] || locales[defaultLocale];
448
- return format2(date, formatStr, {
449
- locale: localeObj
450
- });
451
- }
452
1113
  export {
453
1114
  Calendar2 as Calendar,
454
1115
  CalendarChips,
@@ -456,6 +1117,17 @@ export {
456
1117
  CalendarHeader,
457
1118
  DatePicker,
458
1119
  DualCalendar2 as DualCalendar,
459
- formatDate
1120
+ buildSegmentedValue,
1121
+ clampDatePart,
1122
+ datePartsFromDate,
1123
+ datePartsToDate,
1124
+ daysInMonth,
1125
+ formatDate,
1126
+ getDateSegments,
1127
+ isDatePartsComplete,
1128
+ isSegmentComplete,
1129
+ parseDateFormat,
1130
+ stepDatePart,
1131
+ stepDateSegment
460
1132
  };
461
1133
  //# sourceMappingURL=index.mjs.map