@proyecto-viviana/solid-stately 0.3.0 → 0.4.1
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/IncompleteDate.d.ts +61 -0
- package/dist/calendar/createCalendarState.d.ts +20 -10
- package/dist/calendar/createDateFieldState.d.ts +65 -20
- package/dist/calendar/createRangeCalendarState.d.ts +20 -2
- package/dist/calendar/createTimeFieldState.d.ts +38 -67
- package/dist/calendar/index.d.ts +1 -1
- package/dist/calendar/intl/index.d.ts +10 -0
- package/dist/collections/ListCollection.d.ts +8 -0
- package/dist/collections/createListState.d.ts +16 -1
- package/dist/collections/createSelectionState.d.ts +17 -5
- package/dist/collections/getChildNodes.d.ts +19 -0
- package/dist/collections/index.d.ts +3 -2
- package/dist/collections/types.d.ts +9 -0
- package/dist/color/intl/index.d.ts +14 -0
- package/dist/combobox/createComboBoxState.d.ts +7 -0
- package/dist/dnd/createDroppableCollectionState.d.ts +17 -2
- package/dist/dnd/index.d.ts +1 -1
- package/dist/dnd/types.d.ts +3 -1
- package/dist/grid/types.d.ts +17 -3
- package/dist/index.d.ts +6 -6
- package/dist/index.js +4372 -1087
- package/dist/index.js.map +1 -1
- package/dist/select/createSelectState.d.ts +6 -0
- package/dist/selection/Selection.d.ts +16 -0
- package/dist/selection/SelectionManager.d.ts +119 -0
- package/dist/selection/createMultipleSelectionState.d.ts +59 -0
- package/dist/selection/index.d.ts +1 -0
- package/dist/table/TableCollection.d.ts +30 -0
- package/dist/table/createTreeGridState.d.ts +15 -0
- package/dist/table/index.d.ts +2 -1
- package/dist/table/types.d.ts +58 -0
- package/dist/toast/createToastState.d.ts +16 -0
- package/dist/toast/index.d.ts +1 -1
- package/dist/tree/types.d.ts +14 -6
- package/package.json +1 -1
- package/src/calendar/IncompleteDate.ts +430 -0
- package/src/calendar/createCalendarState.ts +129 -40
- package/src/calendar/createDateFieldState.ts +633 -477
- package/src/calendar/createRangeCalendarState.ts +343 -30
- package/src/calendar/createTimeFieldState.ts +179 -467
- package/src/calendar/index.ts +2 -0
- package/src/calendar/intl/index.ts +364 -0
- package/src/collections/ListCollection.ts +31 -0
- package/src/collections/createListState.ts +139 -17
- package/src/collections/createMenuState.ts +3 -0
- package/src/collections/createSelectionState.ts +38 -28
- package/src/collections/getChildNodes.ts +129 -0
- package/src/collections/index.ts +12 -0
- package/src/collections/types.ts +10 -0
- package/src/color/Color.ts +104 -133
- package/src/color/intl/index.ts +1409 -0
- package/src/combobox/createComboBoxState.ts +38 -13
- package/src/dnd/createDroppableCollectionState.ts +125 -6
- package/src/dnd/index.d.ts +1 -0
- package/src/dnd/index.ts +1 -0
- package/src/dnd/types.d.ts +3 -1
- package/src/dnd/types.ts +4 -1
- package/src/grid/createGridState.ts +36 -2
- package/src/grid/types.ts +17 -3
- package/src/index.d.ts +1 -0
- package/src/index.ts +14 -0
- package/src/select/createSelectState.ts +15 -2
- package/src/selection/Selection.ts +28 -0
- package/src/selection/SelectionManager.ts +541 -0
- package/src/selection/createMultipleSelectionState.ts +247 -0
- package/src/selection/index.ts +6 -0
- package/src/steplist/createStepListState.ts +27 -4
- package/src/table/TableCollection.ts +213 -43
- package/src/table/createTableState.ts +20 -0
- package/src/table/createTreeGridState.ts +146 -0
- package/src/table/index.ts +3 -0
- package/src/table/types.ts +66 -0
- package/src/tabs/createTabListState.ts +11 -9
- package/src/toast/createToastState.ts +43 -10
- package/src/toast/index.ts +1 -0
- package/src/tree/createTreeState.ts +37 -6
- package/src/tree/types.ts +14 -6
|
@@ -21,6 +21,8 @@ import {
|
|
|
21
21
|
endOfMonth,
|
|
22
22
|
startOfWeek,
|
|
23
23
|
getWeeksInMonth,
|
|
24
|
+
maxDate,
|
|
25
|
+
minDate,
|
|
24
26
|
DateFormatter,
|
|
25
27
|
createCalendar as intlCreateCalendar,
|
|
26
28
|
toCalendar as intlToCalendar,
|
|
@@ -28,6 +30,7 @@ import {
|
|
|
28
30
|
} from "@internationalized/date";
|
|
29
31
|
import { access, type MaybeAccessor } from "../utils";
|
|
30
32
|
import type { CalendarDayOfWeek, ValidationState } from "./createCalendarState";
|
|
33
|
+
import { getFormatOptions } from "./createDateFieldState";
|
|
31
34
|
|
|
32
35
|
const dayOfWeekNames: CalendarDayOfWeek[] = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
|
|
33
36
|
type RangeCalendarSelectionAlignment = "start" | "center" | "end";
|
|
@@ -68,8 +71,12 @@ export interface RangeCalendarStateProps<T extends DateValue = DateValue> {
|
|
|
68
71
|
locale?: MaybeAccessor<string | undefined>;
|
|
69
72
|
/** Creates a calendar object for a locale calendar identifier. */
|
|
70
73
|
createCalendar?: (identifier: CalendarIdentifier) => InternationalizedCalendar;
|
|
71
|
-
/**
|
|
72
|
-
|
|
74
|
+
/**
|
|
75
|
+
* Callback that determines if a date is unavailable. The second argument is the
|
|
76
|
+
* current selection anchor (the first selected date), so availability can be
|
|
77
|
+
* derived from it. Mirrors @react-stately/calendar 1.18.
|
|
78
|
+
*/
|
|
79
|
+
isDateUnavailable?: (date: DateValue, anchorDate: CalendarDate | null) => boolean;
|
|
73
80
|
/** The number of months to display at once. */
|
|
74
81
|
visibleMonths?: MaybeAccessor<number | undefined>;
|
|
75
82
|
/** Controls how the initial visible months are aligned around the current value. */
|
|
@@ -95,6 +102,10 @@ export interface RangeCalendarState<T extends DateValue = DateValue> {
|
|
|
95
102
|
focusedDate: Accessor<CalendarDate>;
|
|
96
103
|
/** Sets the focused date. */
|
|
97
104
|
setFocusedDate: (date: CalendarDate) => void;
|
|
105
|
+
/** The minimum allowed date that a user may select. */
|
|
106
|
+
minValue: Accessor<DateValue | null>;
|
|
107
|
+
/** The maximum allowed date that a user may select. */
|
|
108
|
+
maxValue: Accessor<DateValue | null>;
|
|
98
109
|
/** The anchor date when selecting a range (first click). */
|
|
99
110
|
anchorDate: Accessor<CalendarDate | null>;
|
|
100
111
|
/** Sets the anchor date. */
|
|
@@ -179,6 +190,16 @@ export interface RangeCalendarState<T extends DateValue = DateValue> {
|
|
|
179
190
|
isDragging: Accessor<boolean>;
|
|
180
191
|
/** Sets whether the user is dragging to select. */
|
|
181
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;
|
|
182
203
|
}
|
|
183
204
|
|
|
184
205
|
/**
|
|
@@ -189,6 +210,8 @@ export function createRangeCalendarState<T extends DateValue = CalendarDate>(
|
|
|
189
210
|
): RangeCalendarState<T> {
|
|
190
211
|
const timeZone = getLocalTimeZone();
|
|
191
212
|
const locale = createMemo(() => access(props.locale) ?? "en-US");
|
|
213
|
+
const minValueState = createMemo(() => access(props.minValue) ?? null);
|
|
214
|
+
const maxValueState = createMemo(() => access(props.maxValue) ?? null);
|
|
192
215
|
const resolvedOptions = createMemo(() => new DateFormatter(locale()).resolvedOptions());
|
|
193
216
|
const calendar = createMemo(() =>
|
|
194
217
|
(props.createCalendar ?? intlCreateCalendar)(resolvedOptions().calendar as CalendarIdentifier),
|
|
@@ -200,17 +223,56 @@ export function createRangeCalendarState<T extends DateValue = CalendarDate>(
|
|
|
200
223
|
const toDisplayCalendarDate = (date: DateValue): CalendarDate =>
|
|
201
224
|
intlToCalendar(toCalendarDate(date), calendar());
|
|
202
225
|
|
|
226
|
+
// The anchor date is the first selected date during a range drag. Declared up
|
|
227
|
+
// here (ahead of constrainDate) because the available-range derivation below
|
|
228
|
+
// depends on it, and constrainDate consults that range.
|
|
229
|
+
const [anchorDate, setAnchorDate] = createSignal<CalendarDate | null>(null);
|
|
230
|
+
|
|
231
|
+
// Mirrors @react-stately/calendar useRangeCalendarState getAvailableRange: once
|
|
232
|
+
// a range selection is anchored and unavailable dates exist, contiguous ranges
|
|
233
|
+
// are required (unless allowsNonContiguousRanges), so the selectable span is
|
|
234
|
+
// bounded by the nearest unavailable date on each side of the anchor.
|
|
235
|
+
const getAvailableRange = (
|
|
236
|
+
anchor: CalendarDate | null,
|
|
237
|
+
): { start?: CalendarDate; end?: CalendarDate } | null => {
|
|
238
|
+
if (anchor && props.isDateUnavailable && !props.allowsNonContiguousRanges) {
|
|
239
|
+
const predicate = (date: CalendarDate): boolean => props.isDateUnavailable!(date, anchor);
|
|
240
|
+
return {
|
|
241
|
+
start: nextUnavailableDate(anchor, predicate, visibleMonths, -1),
|
|
242
|
+
end: nextUnavailableDate(anchor, predicate, visibleMonths, 1),
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
return null;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const availableRange = createMemo(() => getAvailableRange(anchorDate()));
|
|
249
|
+
|
|
250
|
+
// minValue/maxValue narrowed by the available range, in display-calendar space
|
|
251
|
+
// (what every cell/navigation bound below compares against). Mirrors upstream's
|
|
252
|
+
// maxDate(minValue, availableRange?.start) / minDate(maxValue, availableRange?.end);
|
|
253
|
+
// maxDate/minDate ignore undefined operands, so an absent bound passes through.
|
|
254
|
+
const effectiveMinValue = createMemo<CalendarDate | null>(() => {
|
|
255
|
+
const min = access(props.minValue);
|
|
256
|
+
const minCal = min != null ? toDisplayCalendarDate(min) : undefined;
|
|
257
|
+
return maxDate(minCal, availableRange()?.start) ?? null;
|
|
258
|
+
});
|
|
259
|
+
const effectiveMaxValue = createMemo<CalendarDate | null>(() => {
|
|
260
|
+
const max = access(props.maxValue);
|
|
261
|
+
const maxCal = max != null ? toDisplayCalendarDate(max) : undefined;
|
|
262
|
+
return minDate(maxCal, availableRange()?.end) ?? null;
|
|
263
|
+
});
|
|
264
|
+
|
|
203
265
|
const constrainDate = (date: CalendarDate): CalendarDate => {
|
|
204
|
-
const
|
|
205
|
-
const
|
|
266
|
+
const min = effectiveMinValue();
|
|
267
|
+
const max = effectiveMaxValue();
|
|
206
268
|
|
|
207
269
|
let constrained = date;
|
|
208
270
|
|
|
209
|
-
if (
|
|
210
|
-
constrained =
|
|
271
|
+
if (min && constrained.compare(min) < 0) {
|
|
272
|
+
constrained = min;
|
|
211
273
|
}
|
|
212
|
-
if (
|
|
213
|
-
constrained =
|
|
274
|
+
if (max && constrained.compare(max) > 0) {
|
|
275
|
+
constrained = max;
|
|
214
276
|
}
|
|
215
277
|
|
|
216
278
|
return constrained;
|
|
@@ -319,7 +381,6 @@ export function createRangeCalendarState<T extends DateValue = CalendarDate>(
|
|
|
319
381
|
const [visibleRangeStart, setVisibleRangeStart] = createSignal<CalendarDate>(
|
|
320
382
|
alignVisibleRangeStart(initialFocusedDate),
|
|
321
383
|
);
|
|
322
|
-
const [anchorDate, setAnchorDate] = createSignal<CalendarDate | null>(null);
|
|
323
384
|
const [isFocused, setFocused] = createSignal(false);
|
|
324
385
|
const [isDragging, setDragging] = createSignal(false);
|
|
325
386
|
|
|
@@ -344,7 +405,27 @@ export function createRangeCalendarState<T extends DateValue = CalendarDate>(
|
|
|
344
405
|
const isDisabled = createMemo(() => access(props.isDisabled) ?? false);
|
|
345
406
|
const isReadOnly = createMemo(() => access(props.isReadOnly) ?? false);
|
|
346
407
|
const validationState = createMemo(() => access(props.validationState));
|
|
347
|
-
|
|
408
|
+
// A committed range is invalid when (not mid-drag) either endpoint is
|
|
409
|
+
// unavailable or falls outside [minValue, maxValue]. Mirrors upstream
|
|
410
|
+
// useRangeCalendarState's isInvalid derivation. Calls props.isDateUnavailable
|
|
411
|
+
// directly (with a null anchor) rather than the closures defined further down,
|
|
412
|
+
// since this memo runs eagerly during init.
|
|
413
|
+
const isInvalidSelection = createMemo(() => {
|
|
414
|
+
const v = sourceValue();
|
|
415
|
+
if (!v || anchorDate()) return false;
|
|
416
|
+
if (props.isDateUnavailable?.(v.start, null) || props.isDateUnavailable?.(v.end, null)) {
|
|
417
|
+
return true;
|
|
418
|
+
}
|
|
419
|
+
const min = access(props.minValue);
|
|
420
|
+
const max = access(props.maxValue);
|
|
421
|
+
const minCal = min != null ? toDisplayCalendarDate(min) : null;
|
|
422
|
+
const maxCal = max != null ? toDisplayCalendarDate(max) : null;
|
|
423
|
+
return (
|
|
424
|
+
isDateOutsideRange(toDisplayCalendarDate(v.start), minCal, maxCal) ||
|
|
425
|
+
isDateOutsideRange(toDisplayCalendarDate(v.end), minCal, maxCal)
|
|
426
|
+
);
|
|
427
|
+
});
|
|
428
|
+
const isValueInvalid = createMemo(() => validationState() === "invalid" || isInvalidSelection());
|
|
348
429
|
|
|
349
430
|
// Highlighted range during selection
|
|
350
431
|
const highlightedRange = createMemo<RangeValue<CalendarDate> | null>(() => {
|
|
@@ -373,6 +454,23 @@ export function createRangeCalendarState<T extends DateValue = CalendarDate>(
|
|
|
373
454
|
return { start, end: visibleRangeEndFromStart(start) };
|
|
374
455
|
});
|
|
375
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
|
+
|
|
376
474
|
createEffect(() => {
|
|
377
475
|
const controlledFocused = access(props.focusedValue);
|
|
378
476
|
if (!controlledFocused) {
|
|
@@ -483,8 +581,8 @@ export function createRangeCalendarState<T extends DateValue = CalendarDate>(
|
|
|
483
581
|
return;
|
|
484
582
|
}
|
|
485
583
|
|
|
584
|
+
syncVisibleRangeForFocusedDate(constrained);
|
|
486
585
|
setFocusedDateInternal(constrained);
|
|
487
|
-
setVisibleRangeStart(alignVisibleRangeStart(constrained));
|
|
488
586
|
props.onFocusChange?.(constrained);
|
|
489
587
|
};
|
|
490
588
|
|
|
@@ -494,7 +592,17 @@ export function createRangeCalendarState<T extends DateValue = CalendarDate>(
|
|
|
494
592
|
if (!range) return false;
|
|
495
593
|
|
|
496
594
|
const calDate = toDisplayCalendarDate(date);
|
|
497
|
-
|
|
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
|
+
);
|
|
498
606
|
};
|
|
499
607
|
|
|
500
608
|
// Check if a date is the start of the selection
|
|
@@ -513,12 +621,18 @@ export function createRangeCalendarState<T extends DateValue = CalendarDate>(
|
|
|
513
621
|
|
|
514
622
|
// Check if a date is focused
|
|
515
623
|
const isCellFocused = (date: DateValue): boolean => {
|
|
516
|
-
|
|
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());
|
|
517
631
|
};
|
|
518
632
|
|
|
519
633
|
// Check if a date is unavailable
|
|
520
634
|
const isCellUnavailable = (date: DateValue): boolean => {
|
|
521
|
-
return props.isDateUnavailable?.(date) ?? false;
|
|
635
|
+
return props.isDateUnavailable?.(date, anchorDate()) ?? false;
|
|
522
636
|
};
|
|
523
637
|
|
|
524
638
|
// Check if a date is disabled
|
|
@@ -526,23 +640,33 @@ export function createRangeCalendarState<T extends DateValue = CalendarDate>(
|
|
|
526
640
|
if (isDisabled()) return true;
|
|
527
641
|
if (props.isDateDisabled?.(date)) return true;
|
|
528
642
|
|
|
529
|
-
const
|
|
530
|
-
const
|
|
643
|
+
const min = effectiveMinValue();
|
|
644
|
+
const max = effectiveMaxValue();
|
|
531
645
|
const calDate = toDisplayCalendarDate(date);
|
|
532
646
|
|
|
533
|
-
|
|
534
|
-
|
|
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
|
+
|
|
657
|
+
if (min && calDate.compare(min) < 0) return true;
|
|
658
|
+
if (max && calDate.compare(max) > 0) return true;
|
|
535
659
|
|
|
536
660
|
return false;
|
|
537
661
|
};
|
|
538
662
|
|
|
539
663
|
const isDateOutsideAllowedRange = (date: DateValue): boolean => {
|
|
540
|
-
const
|
|
541
|
-
const
|
|
664
|
+
const min = effectiveMinValue();
|
|
665
|
+
const max = effectiveMaxValue();
|
|
542
666
|
const calDate = toDisplayCalendarDate(date);
|
|
543
667
|
|
|
544
|
-
if (
|
|
545
|
-
if (
|
|
668
|
+
if (min && calDate.compare(min) < 0) return true;
|
|
669
|
+
if (max && calDate.compare(max) > 0) return true;
|
|
546
670
|
|
|
547
671
|
return false;
|
|
548
672
|
};
|
|
@@ -560,14 +684,26 @@ export function createRangeCalendarState<T extends DateValue = CalendarDate>(
|
|
|
560
684
|
};
|
|
561
685
|
|
|
562
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.
|
|
563
693
|
const focusPreviousPage = () => {
|
|
564
694
|
const pageMonths = access(props.pageBehavior) === "single" ? 1 : visibleMonths;
|
|
565
|
-
|
|
695
|
+
const nextFocusedDate = constrainDate(focusedDate().subtract({ months: pageMonths }));
|
|
696
|
+
setFocusedDateInternal(nextFocusedDate);
|
|
697
|
+
setVisibleRangeStart(startOfMonth(visibleRangeStart().subtract({ months: pageMonths })));
|
|
698
|
+
props.onFocusChange?.(nextFocusedDate);
|
|
566
699
|
};
|
|
567
700
|
|
|
568
701
|
const focusNextPage = () => {
|
|
569
702
|
const pageMonths = access(props.pageBehavior) === "single" ? 1 : visibleMonths;
|
|
570
|
-
|
|
703
|
+
const nextFocusedDate = constrainDate(focusedDate().add({ months: pageMonths }));
|
|
704
|
+
setFocusedDateInternal(nextFocusedDate);
|
|
705
|
+
setVisibleRangeStart(startOfMonth(visibleRangeStart().add({ months: pageMonths })));
|
|
706
|
+
props.onFocusChange?.(nextFocusedDate);
|
|
571
707
|
};
|
|
572
708
|
|
|
573
709
|
const focusPreviousSection = () => {
|
|
@@ -621,26 +757,51 @@ export function createRangeCalendarState<T extends DateValue = CalendarDate>(
|
|
|
621
757
|
};
|
|
622
758
|
|
|
623
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).
|
|
624
770
|
if (isReadOnly() || isDisabled()) return;
|
|
625
|
-
|
|
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;
|
|
626
787
|
|
|
627
788
|
const anchor = anchorDate();
|
|
628
789
|
|
|
629
790
|
if (!anchor) {
|
|
630
791
|
// First click - set anchor
|
|
631
|
-
setAnchorDate(
|
|
792
|
+
setAnchorDate(selectableDate);
|
|
632
793
|
setDragging(true);
|
|
633
794
|
} else {
|
|
634
795
|
// Second click - complete selection
|
|
635
796
|
let start: CalendarDate;
|
|
636
797
|
let end: CalendarDate;
|
|
637
798
|
|
|
638
|
-
if (
|
|
639
|
-
start =
|
|
799
|
+
if (selectableDate.compare(anchor) < 0) {
|
|
800
|
+
start = selectableDate;
|
|
640
801
|
end = anchor;
|
|
641
802
|
} else {
|
|
642
803
|
start = anchor;
|
|
643
|
-
end =
|
|
804
|
+
end = selectableDate;
|
|
644
805
|
}
|
|
645
806
|
|
|
646
807
|
setValue({
|
|
@@ -651,7 +812,7 @@ export function createRangeCalendarState<T extends DateValue = CalendarDate>(
|
|
|
651
812
|
setDragging(false);
|
|
652
813
|
}
|
|
653
814
|
|
|
654
|
-
setFocusedDate(
|
|
815
|
+
setFocusedDate(selectableDate);
|
|
655
816
|
};
|
|
656
817
|
|
|
657
818
|
// Get dates in a specific week
|
|
@@ -681,6 +842,89 @@ export function createRangeCalendarState<T extends DateValue = CalendarDate>(
|
|
|
681
842
|
return getWeeksInMonth(monthDate, locale(), firstDayOfWeekName());
|
|
682
843
|
};
|
|
683
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
|
+
|
|
684
928
|
return {
|
|
685
929
|
value,
|
|
686
930
|
setValue,
|
|
@@ -694,6 +938,8 @@ export function createRangeCalendarState<T extends DateValue = CalendarDate>(
|
|
|
694
938
|
visibleRange,
|
|
695
939
|
timeZone,
|
|
696
940
|
locale,
|
|
941
|
+
minValue: minValueState,
|
|
942
|
+
maxValue: maxValueState,
|
|
697
943
|
firstDayOfWeek: firstDayOfWeekName,
|
|
698
944
|
validationState,
|
|
699
945
|
isValueInvalid,
|
|
@@ -728,6 +974,7 @@ export function createRangeCalendarState<T extends DateValue = CalendarDate>(
|
|
|
728
974
|
visibleMonths,
|
|
729
975
|
isDragging,
|
|
730
976
|
setDragging,
|
|
977
|
+
formatValue,
|
|
731
978
|
};
|
|
732
979
|
}
|
|
733
980
|
|
|
@@ -747,3 +994,69 @@ function convertValue(newValue: CalendarDate, oldValue?: DateValue | null): Date
|
|
|
747
994
|
|
|
748
995
|
return localValue;
|
|
749
996
|
}
|
|
997
|
+
|
|
998
|
+
/** Whether a date falls outside the closed [minValue, maxValue] interval. */
|
|
999
|
+
function isDateOutsideRange(
|
|
1000
|
+
date: CalendarDate,
|
|
1001
|
+
minValue?: CalendarDate | null,
|
|
1002
|
+
maxValue?: CalendarDate | null,
|
|
1003
|
+
): boolean {
|
|
1004
|
+
return (
|
|
1005
|
+
(minValue != null && date.compare(minValue) < 0) ||
|
|
1006
|
+
(maxValue != null && date.compare(maxValue) > 0)
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
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
|
+
|
|
1036
|
+
/**
|
|
1037
|
+
* Walks outward from the anchor one day at a time in `dir` until it reaches an
|
|
1038
|
+
* unavailable date, then returns the last available date before it — or
|
|
1039
|
+
* undefined when no unavailable date is found within `visibleMonths` of the
|
|
1040
|
+
* anchor (no narrowing needed on that side). Mirrors @react-stately/calendar
|
|
1041
|
+
* nextUnavailableDate.
|
|
1042
|
+
*/
|
|
1043
|
+
function nextUnavailableDate(
|
|
1044
|
+
anchorDate: CalendarDate,
|
|
1045
|
+
isDateUnavailable: (date: CalendarDate) => boolean,
|
|
1046
|
+
visibleMonths: number,
|
|
1047
|
+
dir: number,
|
|
1048
|
+
): CalendarDate | undefined {
|
|
1049
|
+
let nextDate = anchorDate.add({ days: dir });
|
|
1050
|
+
const minBound = anchorDate.subtract({ months: visibleMonths });
|
|
1051
|
+
const maxBound = anchorDate.add({ months: visibleMonths });
|
|
1052
|
+
while (
|
|
1053
|
+
(dir < 0 ? nextDate.compare(minBound) >= 0 : nextDate.compare(maxBound) <= 0) &&
|
|
1054
|
+
!isDateUnavailable(nextDate)
|
|
1055
|
+
) {
|
|
1056
|
+
nextDate = nextDate.add({ days: dir });
|
|
1057
|
+
}
|
|
1058
|
+
if (isDateUnavailable(nextDate)) {
|
|
1059
|
+
return nextDate.add({ days: -dir });
|
|
1060
|
+
}
|
|
1061
|
+
return undefined;
|
|
1062
|
+
}
|