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