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