@proyecto-viviana/solid-stately 0.4.0 → 0.5.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.
Files changed (34) hide show
  1. package/dist/calendar/createDateFieldState.d.ts +56 -18
  2. package/dist/calendar/createRangeCalendarState.d.ts +10 -0
  3. package/dist/calendar/createTimeFieldState.d.ts +38 -67
  4. package/dist/calendar/intl/index.d.ts +10 -0
  5. package/dist/collections/ListCollection.d.ts +8 -0
  6. package/dist/collections/createListState.d.ts +12 -0
  7. package/dist/collections/getChildNodes.d.ts +8 -0
  8. package/dist/collections/index.d.ts +2 -2
  9. package/dist/collections/types.d.ts +12 -0
  10. package/dist/color/intl/index.d.ts +14 -0
  11. package/dist/dnd/createDroppableCollectionState.d.ts +17 -2
  12. package/dist/index.d.ts +1 -1
  13. package/dist/index.js +3451 -1309
  14. package/dist/index.js.map +1 -1
  15. package/dist/selection/SelectionManager.d.ts +9 -0
  16. package/package.json +1 -1
  17. package/src/calendar/createCalendarState.ts +26 -4
  18. package/src/calendar/createDateFieldState.ts +614 -459
  19. package/src/calendar/createRangeCalendarState.ts +212 -11
  20. package/src/calendar/createTimeFieldState.ts +179 -467
  21. package/src/calendar/intl/index.ts +364 -0
  22. package/src/collections/ListCollection.ts +40 -1
  23. package/src/collections/createListState.ts +105 -2
  24. package/src/collections/getChildNodes.ts +31 -0
  25. package/src/collections/index.ts +3 -0
  26. package/src/collections/types.ts +13 -0
  27. package/src/color/Color.ts +104 -133
  28. package/src/color/intl/index.ts +1409 -0
  29. package/src/combobox/createComboBoxState.ts +48 -16
  30. package/src/dnd/createDroppableCollectionState.ts +125 -6
  31. package/src/index.ts +3 -0
  32. package/src/selection/SelectionManager.ts +15 -0
  33. package/src/steplist/createStepListState.ts +27 -4
  34. package/src/tabs/createTabListState.ts +9 -1
@@ -30,6 +30,7 @@ import {
30
30
  } from "@internationalized/date";
31
31
  import { access, type MaybeAccessor } from "../utils";
32
32
  import type { CalendarDayOfWeek, ValidationState } from "./createCalendarState";
33
+ import { getFormatOptions } from "./createDateFieldState";
33
34
 
34
35
  const dayOfWeekNames: CalendarDayOfWeek[] = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
35
36
  type RangeCalendarSelectionAlignment = "start" | "center" | "end";
@@ -189,6 +190,16 @@ export interface RangeCalendarState<T extends DateValue = DateValue> {
189
190
  isDragging: Accessor<boolean>;
190
191
  /** Sets whether the user is dragging to select. */
191
192
  setDragging: (dragging: boolean) => void;
193
+ /**
194
+ * Formats the selected range as a `{ start, end }` pair of localized strings,
195
+ * or `null` when the range is incomplete. Mirrors upstream
196
+ * `DateRangePickerState.formatValue` — the port collapses that state into the
197
+ * range calendar state, so the range picker reads its SR description from here.
198
+ */
199
+ formatValue: (
200
+ locale: string,
201
+ fieldOptions: Intl.DateTimeFormatOptions,
202
+ ) => { start: string; end: string } | null;
192
203
  }
193
204
 
194
205
  /**
@@ -443,6 +454,23 @@ export function createRangeCalendarState<T extends DateValue = CalendarDate>(
443
454
  return { start, end: visibleRangeEndFromStart(start) };
444
455
  });
445
456
 
457
+ // Realign the visible range only when the focused date moves outside it,
458
+ // mirroring @react-stately/calendar useCalendarState (the render-time
459
+ // `focusedDate.compare(startDate) < 0 → alignEnd` / `compare(endDate) > 0 →
460
+ // alignStart` branches — focusCell itself never touches the range). Guarding
461
+ // on out-of-range keeps a same-month focus from re-firing visibleRangeStart
462
+ // with a fresh equal-valued object, which would otherwise recompute the grid
463
+ // and recreate every cell on focus.
464
+ const syncVisibleRangeForFocusedDate = (nextFocusedDate: CalendarDate) => {
465
+ const range = visibleRange();
466
+
467
+ if (nextFocusedDate.compare(range.start) < 0) {
468
+ setVisibleRangeStart(alignEnd(nextFocusedDate));
469
+ } else if (nextFocusedDate.compare(range.end) > 0) {
470
+ setVisibleRangeStart(alignStart(nextFocusedDate));
471
+ }
472
+ };
473
+
446
474
  createEffect(() => {
447
475
  const controlledFocused = access(props.focusedValue);
448
476
  if (!controlledFocused) {
@@ -553,8 +581,8 @@ export function createRangeCalendarState<T extends DateValue = CalendarDate>(
553
581
  return;
554
582
  }
555
583
 
584
+ syncVisibleRangeForFocusedDate(constrained);
556
585
  setFocusedDateInternal(constrained);
557
- setVisibleRangeStart(alignVisibleRangeStart(constrained));
558
586
  props.onFocusChange?.(constrained);
559
587
  };
560
588
 
@@ -564,7 +592,17 @@ export function createRangeCalendarState<T extends DateValue = CalendarDate>(
564
592
  if (!range) return false;
565
593
 
566
594
  const calDate = toDisplayCalendarDate(date);
567
- return calDate.compare(range.start) >= 0 && calDate.compare(range.end) <= 0;
595
+ // Mirror @react-stately/calendar useRangeCalendarState isSelected: a disabled
596
+ // or unavailable cell is NOT reported selected, so its accessible name drops
597
+ // the "… selected" suffix + aria-selected (e.g. when the whole calendar is
598
+ // disabled). The "Selected Range: …" description prefix is separate and still
599
+ // applies to the range endpoints.
600
+ return (
601
+ calDate.compare(range.start) >= 0 &&
602
+ calDate.compare(range.end) <= 0 &&
603
+ !isCellDisabled(date) &&
604
+ !isCellUnavailable(date)
605
+ );
568
606
  };
569
607
 
570
608
  // Check if a date is the start of the selection
@@ -583,7 +621,13 @@ export function createRangeCalendarState<T extends DateValue = CalendarDate>(
583
621
 
584
622
  // Check if a date is focused
585
623
  const isCellFocused = (date: DateValue): boolean => {
586
- return isSameDay(toDisplayCalendarDate(date), focusedDate());
624
+ // Gate on the calendar-level `isFocused` flag, mirroring @react-stately
625
+ // useCalendarState (`isFocused && focusedDate && isSameDay`, inherited by
626
+ // useRangeCalendarState). The roving tabIndex tracks `focusedDate` directly
627
+ // at the cell layer, so one cell stays tabbable without this being true —
628
+ // this drives focusSafely + the range-selection prompt, which must NOT fire
629
+ // until the calendar actually holds focus.
630
+ return isFocused() && isSameDay(toDisplayCalendarDate(date), focusedDate());
587
631
  };
588
632
 
589
633
  // Check if a date is unavailable
@@ -600,6 +644,16 @@ export function createRangeCalendarState<T extends DateValue = CalendarDate>(
600
644
  const max = effectiveMaxValue();
601
645
  const calDate = toDisplayCalendarDate(date);
602
646
 
647
+ // Dates outside the visible range are disabled, mirroring @react-stately
648
+ // useCalendarState isCellDisabled (`date.compare(startDate) < 0 ||
649
+ // date.compare(endDate) > 0` over the visible range, inherited by
650
+ // useRangeCalendarState). This marks the leading/trailing padding cells
651
+ // (days from adjacent months filling the first/last grid weeks) as
652
+ // aria-disabled.
653
+ const range = visibleRange();
654
+ if (calDate.compare(toDisplayCalendarDate(range.start)) < 0) return true;
655
+ if (calDate.compare(toDisplayCalendarDate(range.end)) > 0) return true;
656
+
603
657
  if (min && calDate.compare(min) < 0) return true;
604
658
  if (max && calDate.compare(max) > 0) return true;
605
659
 
@@ -630,14 +684,26 @@ export function createRangeCalendarState<T extends DateValue = CalendarDate>(
630
684
  };
631
685
 
632
686
  // Navigation methods
687
+ // Paging realigns the visible range start explicitly, mirroring
688
+ // @react-stately/calendar focusPreviousPage/focusNextPage (which set both
689
+ // focusedDate and startDate directly). This must NOT route through
690
+ // setFocusedDate: that only realigns the range when the focused date leaves
691
+ // the window, so a single-month page whose focus stays in view would never
692
+ // advance the grid.
633
693
  const focusPreviousPage = () => {
634
694
  const pageMonths = access(props.pageBehavior) === "single" ? 1 : visibleMonths;
635
- setFocusedDate(focusedDate().subtract({ months: pageMonths }));
695
+ const nextFocusedDate = constrainDate(focusedDate().subtract({ months: pageMonths }));
696
+ setFocusedDateInternal(nextFocusedDate);
697
+ setVisibleRangeStart(startOfMonth(visibleRangeStart().subtract({ months: pageMonths })));
698
+ props.onFocusChange?.(nextFocusedDate);
636
699
  };
637
700
 
638
701
  const focusNextPage = () => {
639
702
  const pageMonths = access(props.pageBehavior) === "single" ? 1 : visibleMonths;
640
- setFocusedDate(focusedDate().add({ months: pageMonths }));
703
+ const nextFocusedDate = constrainDate(focusedDate().add({ months: pageMonths }));
704
+ setFocusedDateInternal(nextFocusedDate);
705
+ setVisibleRangeStart(startOfMonth(visibleRangeStart().add({ months: pageMonths })));
706
+ props.onFocusChange?.(nextFocusedDate);
641
707
  };
642
708
 
643
709
  const focusPreviousSection = () => {
@@ -691,26 +757,51 @@ export function createRangeCalendarState<T extends DateValue = CalendarDate>(
691
757
  };
692
758
 
693
759
  const selectDate = (date: CalendarDate) => {
760
+ // Mirror @react-stately/calendar useRangeCalendarState.selectDate: NO
761
+ // isCellDisabled/isCellUnavailable guard here. Upstream gates selection at
762
+ // the cell layer (createRangeCalendarCell handlePointerDown/handleClick check
763
+ // isSelectable before calling selectDate); the state method itself constrains
764
+ // to [minValue, maxValue] (narrowed by the available range) and walks back
765
+ // over unavailable dates, refusing only when no available date remains — and
766
+ // when the whole calendar is disabled/readOnly. Double-guarding on
767
+ // isCellDisabled also broke programmatic selection of dates outside the
768
+ // current visible range (now that isCellDisabled bounds on the visible range,
769
+ // like upstream useCalendarState).
694
770
  if (isReadOnly() || isDisabled()) return;
695
- if (isCellDisabled(date) || isCellUnavailable(date)) return;
771
+
772
+ // constrainDate == upstream constrainValue(date, min, max): clamp into the
773
+ // (available-range-narrowed) [minValue, maxValue] interval.
774
+ const constrainedDate = constrainDate(date);
775
+ // Availability is evaluated relative to the current anchor, matching
776
+ // isCellUnavailable, then previousAvailableDate walks back to the last
777
+ // available date at/before the constrained date within the visible range.
778
+ const predicate = props.isDateUnavailable
779
+ ? (d: CalendarDate) => props.isDateUnavailable!(d, anchorDate())
780
+ : undefined;
781
+ const selectableDate = previousAvailableDate(
782
+ constrainedDate,
783
+ toDisplayCalendarDate(visibleRange().start),
784
+ predicate,
785
+ );
786
+ if (!selectableDate) return;
696
787
 
697
788
  const anchor = anchorDate();
698
789
 
699
790
  if (!anchor) {
700
791
  // First click - set anchor
701
- setAnchorDate(date);
792
+ setAnchorDate(selectableDate);
702
793
  setDragging(true);
703
794
  } else {
704
795
  // Second click - complete selection
705
796
  let start: CalendarDate;
706
797
  let end: CalendarDate;
707
798
 
708
- if (date.compare(anchor) < 0) {
709
- start = date;
799
+ if (selectableDate.compare(anchor) < 0) {
800
+ start = selectableDate;
710
801
  end = anchor;
711
802
  } else {
712
803
  start = anchor;
713
- end = date;
804
+ end = selectableDate;
714
805
  }
715
806
 
716
807
  setValue({
@@ -721,7 +812,7 @@ export function createRangeCalendarState<T extends DateValue = CalendarDate>(
721
812
  setDragging(false);
722
813
  }
723
814
 
724
- setFocusedDate(date);
815
+ setFocusedDate(selectableDate);
725
816
  };
726
817
 
727
818
  // Get dates in a specific week
@@ -751,6 +842,89 @@ export function createRangeCalendarState<T extends DateValue = CalendarDate>(
751
842
  return getWeeksInMonth(monthDate, locale(), firstDayOfWeekName());
752
843
  };
753
844
 
845
+ // Format the selected range as `{ start, end }` strings. Faithful port of
846
+ // @react-stately/datepicker `useDateRangePickerState.formatValue`: when both
847
+ // endpoints share timezone + granularity and are distinct, it uses
848
+ // `formatRangeToParts` to split at the last shared literal so shared fields
849
+ // (e.g. the month/year) render once (=> "February 3" / "14, 2025").
850
+ const formatValue = (
851
+ locale2: string,
852
+ fieldOptions: Intl.DateTimeFormatOptions,
853
+ ): { start: string; end: string } | null => {
854
+ const v = value();
855
+ if (!v || !v.start || !v.end) {
856
+ return null;
857
+ }
858
+
859
+ const start = v.start as unknown as DateValue;
860
+ const end = v.end as unknown as DateValue;
861
+ const startTimeZone = "timeZone" in start ? (start as { timeZone: string }).timeZone : undefined;
862
+ const startGranularity = "minute" in start ? "minute" : "day";
863
+ const endTimeZone = "timeZone" in end ? (end as { timeZone: string }).timeZone : undefined;
864
+ const endGranularity = "minute" in end ? "minute" : "day";
865
+
866
+ const startOptions = getFormatOptions(fieldOptions, {
867
+ granularity: startGranularity,
868
+ timeZone: startTimeZone,
869
+ showEra:
870
+ (start.calendar.identifier === "gregory" && (start as { era: string }).era === "BC") ||
871
+ (end.calendar.identifier === "gregory" && (end as { era: string }).era === "BC"),
872
+ });
873
+
874
+ const startDate = start.toDate(startTimeZone || "UTC");
875
+ const endDate = end.toDate(endTimeZone || "UTC");
876
+
877
+ const startFormatter = new DateFormatter(locale2, startOptions);
878
+ let endFormatter: DateFormatter;
879
+ if (
880
+ startTimeZone === endTimeZone &&
881
+ startGranularity === endGranularity &&
882
+ start.compare(end) !== 0
883
+ ) {
884
+ // Use formatRange, as it results in shorter output when some of the fields
885
+ // are shared between the start and end dates (e.g. the same month).
886
+ try {
887
+ const parts = startFormatter.formatRangeToParts(startDate, endDate);
888
+
889
+ // Find the separator between the start and end date: the last shared
890
+ // literal before the end range begins.
891
+ let separatorIndex = -1;
892
+ for (let i = 0; i < parts.length; i++) {
893
+ const part = parts[i];
894
+ if (part.source === "shared" && part.type === "literal") {
895
+ separatorIndex = i;
896
+ } else if (part.source === "endRange") {
897
+ break;
898
+ }
899
+ }
900
+
901
+ let startStr = "";
902
+ let endStr = "";
903
+ for (let i = 0; i < parts.length; i++) {
904
+ if (i < separatorIndex) startStr += parts[i].value;
905
+ else if (i > separatorIndex) endStr += parts[i].value;
906
+ }
907
+
908
+ return { start: startStr, end: endStr };
909
+ } catch {
910
+ // ignore — fall through to per-endpoint formatting
911
+ }
912
+
913
+ endFormatter = startFormatter;
914
+ } else {
915
+ const endOptions = getFormatOptions(fieldOptions, {
916
+ granularity: endGranularity,
917
+ timeZone: endTimeZone,
918
+ });
919
+ endFormatter = new DateFormatter(locale2, endOptions);
920
+ }
921
+
922
+ return {
923
+ start: startFormatter.format(startDate),
924
+ end: endFormatter.format(endDate),
925
+ };
926
+ };
927
+
754
928
  return {
755
929
  value,
756
930
  setValue,
@@ -800,6 +974,7 @@ export function createRangeCalendarState<T extends DateValue = CalendarDate>(
800
974
  visibleMonths,
801
975
  isDragging,
802
976
  setDragging,
977
+ formatValue,
803
978
  };
804
979
  }
805
980
 
@@ -832,6 +1007,32 @@ function isDateOutsideRange(
832
1007
  );
833
1008
  }
834
1009
 
1010
+ /**
1011
+ * Walks backward from `date` one day at a time while the date is unavailable and
1012
+ * still at/after `minValue` (the visible-range start), returning the last
1013
+ * available date — or null when the walk falls before `minValue`. With no
1014
+ * `isDateUnavailable` predicate the date is returned unchanged. Mirrors
1015
+ * @react-stately/calendar previousAvailableDate.
1016
+ */
1017
+ function previousAvailableDate(
1018
+ date: CalendarDate,
1019
+ minValue: CalendarDate,
1020
+ isDateUnavailable?: (date: CalendarDate) => boolean,
1021
+ ): CalendarDate | null {
1022
+ if (!isDateUnavailable) {
1023
+ return date;
1024
+ }
1025
+
1026
+ while (date.compare(minValue) >= 0 && isDateUnavailable(date)) {
1027
+ date = date.subtract({ days: 1 });
1028
+ }
1029
+
1030
+ if (date.compare(minValue) >= 0) {
1031
+ return date;
1032
+ }
1033
+ return null;
1034
+ }
1035
+
835
1036
  /**
836
1037
  * Walks outward from the anchor one day at a time in `dir` until it reaches an
837
1038
  * unavailable date, then returns the last available date before it — or