@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.
- package/dist/calendar/createDateFieldState.d.ts +56 -18
- package/dist/calendar/createRangeCalendarState.d.ts +10 -0
- package/dist/calendar/createTimeFieldState.d.ts +38 -67
- package/dist/calendar/intl/index.d.ts +10 -0
- package/dist/collections/ListCollection.d.ts +8 -0
- package/dist/collections/createListState.d.ts +12 -0
- package/dist/collections/getChildNodes.d.ts +8 -0
- package/dist/collections/index.d.ts +2 -2
- package/dist/collections/types.d.ts +12 -0
- package/dist/color/intl/index.d.ts +14 -0
- package/dist/dnd/createDroppableCollectionState.d.ts +17 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3451 -1309
- package/dist/index.js.map +1 -1
- package/dist/selection/SelectionManager.d.ts +9 -0
- package/package.json +1 -1
- package/src/calendar/createCalendarState.ts +26 -4
- package/src/calendar/createDateFieldState.ts +614 -459
- package/src/calendar/createRangeCalendarState.ts +212 -11
- package/src/calendar/createTimeFieldState.ts +179 -467
- package/src/calendar/intl/index.ts +364 -0
- package/src/collections/ListCollection.ts +40 -1
- package/src/collections/createListState.ts +105 -2
- package/src/collections/getChildNodes.ts +31 -0
- package/src/collections/index.ts +3 -0
- package/src/collections/types.ts +13 -0
- package/src/color/Color.ts +104 -133
- package/src/color/intl/index.ts +1409 -0
- package/src/combobox/createComboBoxState.ts +48 -16
- package/src/dnd/createDroppableCollectionState.ts +125 -6
- package/src/index.ts +3 -0
- package/src/selection/SelectionManager.ts +15 -0
- package/src/steplist/createStepListState.ts +27 -4
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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 (
|
|
709
|
-
start =
|
|
799
|
+
if (selectableDate.compare(anchor) < 0) {
|
|
800
|
+
start = selectableDate;
|
|
710
801
|
end = anchor;
|
|
711
802
|
} else {
|
|
712
803
|
start = anchor;
|
|
713
|
-
end =
|
|
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(
|
|
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
|