@proyecto-viviana/solid-stately 0.3.0 → 0.4.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/IncompleteDate.d.ts +61 -0
- package/dist/calendar/createCalendarState.d.ts +20 -10
- package/dist/calendar/createDateFieldState.d.ts +10 -3
- package/dist/calendar/createRangeCalendarState.d.ts +10 -2
- package/dist/calendar/index.d.ts +1 -1
- package/dist/collections/createListState.d.ts +4 -1
- package/dist/collections/createSelectionState.d.ts +17 -5
- package/dist/collections/getChildNodes.d.ts +11 -0
- package/dist/collections/index.d.ts +2 -1
- package/dist/combobox/createComboBoxState.d.ts +7 -0
- 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 +1570 -425
- 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 +110 -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 +104 -37
- package/src/calendar/createDateFieldState.ts +262 -261
- package/src/calendar/createRangeCalendarState.ts +131 -19
- package/src/calendar/index.ts +2 -0
- package/src/collections/createListState.ts +50 -19
- package/src/collections/createMenuState.ts +3 -0
- package/src/collections/createSelectionState.ts +38 -28
- package/src/collections/getChildNodes.ts +98 -0
- package/src/collections/index.ts +9 -0
- package/src/combobox/createComboBoxState.ts +8 -0
- 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 +11 -0
- package/src/select/createSelectState.ts +15 -2
- package/src/selection/Selection.ts +28 -0
- package/src/selection/SelectionManager.ts +526 -0
- package/src/selection/createMultipleSelectionState.ts +247 -0
- package/src/selection/index.ts +6 -0
- 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,
|
|
@@ -68,8 +70,12 @@ export interface RangeCalendarStateProps<T extends DateValue = DateValue> {
|
|
|
68
70
|
locale?: MaybeAccessor<string | undefined>;
|
|
69
71
|
/** Creates a calendar object for a locale calendar identifier. */
|
|
70
72
|
createCalendar?: (identifier: CalendarIdentifier) => InternationalizedCalendar;
|
|
71
|
-
/**
|
|
72
|
-
|
|
73
|
+
/**
|
|
74
|
+
* Callback that determines if a date is unavailable. The second argument is the
|
|
75
|
+
* current selection anchor (the first selected date), so availability can be
|
|
76
|
+
* derived from it. Mirrors @react-stately/calendar 1.18.
|
|
77
|
+
*/
|
|
78
|
+
isDateUnavailable?: (date: DateValue, anchorDate: CalendarDate | null) => boolean;
|
|
73
79
|
/** The number of months to display at once. */
|
|
74
80
|
visibleMonths?: MaybeAccessor<number | undefined>;
|
|
75
81
|
/** Controls how the initial visible months are aligned around the current value. */
|
|
@@ -95,6 +101,10 @@ export interface RangeCalendarState<T extends DateValue = DateValue> {
|
|
|
95
101
|
focusedDate: Accessor<CalendarDate>;
|
|
96
102
|
/** Sets the focused date. */
|
|
97
103
|
setFocusedDate: (date: CalendarDate) => void;
|
|
104
|
+
/** The minimum allowed date that a user may select. */
|
|
105
|
+
minValue: Accessor<DateValue | null>;
|
|
106
|
+
/** The maximum allowed date that a user may select. */
|
|
107
|
+
maxValue: Accessor<DateValue | null>;
|
|
98
108
|
/** The anchor date when selecting a range (first click). */
|
|
99
109
|
anchorDate: Accessor<CalendarDate | null>;
|
|
100
110
|
/** Sets the anchor date. */
|
|
@@ -189,6 +199,8 @@ export function createRangeCalendarState<T extends DateValue = CalendarDate>(
|
|
|
189
199
|
): RangeCalendarState<T> {
|
|
190
200
|
const timeZone = getLocalTimeZone();
|
|
191
201
|
const locale = createMemo(() => access(props.locale) ?? "en-US");
|
|
202
|
+
const minValueState = createMemo(() => access(props.minValue) ?? null);
|
|
203
|
+
const maxValueState = createMemo(() => access(props.maxValue) ?? null);
|
|
192
204
|
const resolvedOptions = createMemo(() => new DateFormatter(locale()).resolvedOptions());
|
|
193
205
|
const calendar = createMemo(() =>
|
|
194
206
|
(props.createCalendar ?? intlCreateCalendar)(resolvedOptions().calendar as CalendarIdentifier),
|
|
@@ -200,17 +212,56 @@ export function createRangeCalendarState<T extends DateValue = CalendarDate>(
|
|
|
200
212
|
const toDisplayCalendarDate = (date: DateValue): CalendarDate =>
|
|
201
213
|
intlToCalendar(toCalendarDate(date), calendar());
|
|
202
214
|
|
|
215
|
+
// The anchor date is the first selected date during a range drag. Declared up
|
|
216
|
+
// here (ahead of constrainDate) because the available-range derivation below
|
|
217
|
+
// depends on it, and constrainDate consults that range.
|
|
218
|
+
const [anchorDate, setAnchorDate] = createSignal<CalendarDate | null>(null);
|
|
219
|
+
|
|
220
|
+
// Mirrors @react-stately/calendar useRangeCalendarState getAvailableRange: once
|
|
221
|
+
// a range selection is anchored and unavailable dates exist, contiguous ranges
|
|
222
|
+
// are required (unless allowsNonContiguousRanges), so the selectable span is
|
|
223
|
+
// bounded by the nearest unavailable date on each side of the anchor.
|
|
224
|
+
const getAvailableRange = (
|
|
225
|
+
anchor: CalendarDate | null,
|
|
226
|
+
): { start?: CalendarDate; end?: CalendarDate } | null => {
|
|
227
|
+
if (anchor && props.isDateUnavailable && !props.allowsNonContiguousRanges) {
|
|
228
|
+
const predicate = (date: CalendarDate): boolean => props.isDateUnavailable!(date, anchor);
|
|
229
|
+
return {
|
|
230
|
+
start: nextUnavailableDate(anchor, predicate, visibleMonths, -1),
|
|
231
|
+
end: nextUnavailableDate(anchor, predicate, visibleMonths, 1),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
return null;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const availableRange = createMemo(() => getAvailableRange(anchorDate()));
|
|
238
|
+
|
|
239
|
+
// minValue/maxValue narrowed by the available range, in display-calendar space
|
|
240
|
+
// (what every cell/navigation bound below compares against). Mirrors upstream's
|
|
241
|
+
// maxDate(minValue, availableRange?.start) / minDate(maxValue, availableRange?.end);
|
|
242
|
+
// maxDate/minDate ignore undefined operands, so an absent bound passes through.
|
|
243
|
+
const effectiveMinValue = createMemo<CalendarDate | null>(() => {
|
|
244
|
+
const min = access(props.minValue);
|
|
245
|
+
const minCal = min != null ? toDisplayCalendarDate(min) : undefined;
|
|
246
|
+
return maxDate(minCal, availableRange()?.start) ?? null;
|
|
247
|
+
});
|
|
248
|
+
const effectiveMaxValue = createMemo<CalendarDate | null>(() => {
|
|
249
|
+
const max = access(props.maxValue);
|
|
250
|
+
const maxCal = max != null ? toDisplayCalendarDate(max) : undefined;
|
|
251
|
+
return minDate(maxCal, availableRange()?.end) ?? null;
|
|
252
|
+
});
|
|
253
|
+
|
|
203
254
|
const constrainDate = (date: CalendarDate): CalendarDate => {
|
|
204
|
-
const
|
|
205
|
-
const
|
|
255
|
+
const min = effectiveMinValue();
|
|
256
|
+
const max = effectiveMaxValue();
|
|
206
257
|
|
|
207
258
|
let constrained = date;
|
|
208
259
|
|
|
209
|
-
if (
|
|
210
|
-
constrained =
|
|
260
|
+
if (min && constrained.compare(min) < 0) {
|
|
261
|
+
constrained = min;
|
|
211
262
|
}
|
|
212
|
-
if (
|
|
213
|
-
constrained =
|
|
263
|
+
if (max && constrained.compare(max) > 0) {
|
|
264
|
+
constrained = max;
|
|
214
265
|
}
|
|
215
266
|
|
|
216
267
|
return constrained;
|
|
@@ -319,7 +370,6 @@ export function createRangeCalendarState<T extends DateValue = CalendarDate>(
|
|
|
319
370
|
const [visibleRangeStart, setVisibleRangeStart] = createSignal<CalendarDate>(
|
|
320
371
|
alignVisibleRangeStart(initialFocusedDate),
|
|
321
372
|
);
|
|
322
|
-
const [anchorDate, setAnchorDate] = createSignal<CalendarDate | null>(null);
|
|
323
373
|
const [isFocused, setFocused] = createSignal(false);
|
|
324
374
|
const [isDragging, setDragging] = createSignal(false);
|
|
325
375
|
|
|
@@ -344,7 +394,27 @@ export function createRangeCalendarState<T extends DateValue = CalendarDate>(
|
|
|
344
394
|
const isDisabled = createMemo(() => access(props.isDisabled) ?? false);
|
|
345
395
|
const isReadOnly = createMemo(() => access(props.isReadOnly) ?? false);
|
|
346
396
|
const validationState = createMemo(() => access(props.validationState));
|
|
347
|
-
|
|
397
|
+
// A committed range is invalid when (not mid-drag) either endpoint is
|
|
398
|
+
// unavailable or falls outside [minValue, maxValue]. Mirrors upstream
|
|
399
|
+
// useRangeCalendarState's isInvalid derivation. Calls props.isDateUnavailable
|
|
400
|
+
// directly (with a null anchor) rather than the closures defined further down,
|
|
401
|
+
// since this memo runs eagerly during init.
|
|
402
|
+
const isInvalidSelection = createMemo(() => {
|
|
403
|
+
const v = sourceValue();
|
|
404
|
+
if (!v || anchorDate()) return false;
|
|
405
|
+
if (props.isDateUnavailable?.(v.start, null) || props.isDateUnavailable?.(v.end, null)) {
|
|
406
|
+
return true;
|
|
407
|
+
}
|
|
408
|
+
const min = access(props.minValue);
|
|
409
|
+
const max = access(props.maxValue);
|
|
410
|
+
const minCal = min != null ? toDisplayCalendarDate(min) : null;
|
|
411
|
+
const maxCal = max != null ? toDisplayCalendarDate(max) : null;
|
|
412
|
+
return (
|
|
413
|
+
isDateOutsideRange(toDisplayCalendarDate(v.start), minCal, maxCal) ||
|
|
414
|
+
isDateOutsideRange(toDisplayCalendarDate(v.end), minCal, maxCal)
|
|
415
|
+
);
|
|
416
|
+
});
|
|
417
|
+
const isValueInvalid = createMemo(() => validationState() === "invalid" || isInvalidSelection());
|
|
348
418
|
|
|
349
419
|
// Highlighted range during selection
|
|
350
420
|
const highlightedRange = createMemo<RangeValue<CalendarDate> | null>(() => {
|
|
@@ -518,7 +588,7 @@ export function createRangeCalendarState<T extends DateValue = CalendarDate>(
|
|
|
518
588
|
|
|
519
589
|
// Check if a date is unavailable
|
|
520
590
|
const isCellUnavailable = (date: DateValue): boolean => {
|
|
521
|
-
return props.isDateUnavailable?.(date) ?? false;
|
|
591
|
+
return props.isDateUnavailable?.(date, anchorDate()) ?? false;
|
|
522
592
|
};
|
|
523
593
|
|
|
524
594
|
// Check if a date is disabled
|
|
@@ -526,23 +596,23 @@ export function createRangeCalendarState<T extends DateValue = CalendarDate>(
|
|
|
526
596
|
if (isDisabled()) return true;
|
|
527
597
|
if (props.isDateDisabled?.(date)) return true;
|
|
528
598
|
|
|
529
|
-
const
|
|
530
|
-
const
|
|
599
|
+
const min = effectiveMinValue();
|
|
600
|
+
const max = effectiveMaxValue();
|
|
531
601
|
const calDate = toDisplayCalendarDate(date);
|
|
532
602
|
|
|
533
|
-
if (
|
|
534
|
-
if (
|
|
603
|
+
if (min && calDate.compare(min) < 0) return true;
|
|
604
|
+
if (max && calDate.compare(max) > 0) return true;
|
|
535
605
|
|
|
536
606
|
return false;
|
|
537
607
|
};
|
|
538
608
|
|
|
539
609
|
const isDateOutsideAllowedRange = (date: DateValue): boolean => {
|
|
540
|
-
const
|
|
541
|
-
const
|
|
610
|
+
const min = effectiveMinValue();
|
|
611
|
+
const max = effectiveMaxValue();
|
|
542
612
|
const calDate = toDisplayCalendarDate(date);
|
|
543
613
|
|
|
544
|
-
if (
|
|
545
|
-
if (
|
|
614
|
+
if (min && calDate.compare(min) < 0) return true;
|
|
615
|
+
if (max && calDate.compare(max) > 0) return true;
|
|
546
616
|
|
|
547
617
|
return false;
|
|
548
618
|
};
|
|
@@ -694,6 +764,8 @@ export function createRangeCalendarState<T extends DateValue = CalendarDate>(
|
|
|
694
764
|
visibleRange,
|
|
695
765
|
timeZone,
|
|
696
766
|
locale,
|
|
767
|
+
minValue: minValueState,
|
|
768
|
+
maxValue: maxValueState,
|
|
697
769
|
firstDayOfWeek: firstDayOfWeekName,
|
|
698
770
|
validationState,
|
|
699
771
|
isValueInvalid,
|
|
@@ -747,3 +819,43 @@ function convertValue(newValue: CalendarDate, oldValue?: DateValue | null): Date
|
|
|
747
819
|
|
|
748
820
|
return localValue;
|
|
749
821
|
}
|
|
822
|
+
|
|
823
|
+
/** Whether a date falls outside the closed [minValue, maxValue] interval. */
|
|
824
|
+
function isDateOutsideRange(
|
|
825
|
+
date: CalendarDate,
|
|
826
|
+
minValue?: CalendarDate | null,
|
|
827
|
+
maxValue?: CalendarDate | null,
|
|
828
|
+
): boolean {
|
|
829
|
+
return (
|
|
830
|
+
(minValue != null && date.compare(minValue) < 0) ||
|
|
831
|
+
(maxValue != null && date.compare(maxValue) > 0)
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* Walks outward from the anchor one day at a time in `dir` until it reaches an
|
|
837
|
+
* unavailable date, then returns the last available date before it — or
|
|
838
|
+
* undefined when no unavailable date is found within `visibleMonths` of the
|
|
839
|
+
* anchor (no narrowing needed on that side). Mirrors @react-stately/calendar
|
|
840
|
+
* nextUnavailableDate.
|
|
841
|
+
*/
|
|
842
|
+
function nextUnavailableDate(
|
|
843
|
+
anchorDate: CalendarDate,
|
|
844
|
+
isDateUnavailable: (date: CalendarDate) => boolean,
|
|
845
|
+
visibleMonths: number,
|
|
846
|
+
dir: number,
|
|
847
|
+
): CalendarDate | undefined {
|
|
848
|
+
let nextDate = anchorDate.add({ days: dir });
|
|
849
|
+
const minBound = anchorDate.subtract({ months: visibleMonths });
|
|
850
|
+
const maxBound = anchorDate.add({ months: visibleMonths });
|
|
851
|
+
while (
|
|
852
|
+
(dir < 0 ? nextDate.compare(minBound) >= 0 : nextDate.compare(maxBound) <= 0) &&
|
|
853
|
+
!isDateUnavailable(nextDate)
|
|
854
|
+
) {
|
|
855
|
+
nextDate = nextDate.add({ days: dir });
|
|
856
|
+
}
|
|
857
|
+
if (isDateUnavailable(nextDate)) {
|
|
858
|
+
return nextDate.add({ days: -dir });
|
|
859
|
+
}
|
|
860
|
+
return undefined;
|
|
861
|
+
}
|
package/src/calendar/index.ts
CHANGED
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
* Based on @react-stately/list.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { createMemo, type Accessor } from "solid-js";
|
|
8
8
|
import { access, type MaybeAccessor } from "../utils";
|
|
9
9
|
import { ListCollection } from "./ListCollection";
|
|
10
|
-
import {
|
|
10
|
+
import type { SelectionState, SelectionPressEvent } from "./createSelectionState";
|
|
11
|
+
import { createMultipleSelectionState } from "../selection/createMultipleSelectionState";
|
|
12
|
+
import { SelectionManager } from "../selection/SelectionManager";
|
|
11
13
|
import type {
|
|
12
14
|
Collection,
|
|
13
15
|
CollectionItemLike,
|
|
@@ -51,6 +53,8 @@ export interface ListStateProps<T = unknown> {
|
|
|
51
53
|
export interface ListState<T = unknown> extends SelectionState {
|
|
52
54
|
/** The collection of items. */
|
|
53
55
|
readonly collection: Accessor<Collection<T>>;
|
|
56
|
+
/** The collection-aware selection manager (single source of truth for selection). */
|
|
57
|
+
readonly selectionManager: SelectionManager<T>;
|
|
54
58
|
/** Whether the collection is focused. */
|
|
55
59
|
readonly isFocused: Accessor<boolean>;
|
|
56
60
|
/** Set the focused state. */
|
|
@@ -116,8 +120,9 @@ export function createListState<T = unknown>(
|
|
|
116
120
|
return [...new Set([...propsDisabled, ...itemDisabled])];
|
|
117
121
|
});
|
|
118
122
|
|
|
119
|
-
//
|
|
120
|
-
|
|
123
|
+
// Raw selection + focus state (the lower layer). This owns the selected keys,
|
|
124
|
+
// focus, selection behavior, and disabled keys, but is not collection-aware.
|
|
125
|
+
const state = createMultipleSelectionState({
|
|
121
126
|
get selectionMode() {
|
|
122
127
|
return getProps().selectionMode;
|
|
123
128
|
},
|
|
@@ -147,24 +152,50 @@ export function createListState<T = unknown>(
|
|
|
147
152
|
},
|
|
148
153
|
});
|
|
149
154
|
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
const setFocusedKey = (key: Key | null, childStrategy?: FocusStrategy) => {
|
|
156
|
-
setFocusedKeyInternal(key);
|
|
157
|
-
setChildFocusStrategy(childStrategy ?? null);
|
|
158
|
-
};
|
|
155
|
+
// The collection-aware selection manager (the upper layer): the single source
|
|
156
|
+
// of truth for every selection operation. It reads the collection live so it
|
|
157
|
+
// stays reactive as the collection memo recomputes.
|
|
158
|
+
const selectionManager = new SelectionManager<T>(() => collection(), state);
|
|
159
159
|
|
|
160
160
|
return {
|
|
161
161
|
collection,
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
162
|
+
selectionManager,
|
|
163
|
+
|
|
164
|
+
// Focus — delegate to the raw state (the single source of truth). The
|
|
165
|
+
// setter routes through the manager so it inherits the existence guard and
|
|
166
|
+
// the upstream 'first' child-focus default.
|
|
167
|
+
isFocused: () => state.isFocused,
|
|
168
|
+
setFocused: (isFocused: boolean) => state.setFocused(isFocused),
|
|
169
|
+
focusedKey: () => state.focusedKey,
|
|
170
|
+
setFocusedKey: (key: Key | null, childStrategy?: FocusStrategy) =>
|
|
171
|
+
selectionManager.setFocusedKey(key, childStrategy),
|
|
172
|
+
childFocusStrategy: () => state.childFocusStrategy,
|
|
173
|
+
|
|
174
|
+
// Flattened selection surface — delegates to the manager so the list path
|
|
175
|
+
// shares one faithful engine. extendSelection keeps its legacy second
|
|
176
|
+
// argument for back-compat, but ignores it (the manager reads the
|
|
177
|
+
// collection itself).
|
|
178
|
+
selectionMode: () => selectionManager.selectionMode,
|
|
179
|
+
selectionBehavior: () => selectionManager.selectionBehavior,
|
|
180
|
+
disallowEmptySelection: () => selectionManager.disallowEmptySelection,
|
|
181
|
+
selectedKeys: () => state.selectedKeys,
|
|
182
|
+
disabledKeys: () => selectionManager.disabledKeys,
|
|
183
|
+
disabledBehavior: () => selectionManager.disabledBehavior,
|
|
184
|
+
isEmpty: () => selectionManager.isEmpty,
|
|
185
|
+
isSelectAll: () => selectionManager.isSelectAll,
|
|
186
|
+
isSelected: (key: Key) => selectionManager.isSelected(key),
|
|
187
|
+
isDisabled: (key: Key) => selectionManager.isDisabled(key),
|
|
188
|
+
setSelectionBehavior: (behavior: SelectionBehavior) =>
|
|
189
|
+
selectionManager.setSelectionBehavior(behavior),
|
|
190
|
+
toggleSelection: (key: Key) => selectionManager.toggleSelection(key),
|
|
191
|
+
replaceSelection: (key: Key) => selectionManager.replaceSelection(key),
|
|
192
|
+
setSelectedKeys: (keys: Iterable<Key>) => selectionManager.setSelectedKeys(keys),
|
|
193
|
+
selectAll: () => selectionManager.selectAll(),
|
|
194
|
+
clearSelection: () => selectionManager.clearSelection(),
|
|
195
|
+
toggleSelectAll: () => selectionManager.toggleSelectAll(),
|
|
196
|
+
extendSelection: (toKey: Key, _collection?: Collection) =>
|
|
197
|
+
selectionManager.extendSelection(toKey),
|
|
198
|
+
select: (key: Key, e?: SelectionPressEvent) => selectionManager.select(key, e),
|
|
168
199
|
};
|
|
169
200
|
}
|
|
170
201
|
|
|
@@ -74,11 +74,24 @@ export interface SelectionState {
|
|
|
74
74
|
/** Extend selection to a key (for shift-click). */
|
|
75
75
|
extendSelection(toKey: Key, collection: Collection): void;
|
|
76
76
|
/** Select a key based on interaction. */
|
|
77
|
-
select(
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
77
|
+
select(key: Key, e?: SelectionPressEvent): void;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The press/pointer interaction consulted by {@link SelectionState.select}.
|
|
82
|
+
* Mirrors react-stately's `SelectionManager.select(e)`: only `pointerType` is
|
|
83
|
+
* read here (touch / virtual force toggle, since those devices have no modifier
|
|
84
|
+
* keys). Modifier keys (ctrl / meta / shift) are resolved one layer up in the
|
|
85
|
+
* aria-layer `onSelect` (solidaria `selectItem`), matching upstream's split
|
|
86
|
+
* between react-stately and react-aria. The modifier fields are accepted so a
|
|
87
|
+
* full press/pointer event is assignable, but they are not read here.
|
|
88
|
+
*/
|
|
89
|
+
export interface SelectionPressEvent {
|
|
90
|
+
pointerType?: "mouse" | "pen" | "touch" | "keyboard" | "virtual";
|
|
91
|
+
shiftKey?: boolean;
|
|
92
|
+
ctrlKey?: boolean;
|
|
93
|
+
metaKey?: boolean;
|
|
94
|
+
altKey?: boolean;
|
|
82
95
|
}
|
|
83
96
|
|
|
84
97
|
/**
|
|
@@ -268,33 +281,30 @@ export function createSelectionState(
|
|
|
268
281
|
updateSelection(new Set(rangeKeys));
|
|
269
282
|
};
|
|
270
283
|
|
|
271
|
-
const select = (
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
284
|
+
const select = (key: Key, e?: SelectionPressEvent) => {
|
|
285
|
+
// Mirrors react-stately's SelectionManager.select: only pointerType +
|
|
286
|
+
// selectionBehavior are consulted here. Modifier-key handling (ctrl / meta
|
|
287
|
+
// ⇒ toggle, shift ⇒ extend) lives one layer up in solidaria's aria-layer
|
|
288
|
+
// `selectItem`, which is where the platform-aware isCtrlKeyPressed is
|
|
289
|
+
// available. The primitives self-guard disabled keys, so no guard is
|
|
290
|
+
// needed here (matching upstream).
|
|
277
291
|
if (selectionMode() === "none") return;
|
|
278
292
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
if (
|
|
293
|
+
if (selectionMode() === "single") {
|
|
294
|
+
// Single mode ignores selectionBehavior. Re-selecting the current item
|
|
295
|
+
// deselects it when empty selection is allowed; otherwise it stays
|
|
296
|
+
// selected (replace).
|
|
297
|
+
if (isSelected(key) && !disallowEmptySelection()) {
|
|
298
|
+
toggleSelection(key);
|
|
299
|
+
} else {
|
|
284
300
|
replaceSelection(key);
|
|
285
|
-
} else if (!disallowEmptySelection()) {
|
|
286
|
-
clearSelection();
|
|
287
301
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
if (e?.ctrlKey || e?.metaKey || behavior === "toggle") {
|
|
302
|
+
} else if (
|
|
303
|
+
selectionBehavior() === "toggle" ||
|
|
304
|
+
(e != null && (e.pointerType === "touch" || e.pointerType === "virtual"))
|
|
305
|
+
) {
|
|
306
|
+
// touch / virtual (VoiceOver) have no modifier keys, so multi-select is
|
|
307
|
+
// only reachable by toggling.
|
|
298
308
|
toggleSelection(key);
|
|
299
309
|
} else {
|
|
300
310
|
replaceSelection(key);
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers for traversing collection nodes and comparing their document order.
|
|
3
|
+
*
|
|
4
|
+
* Based on @react-stately/collections.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Collection, CollectionNode } from "./types";
|
|
8
|
+
|
|
9
|
+
export function getChildNodes<T>(
|
|
10
|
+
node: CollectionNode<T>,
|
|
11
|
+
collection: Collection<T>,
|
|
12
|
+
): Iterable<CollectionNode<T>> {
|
|
13
|
+
// New API: call collection.getChildren with the node key.
|
|
14
|
+
if (typeof collection.getChildren === "function") {
|
|
15
|
+
return collection.getChildren(node.key);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Old API: access childNodes directly.
|
|
19
|
+
return node.childNodes;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getFirstItem<T>(iterable: Iterable<T>): T | undefined {
|
|
23
|
+
return getNthItem(iterable, 0);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getNthItem<T>(iterable: Iterable<T>, index: number): T | undefined {
|
|
27
|
+
if (index < 0) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let i = 0;
|
|
32
|
+
for (const item of iterable) {
|
|
33
|
+
if (i === index) {
|
|
34
|
+
return item;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
i++;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function getLastItem<T>(iterable: Iterable<T>): T | undefined {
|
|
42
|
+
let lastItem: T | undefined = undefined;
|
|
43
|
+
for (const value of iterable) {
|
|
44
|
+
lastItem = value;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return lastItem;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function compareNodeOrder<T>(
|
|
51
|
+
collection: Collection<T>,
|
|
52
|
+
a: CollectionNode<T>,
|
|
53
|
+
b: CollectionNode<T>,
|
|
54
|
+
): number {
|
|
55
|
+
// If the two nodes have the same parent, compare their indices.
|
|
56
|
+
if (a.parentKey === b.parentKey) {
|
|
57
|
+
return a.index - b.index;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Otherwise, collect all of the ancestors from each node, and find the first one that doesn't match starting from the root.
|
|
61
|
+
// Include the base nodes in case we are comparing nodes of different levels so that we can compare the higher node to the lower level node's
|
|
62
|
+
// ancestor of the same level
|
|
63
|
+
const aAncestors = [...getAncestors(collection, a), a];
|
|
64
|
+
const bAncestors = [...getAncestors(collection, b), b];
|
|
65
|
+
const firstNonMatchingAncestor = aAncestors
|
|
66
|
+
.slice(0, bAncestors.length)
|
|
67
|
+
.findIndex((a, i) => a !== bAncestors[i]);
|
|
68
|
+
if (firstNonMatchingAncestor !== -1) {
|
|
69
|
+
// Compare the indices of two children within the common ancestor.
|
|
70
|
+
a = aAncestors[firstNonMatchingAncestor];
|
|
71
|
+
b = bAncestors[firstNonMatchingAncestor];
|
|
72
|
+
return a.index - b.index;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// If there isn't a non matching ancestor, we might be in a case where one of the nodes is the ancestor of the other.
|
|
76
|
+
if (aAncestors.findIndex((node) => node === b) >= 0) {
|
|
77
|
+
return 1;
|
|
78
|
+
} else if (bAncestors.findIndex((node) => node === a) >= 0) {
|
|
79
|
+
return -1;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 🤷
|
|
83
|
+
return -1;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function getAncestors<T>(collection: Collection<T>, node: CollectionNode<T>): CollectionNode<T>[] {
|
|
87
|
+
const parents: CollectionNode<T>[] = [];
|
|
88
|
+
|
|
89
|
+
let currNode: CollectionNode<T> | null = node;
|
|
90
|
+
while (currNode?.parentKey != null) {
|
|
91
|
+
currNode = collection.getItem(currNode.parentKey);
|
|
92
|
+
if (currNode) {
|
|
93
|
+
parents.unshift(currNode);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return parents;
|
|
98
|
+
}
|
package/src/collections/index.ts
CHANGED
|
@@ -16,10 +16,19 @@ export type {
|
|
|
16
16
|
|
|
17
17
|
export { ListCollection, createListCollection } from "./ListCollection";
|
|
18
18
|
|
|
19
|
+
export {
|
|
20
|
+
getChildNodes,
|
|
21
|
+
getFirstItem,
|
|
22
|
+
getNthItem,
|
|
23
|
+
getLastItem,
|
|
24
|
+
compareNodeOrder,
|
|
25
|
+
} from "./getChildNodes";
|
|
26
|
+
|
|
19
27
|
export {
|
|
20
28
|
createSelectionState,
|
|
21
29
|
type SelectionStateProps,
|
|
22
30
|
type SelectionState,
|
|
31
|
+
type SelectionPressEvent,
|
|
23
32
|
} from "./createSelectionState";
|
|
24
33
|
|
|
25
34
|
export {
|
|
@@ -11,6 +11,7 @@ import { access, type MaybeAccessor } from "../utils";
|
|
|
11
11
|
import { createListState } from "../collections/createListState";
|
|
12
12
|
import { createOverlayTriggerState } from "../overlays";
|
|
13
13
|
import type { Key, CollectionNode, Collection, FocusStrategy } from "../collections/types";
|
|
14
|
+
import type { SelectionManager } from "../selection/SelectionManager";
|
|
14
15
|
|
|
15
16
|
export type MenuTriggerAction = "focus" | "input" | "manual";
|
|
16
17
|
|
|
@@ -78,6 +79,12 @@ export interface ComboBoxStateProps<T = unknown> {
|
|
|
78
79
|
export interface ComboBoxState<T = unknown> {
|
|
79
80
|
/** The collection of items (may be filtered). */
|
|
80
81
|
readonly collection: Accessor<Collection<T>>;
|
|
82
|
+
/**
|
|
83
|
+
* The collection-aware selection manager. Mirrors upstream `useComboBoxState`,
|
|
84
|
+
* which returns the `useListState` selection manager bound to the unfiltered
|
|
85
|
+
* collection (selection spans all items; filtering is display-only).
|
|
86
|
+
*/
|
|
87
|
+
readonly selectionManager: SelectionManager<T>;
|
|
81
88
|
/** Whether the combobox dropdown is open. */
|
|
82
89
|
readonly isOpen: Accessor<boolean>;
|
|
83
90
|
/** Open the combobox dropdown. */
|
|
@@ -624,6 +631,7 @@ export function createComboBoxState<T = unknown>(
|
|
|
624
631
|
inputValue,
|
|
625
632
|
defaultInputValue: getProps().defaultInputValue ?? "",
|
|
626
633
|
setInputValue,
|
|
634
|
+
selectionManager: listState.selectionManager,
|
|
627
635
|
focusedKey: listState.focusedKey,
|
|
628
636
|
setFocusedKey: listState.setFocusedKey,
|
|
629
637
|
isFocused,
|
package/src/dnd/index.d.ts
CHANGED
package/src/dnd/index.ts
CHANGED
package/src/dnd/types.d.ts
CHANGED
|
@@ -174,10 +174,12 @@ export interface DroppableCollectionReorderEvent {
|
|
|
174
174
|
/** The drop target. */
|
|
175
175
|
target: ItemDropTarget;
|
|
176
176
|
}
|
|
177
|
+
/** A drag type: either a MIME type string or a symbol such as the directory drag type. */
|
|
178
|
+
export type DragType = string | symbol;
|
|
177
179
|
/** Interface for checking drag types. */
|
|
178
180
|
export interface DragTypes {
|
|
179
181
|
/** Returns whether the drag contains data of the given type. */
|
|
180
|
-
has(type:
|
|
182
|
+
has(type: DragType | DragType[]): boolean;
|
|
181
183
|
}
|
|
182
184
|
/** Delegate for determining drop targets within a collection. */
|
|
183
185
|
export interface DropTargetDelegate {
|
package/src/dnd/types.ts
CHANGED
|
@@ -205,10 +205,13 @@ export interface DroppableCollectionReorderEvent {
|
|
|
205
205
|
target: ItemDropTarget;
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
+
/** A drag type: either a MIME type string or a symbol such as the directory drag type. */
|
|
209
|
+
export type DragType = string | symbol;
|
|
210
|
+
|
|
208
211
|
/** Interface for checking drag types. */
|
|
209
212
|
export interface DragTypes {
|
|
210
213
|
/** Returns whether the drag contains data of the given type. */
|
|
211
|
-
has(type:
|
|
214
|
+
has(type: DragType | DragType[]): boolean;
|
|
212
215
|
}
|
|
213
216
|
|
|
214
217
|
/** Delegate for determining drop targets within a collection. */
|