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