@proyecto-viviana/solid-stately 0.6.0-rc.0 → 0.6.0-rc.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/README.md +59 -29
- package/dist/calendar/createCalendarState.d.ts +3 -1
- package/dist/calendar/createCalendarState.d.ts.map +1 -1
- package/dist/calendar/createDatePickerState.d.ts +2 -0
- package/dist/calendar/createDatePickerState.d.ts.map +1 -1
- package/dist/calendar/createRangeCalendarState.d.ts +2 -0
- package/dist/calendar/createRangeCalendarState.d.ts.map +1 -1
- package/dist/calendar/createTimeFieldState.d.ts +1 -1
- package/dist/calendar/createTimeFieldState.d.ts.map +1 -1
- package/dist/checkbox/createCheckboxGroupState.d.ts +2 -0
- package/dist/checkbox/createCheckboxGroupState.d.ts.map +1 -1
- package/dist/collections/createListState.d.ts.map +1 -1
- package/dist/collections/createMenuState.d.ts +46 -2
- package/dist/collections/createMenuState.d.ts.map +1 -1
- package/dist/collections/createSelectionState.d.ts.map +1 -1
- package/dist/collections/index.d.ts +1 -1
- package/dist/collections/index.d.ts.map +1 -1
- package/dist/color/createColorAreaState.d.ts +4 -0
- package/dist/color/createColorAreaState.d.ts.map +1 -1
- package/dist/color/createColorFieldState.d.ts +2 -0
- package/dist/color/createColorFieldState.d.ts.map +1 -1
- package/dist/color/createColorSliderState.d.ts +2 -0
- package/dist/color/createColorSliderState.d.ts.map +1 -1
- package/dist/color/createColorWheelState.d.ts.map +1 -1
- package/dist/combobox/createComboBoxState.d.ts +90 -23
- package/dist/combobox/createComboBoxState.d.ts.map +1 -1
- package/dist/combobox/index.d.ts +1 -1
- package/dist/combobox/index.d.ts.map +1 -1
- package/dist/data/createAsyncList.d.ts.map +1 -1
- package/dist/data/createListData.d.ts.map +1 -1
- package/dist/data/createTreeData.d.ts.map +1 -1
- package/dist/dnd/createDraggableCollectionState.d.ts +5 -1
- package/dist/dnd/createDraggableCollectionState.d.ts.map +1 -1
- package/dist/dnd/createDroppableCollectionState.d.ts +6 -0
- package/dist/dnd/createDroppableCollectionState.d.ts.map +1 -1
- package/dist/form/createFormValidationState.d.ts.map +1 -1
- package/dist/grid/createGridState.d.ts +2 -2
- package/dist/grid/createGridState.d.ts.map +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1012 -471
- package/dist/index.js.map +1 -1
- package/dist/menu/index.d.ts +2 -1
- package/dist/menu/index.d.ts.map +1 -1
- package/dist/numberfield/createNumberFieldState.d.ts +6 -3
- package/dist/numberfield/createNumberFieldState.d.ts.map +1 -1
- package/dist/private/flags/flags.d.ts.map +1 -0
- package/dist/private/flags/flags.js +1 -1
- package/dist/private/flags/flags.js.map +1 -1
- package/dist/radio/createRadioGroupState.d.ts.map +1 -1
- package/dist/select/createSelectState.d.ts +85 -29
- package/dist/select/createSelectState.d.ts.map +1 -1
- package/dist/select/index.d.ts +1 -1
- package/dist/select/index.d.ts.map +1 -1
- package/dist/selection/SelectionManager.d.ts.map +1 -1
- package/dist/selection/createMultipleSelectionState.d.ts.map +1 -1
- package/dist/slider/createSliderState.d.ts +35 -9
- package/dist/slider/createSliderState.d.ts.map +1 -1
- package/dist/ssr/index.d.ts +0 -6
- package/dist/ssr/index.d.ts.map +1 -1
- package/dist/steplist/createStepListState.d.ts +8 -13
- package/dist/steplist/createStepListState.d.ts.map +1 -1
- package/dist/table/createTableColumnResizeState.d.ts.map +1 -1
- package/dist/table/createTableState.d.ts +2 -2
- package/dist/table/createTableState.d.ts.map +1 -1
- package/dist/table/createTreeGridState.d.ts +2 -2
- package/dist/table/createTreeGridState.d.ts.map +1 -1
- package/dist/table/types.d.ts +2 -2
- package/dist/table/types.d.ts.map +1 -1
- package/dist/tabs/createTabListState.d.ts.map +1 -1
- package/dist/tokenfield/TokenFieldValue.d.ts +28 -4
- package/dist/tokenfield/TokenFieldValue.d.ts.map +1 -1
- package/dist/tokenfield/index.d.ts +1 -1
- package/dist/tokenfield/index.d.ts.map +1 -1
- package/dist/tree/createTreeState.d.ts +2 -2
- package/dist/tree/createTreeState.d.ts.map +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/reactivity.d.ts +5 -0
- package/dist/utils/reactivity.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/calendar/createCalendarState.ts +139 -58
- package/src/calendar/createDatePickerState.ts +14 -11
- package/src/calendar/createRangeCalendarState.ts +43 -17
- package/src/calendar/createTimeFieldState.ts +1 -1
- package/src/checkbox/createCheckboxGroupState.ts +16 -0
- package/src/collections/createListState.ts +4 -4
- package/src/collections/createMenuState.ts +134 -4
- package/src/collections/createSelectionState.ts +2 -2
- package/src/collections/index.ts +4 -0
- package/src/color/createColorAreaState.ts +19 -0
- package/src/color/createColorFieldState.ts +76 -26
- package/src/color/createColorSliderState.ts +25 -2
- package/src/color/createColorWheelState.ts +9 -5
- package/src/combobox/createComboBoxState.ts +371 -121
- package/src/combobox/index.ts +4 -0
- package/src/data/createAsyncList.ts +19 -4
- package/src/data/createListData.ts +8 -2
- package/src/data/createTreeData.ts +11 -3
- package/src/dnd/createDraggableCollectionState.ts +8 -1
- package/src/dnd/createDroppableCollectionState.ts +10 -0
- package/src/form/createFormValidationState.ts +17 -3
- package/src/grid/createGridState.ts +15 -8
- package/src/index.ts +18 -2
- package/src/menu/index.ts +5 -0
- package/src/numberfield/createNumberFieldState.ts +97 -35
- package/src/radio/createRadioGroupState.ts +4 -1
- package/src/select/createSelectState.ts +253 -131
- package/src/select/index.ts +8 -1
- package/src/selection/SelectionManager.ts +3 -1
- package/src/selection/createMultipleSelectionState.ts +2 -2
- package/src/slider/createSliderState.ts +227 -103
- package/src/ssr/index.ts +11 -4
- package/src/steplist/createStepListState.ts +156 -123
- package/src/table/createTableColumnResizeState.ts +51 -11
- package/src/table/createTableState.ts +5 -4
- package/src/table/createTreeGridState.ts +3 -4
- package/src/table/types.ts +2 -2
- package/src/tabs/createTabListState.ts +8 -31
- package/src/tokenfield/TokenFieldValue.ts +75 -16
- package/src/tokenfield/index.ts +1 -0
- package/src/tree/createTreeState.ts +15 -8
- package/src/utils/index.ts +1 -0
- package/src/utils/reactivity.ts +8 -0
- package/dist/flags/flags.d.ts.map +0 -1
- /package/dist/{flags → private/flags}/flags.d.ts +0 -0
- /package/src/{flags → private/flags}/flags.ts +0 -0
|
@@ -22,12 +22,17 @@
|
|
|
22
22
|
|
|
23
23
|
import { createMemo, createEffect, untrack } from "solid-js";
|
|
24
24
|
import type { Accessor } from "solid-js";
|
|
25
|
-
import { createInternalSignal,
|
|
25
|
+
import { createInternalSignal, propsAccessor, type MaybeAccessor } from "../utils";
|
|
26
26
|
import { createListState, type ListState } from "../collections/createListState";
|
|
27
27
|
import { createOverlayTriggerState } from "../overlays";
|
|
28
28
|
import { ListCollection } from "../collections/ListCollection";
|
|
29
29
|
import type { Key, CollectionNode, Collection, FocusStrategy } from "../collections/types";
|
|
30
30
|
import type { SelectionManager } from "../selection/SelectionManager";
|
|
31
|
+
import {
|
|
32
|
+
createFormValidationState,
|
|
33
|
+
type FormValidationState,
|
|
34
|
+
type ValidationFunction,
|
|
35
|
+
} from "../form";
|
|
31
36
|
|
|
32
37
|
export type MenuTriggerAction = "focus" | "input" | "manual";
|
|
33
38
|
|
|
@@ -35,7 +40,37 @@ export type { FocusStrategy } from "../collections/types";
|
|
|
35
40
|
|
|
36
41
|
export type FilterFn = (textValue: string, inputValue: string) => boolean;
|
|
37
42
|
|
|
38
|
-
export
|
|
43
|
+
export type ComboBoxSelectionMode = "single" | "multiple";
|
|
44
|
+
|
|
45
|
+
/** Single mode is `Key | null`. Multiple mode is `readonly Key[]`. */
|
|
46
|
+
export type ComboBoxValueType<M extends ComboBoxSelectionMode> = M extends "single"
|
|
47
|
+
? Key | null
|
|
48
|
+
: readonly Key[];
|
|
49
|
+
|
|
50
|
+
/** `onChange` receives a mutable `Key[]` in multiple mode. */
|
|
51
|
+
export type ComboBoxChangeValueType<M extends ComboBoxSelectionMode> = M extends "single"
|
|
52
|
+
? Key | null
|
|
53
|
+
: Key[];
|
|
54
|
+
|
|
55
|
+
type ComboBoxValidationType<M extends ComboBoxSelectionMode> = M extends "single"
|
|
56
|
+
? Key | null
|
|
57
|
+
: Key[];
|
|
58
|
+
|
|
59
|
+
const EMPTY_COMBOBOX_KEYS: readonly Key[] = [];
|
|
60
|
+
|
|
61
|
+
export interface ComboBoxValidationValue<M extends ComboBoxSelectionMode = "single"> {
|
|
62
|
+
/**
|
|
63
|
+
* The selected key in the ComboBox.
|
|
64
|
+
* @deprecated Use `value`.
|
|
65
|
+
*/
|
|
66
|
+
selectedKey: Key | null;
|
|
67
|
+
/** The keys of the currently selected items. */
|
|
68
|
+
value: ComboBoxValidationType<M>;
|
|
69
|
+
/** The value of the ComboBox input. */
|
|
70
|
+
inputValue: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface ComboBoxStateProps<T = unknown, M extends ComboBoxSelectionMode = "single"> {
|
|
39
74
|
/** The items to display in the combobox dropdown. */
|
|
40
75
|
items?: T[];
|
|
41
76
|
/** Default items when uncontrolled. */
|
|
@@ -48,20 +83,35 @@ export interface ComboBoxStateProps<T = unknown> {
|
|
|
48
83
|
getDisabled?: (item: T) => boolean;
|
|
49
84
|
/** Keys of disabled items. */
|
|
50
85
|
disabledKeys?: Iterable<Key>;
|
|
51
|
-
/**
|
|
52
|
-
|
|
53
|
-
|
|
86
|
+
/**
|
|
87
|
+
* Whether single or multiple selection is enabled.
|
|
88
|
+
* @default 'single'
|
|
89
|
+
*/
|
|
90
|
+
selectionMode?: M;
|
|
91
|
+
/**
|
|
92
|
+
* The current value (controlled).
|
|
93
|
+
* Single mode is `Key | null`. Multiple mode is `readonly Key[]`.
|
|
94
|
+
*/
|
|
95
|
+
value?: ComboBoxValueType<M>;
|
|
96
|
+
/** The default value (uncontrolled). */
|
|
97
|
+
defaultValue?: ComboBoxValueType<M>;
|
|
98
|
+
/** Handler called when the value changes. */
|
|
99
|
+
onChange?: (value: ComboBoxChangeValueType<M>) => void;
|
|
100
|
+
/**
|
|
101
|
+
* The currently selected key in the collection (controlled).
|
|
102
|
+
* @deprecated Use `value`.
|
|
103
|
+
*/
|
|
54
104
|
selectedKey?: Key | null;
|
|
55
|
-
/**
|
|
105
|
+
/**
|
|
106
|
+
* The initial selected key in the collection (uncontrolled).
|
|
107
|
+
* @deprecated Use `defaultValue`.
|
|
108
|
+
*/
|
|
56
109
|
defaultSelectedKey?: Key | null;
|
|
57
|
-
/**
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
/** Handler called when the selection changes (single mode). */
|
|
110
|
+
/**
|
|
111
|
+
* Handler that is called when the selection changes.
|
|
112
|
+
* @deprecated Use `onChange`.
|
|
113
|
+
*/
|
|
62
114
|
onSelectionChange?: (key: Key | null) => void;
|
|
63
|
-
/** Handler called when the selection changes (multiple mode). */
|
|
64
|
-
onSelectionChangeMultiple?: (keys: Set<Key>) => void;
|
|
65
115
|
/** The current input value (controlled). */
|
|
66
116
|
inputValue?: string;
|
|
67
117
|
/** The default input value (uncontrolled). */
|
|
@@ -90,9 +140,25 @@ export interface ComboBoxStateProps<T = unknown> {
|
|
|
90
140
|
menuTrigger?: MenuTriggerAction;
|
|
91
141
|
/** Whether to close the menu on blur. */
|
|
92
142
|
shouldCloseOnBlur?: boolean;
|
|
143
|
+
/** Whether the combobox is invalid (controlled). */
|
|
144
|
+
isInvalid?: boolean;
|
|
145
|
+
/** @deprecated Use isInvalid instead. */
|
|
146
|
+
validationState?: "valid" | "invalid";
|
|
147
|
+
/** Custom validation function. */
|
|
148
|
+
validate?: ValidationFunction<ComboBoxValidationValue<M> | null>;
|
|
149
|
+
/**
|
|
150
|
+
* Whether to use native HTML form validation or ARIA validation semantics.
|
|
151
|
+
* @default "native"
|
|
152
|
+
*/
|
|
153
|
+
validationBehavior?: "aria" | "native";
|
|
154
|
+
/** Field name(s) for server error lookup. */
|
|
155
|
+
name?: string | string[];
|
|
93
156
|
}
|
|
94
157
|
|
|
95
|
-
export interface ComboBoxState<
|
|
158
|
+
export interface ComboBoxState<
|
|
159
|
+
T = unknown,
|
|
160
|
+
M extends ComboBoxSelectionMode = "single",
|
|
161
|
+
> extends FormValidationState {
|
|
96
162
|
/** The collection of items (may be filtered). */
|
|
97
163
|
readonly collection: Accessor<Collection<T>>;
|
|
98
164
|
/**
|
|
@@ -109,19 +175,40 @@ export interface ComboBoxState<T = unknown> {
|
|
|
109
175
|
close(): void;
|
|
110
176
|
/** Toggle the combobox dropdown. */
|
|
111
177
|
toggle(focusStrategy?: FocusStrategy | null, trigger?: MenuTriggerAction): void;
|
|
112
|
-
/**
|
|
178
|
+
/**
|
|
179
|
+
* The key for the first selected item. Null in multiple mode.
|
|
180
|
+
* @deprecated Use `value`.
|
|
181
|
+
*/
|
|
113
182
|
readonly selectedKey: Accessor<Key | null>;
|
|
114
|
-
/**
|
|
183
|
+
/**
|
|
184
|
+
* The default selected key.
|
|
185
|
+
* @deprecated Use `defaultValue`.
|
|
186
|
+
*/
|
|
115
187
|
readonly defaultSelectedKey: Key | null;
|
|
116
|
-
/**
|
|
188
|
+
/**
|
|
189
|
+
* The value of the first selected item.
|
|
190
|
+
* @deprecated Use `selectedItems`.
|
|
191
|
+
*/
|
|
117
192
|
readonly selectedItem: Accessor<CollectionNode<T> | null>;
|
|
118
|
-
/**
|
|
193
|
+
/**
|
|
194
|
+
* Sets the selected key.
|
|
195
|
+
* @deprecated Use `setValue`.
|
|
196
|
+
*/
|
|
119
197
|
setSelectedKey(key: Key | null): void;
|
|
120
|
-
/** The
|
|
198
|
+
/** The current combobox value. */
|
|
199
|
+
readonly value: Accessor<ComboBoxValueType<M>>;
|
|
200
|
+
/** The default combobox value. */
|
|
201
|
+
readonly defaultValue: ComboBoxValueType<M>;
|
|
202
|
+
/** Sets the combobox value. */
|
|
203
|
+
setValue(value: Key | readonly Key[] | null): void;
|
|
204
|
+
/**
|
|
205
|
+
* Selected keys derived from `value`.
|
|
206
|
+
* Kept for the headless Backspace path and the tag/list adapters.
|
|
207
|
+
*/
|
|
121
208
|
readonly selectedKeys: Accessor<Set<Key>>;
|
|
122
|
-
/** Replace the selected keys
|
|
123
|
-
setSelectedKeys(keys:
|
|
124
|
-
/** The currently selected items
|
|
209
|
+
/** Replace the selected keys. */
|
|
210
|
+
setSelectedKeys(keys: Iterable<Key>): void;
|
|
211
|
+
/** The currently selected items. */
|
|
125
212
|
readonly selectedItems: Accessor<CollectionNode<T>[]>;
|
|
126
213
|
/** Remove a selected key (multiple mode). */
|
|
127
214
|
removeSelectedKey(key: Key): void;
|
|
@@ -150,7 +237,7 @@ export interface ComboBoxState<T = unknown> {
|
|
|
150
237
|
/** Select a key and close the menu (for ListState compatibility). */
|
|
151
238
|
select(key: Key): void;
|
|
152
239
|
/** The selection mode. */
|
|
153
|
-
readonly selectionMode: Accessor<
|
|
240
|
+
readonly selectionMode: Accessor<M>;
|
|
154
241
|
/** Check if a key is selected. */
|
|
155
242
|
isSelected(key: Key): boolean;
|
|
156
243
|
/** Whether the combobox is disabled. */
|
|
@@ -172,17 +259,17 @@ export const defaultContainsFilter: FilterFn = (textValue, inputValue) => {
|
|
|
172
259
|
* Creates state for a combobox component.
|
|
173
260
|
* Combines list state with input value management and filtering.
|
|
174
261
|
*/
|
|
175
|
-
export function createComboBoxState<T = unknown>(
|
|
176
|
-
props: MaybeAccessor<ComboBoxStateProps<T>>,
|
|
177
|
-
): ComboBoxState<T> {
|
|
178
|
-
const getProps = (
|
|
262
|
+
export function createComboBoxState<T = unknown, M extends ComboBoxSelectionMode = "single">(
|
|
263
|
+
props: MaybeAccessor<ComboBoxStateProps<T, M>>,
|
|
264
|
+
): ComboBoxState<T, M> {
|
|
265
|
+
const getProps = propsAccessor(props);
|
|
179
266
|
|
|
180
267
|
// Extract options with defaults
|
|
181
268
|
const menuTrigger = () => getProps().menuTrigger ?? "input";
|
|
182
269
|
const allowsEmptyCollection = () => getProps().allowsEmptyCollection ?? false;
|
|
183
270
|
const allowsCustomValue = () => getProps().allowsCustomValue ?? false;
|
|
184
271
|
const shouldCloseOnBlur = () => getProps().shouldCloseOnBlur ?? true;
|
|
185
|
-
const isMultiple = () => getProps().selectionMode === "multiple";
|
|
272
|
+
const isMultiple = () => (getProps().selectionMode ?? "single") === "multiple";
|
|
186
273
|
|
|
187
274
|
// Track focus strategy for list navigation
|
|
188
275
|
const [focusStrategy, setFocusStrategy] = createInternalSignal<FocusStrategy | null>(null);
|
|
@@ -193,31 +280,37 @@ export function createComboBoxState<T = unknown>(
|
|
|
193
280
|
// Track the menu open trigger
|
|
194
281
|
let menuOpenTrigger: MenuTriggerAction = "focus";
|
|
195
282
|
|
|
196
|
-
// ----
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
);
|
|
283
|
+
// ---- Value ----
|
|
284
|
+
// One signal matches upstream ValueType<M>. `selectedKey` stays as a
|
|
285
|
+
// deprecated single-mode view. Multiple mode reads and writes `value`.
|
|
286
|
+
type StoredComboBoxValue = Key | readonly Key[] | null;
|
|
201
287
|
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
288
|
+
const controlledPropValue = (): StoredComboBoxValue | undefined => {
|
|
289
|
+
const propsNow = getProps();
|
|
290
|
+
if (propsNow.value !== undefined) {
|
|
291
|
+
return propsNow.value as StoredComboBoxValue;
|
|
292
|
+
}
|
|
293
|
+
if (!isMultiple() && propsNow.selectedKey !== undefined) {
|
|
294
|
+
return propsNow.selectedKey;
|
|
295
|
+
}
|
|
296
|
+
return undefined;
|
|
206
297
|
};
|
|
207
298
|
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
299
|
+
const isValueControlled = () => controlledPropValue() !== undefined;
|
|
300
|
+
|
|
301
|
+
const initialStoredValue = (): StoredComboBoxValue => {
|
|
302
|
+
const propsNow = getProps();
|
|
303
|
+
if (propsNow.defaultValue !== undefined) {
|
|
304
|
+
return propsNow.defaultValue as StoredComboBoxValue;
|
|
211
305
|
}
|
|
212
|
-
|
|
306
|
+
if ((propsNow.selectionMode ?? "single") === "multiple") {
|
|
307
|
+
return [];
|
|
308
|
+
}
|
|
309
|
+
return propsNow.defaultSelectedKey ?? null;
|
|
213
310
|
};
|
|
214
311
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
const isSelectionControlled = () => getProps().selectedKey !== undefined;
|
|
218
|
-
const [internalSelectedKey, setInternalSelectedKey] = createInternalSignal<Key | null>(
|
|
219
|
-
getProps().defaultSelectedKey ?? null,
|
|
220
|
-
);
|
|
312
|
+
const [internalValue, setInternalValue] =
|
|
313
|
+
createInternalSignal<StoredComboBoxValue>(initialStoredValue());
|
|
221
314
|
|
|
222
315
|
// ---- Input Value State ----
|
|
223
316
|
// Initialized after selection so we can derive from selected item if needed
|
|
@@ -247,20 +340,124 @@ export function createComboBoxState<T = unknown>(
|
|
|
247
340
|
// Track last committed input value
|
|
248
341
|
const [lastValue, setLastValue] = createInternalSignal(inputValue());
|
|
249
342
|
|
|
343
|
+
const storedValue = (): StoredComboBoxValue => {
|
|
344
|
+
const controlled = controlledPropValue();
|
|
345
|
+
return controlled !== undefined ? controlled : internalValue();
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
// `Array.isArray` narrows `readonly Key[]` to `any[]` and then fails to
|
|
349
|
+
// exclude that array from the scalar branch. A string/number check keeps
|
|
350
|
+
// the caller's readonly list intact.
|
|
351
|
+
const displayValue = (): Key | readonly Key[] | null => {
|
|
352
|
+
const current = storedValue();
|
|
353
|
+
if (!isMultiple()) {
|
|
354
|
+
return isKeyList(current) ? (current[0] ?? null) : current;
|
|
355
|
+
}
|
|
356
|
+
if (isKeyList(current)) return current;
|
|
357
|
+
if (current == null) return EMPTY_COMBOBOX_KEYS;
|
|
358
|
+
return [current];
|
|
359
|
+
};
|
|
360
|
+
|
|
250
361
|
const selectedKey: Accessor<Key | null> = () => {
|
|
251
|
-
|
|
362
|
+
if (isMultiple()) return null;
|
|
363
|
+
const current = displayValue();
|
|
364
|
+
return isKeyList(current) ? (current[0] ?? null) : current;
|
|
252
365
|
};
|
|
253
366
|
|
|
254
|
-
const
|
|
367
|
+
const selectedKeys: Accessor<Set<Key>> = () => {
|
|
368
|
+
const current = displayValue();
|
|
369
|
+
if (isKeyList(current)) return new Set(current);
|
|
370
|
+
return current == null ? new Set() : new Set([current]);
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
const setValue = (next: Key | readonly Key[] | null) => {
|
|
255
374
|
// Solid 2 batches writes onto a microtask, so a fully controlled
|
|
256
|
-
// onSelectionChange that updates
|
|
375
|
+
// onSelectionChange that updates the value then inputValue settles in
|
|
257
376
|
// one flush instead of closing then auto-opening.
|
|
258
|
-
if (!
|
|
259
|
-
|
|
377
|
+
if (!isMultiple()) {
|
|
378
|
+
const key: Key | null =
|
|
379
|
+
typeof next === "string" || typeof next === "number"
|
|
380
|
+
? next
|
|
381
|
+
: next === null
|
|
382
|
+
? null
|
|
383
|
+
: (next[0] ?? null);
|
|
384
|
+
const previous = untrack(selectedKey);
|
|
385
|
+
if (!isValueControlled()) {
|
|
386
|
+
setInternalValue(key);
|
|
387
|
+
}
|
|
388
|
+
getProps().onChange?.(key as ComboBoxChangeValueType<M>);
|
|
389
|
+
if (key !== previous) {
|
|
390
|
+
getProps().onSelectionChange?.(key);
|
|
391
|
+
}
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Copy into state so the caller's list is never stored or mutated.
|
|
396
|
+
// `onChange` gets a second array, so a callback can mutate its argument
|
|
397
|
+
// without changing state or the input.
|
|
398
|
+
const keys: Key[] =
|
|
399
|
+
typeof next === "string" || typeof next === "number"
|
|
400
|
+
? [next]
|
|
401
|
+
: next === null
|
|
402
|
+
? []
|
|
403
|
+
: [...next];
|
|
404
|
+
if (!isValueControlled()) {
|
|
405
|
+
setInternalValue(keys);
|
|
260
406
|
}
|
|
261
|
-
getProps().
|
|
407
|
+
getProps().onChange?.([...keys] as ComboBoxChangeValueType<M>);
|
|
262
408
|
};
|
|
263
409
|
|
|
410
|
+
const setSelectedKey = (key: Key | null) => {
|
|
411
|
+
setValue(key);
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
const defaultValue: ComboBoxValueType<M> = (() => {
|
|
415
|
+
const propsNow = getProps();
|
|
416
|
+
const resolved =
|
|
417
|
+
propsNow.defaultValue !== undefined
|
|
418
|
+
? propsNow.defaultValue
|
|
419
|
+
: isMultiple()
|
|
420
|
+
? (EMPTY_COMBOBOX_KEYS as unknown as ComboBoxValueType<M>)
|
|
421
|
+
: ((propsNow.defaultSelectedKey ?? null) as ComboBoxValueType<M>);
|
|
422
|
+
return (resolved ?? untrack(displayValue)) as ComboBoxValueType<M>;
|
|
423
|
+
})();
|
|
424
|
+
|
|
425
|
+
const defaultSelectedKey =
|
|
426
|
+
getProps().defaultSelectedKey ?? (isMultiple() ? null : (untrack(displayValue) as Key | null));
|
|
427
|
+
|
|
428
|
+
const validationValue = createMemo<ComboBoxValidationValue<M> | null>(() => {
|
|
429
|
+
const dVal = displayValue();
|
|
430
|
+
if (isKeyList(dVal) && dVal.length === 0) {
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
return {
|
|
434
|
+
inputValue: inputValue(),
|
|
435
|
+
value: (isKeyList(dVal) ? [...dVal] : dVal) as ComboBoxValidationType<M>,
|
|
436
|
+
selectedKey: selectedKey(),
|
|
437
|
+
};
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
const validation = createFormValidationState({
|
|
441
|
+
get value() {
|
|
442
|
+
return validationValue();
|
|
443
|
+
},
|
|
444
|
+
get isInvalid() {
|
|
445
|
+
return getProps().isInvalid;
|
|
446
|
+
},
|
|
447
|
+
get validationState() {
|
|
448
|
+
return getProps().validationState;
|
|
449
|
+
},
|
|
450
|
+
get validate() {
|
|
451
|
+
return getProps().validate;
|
|
452
|
+
},
|
|
453
|
+
get name() {
|
|
454
|
+
return getProps().name;
|
|
455
|
+
},
|
|
456
|
+
get validationBehavior() {
|
|
457
|
+
return getProps().validationBehavior ?? "native";
|
|
458
|
+
},
|
|
459
|
+
});
|
|
460
|
+
|
|
264
461
|
// ---- Overlay State ----
|
|
265
462
|
// Assigned after createListState; onOpenChange only fires after both exist
|
|
266
463
|
// except a defaultOpen init, which we skip via the optional call.
|
|
@@ -303,34 +500,37 @@ export function createComboBoxState<T = unknown>(
|
|
|
303
500
|
get selectionMode() {
|
|
304
501
|
return isMultiple() ? ("multiple" as const) : ("single" as const);
|
|
305
502
|
},
|
|
306
|
-
|
|
503
|
+
// RAC useComboBoxState.ts:254-255. Single selection cannot be toggled
|
|
504
|
+
// empty. Duplicate events re-emit the same key so a click on the selected
|
|
505
|
+
// option reaches the reset-and-close branch below. Multiple selection
|
|
506
|
+
// still allows clearing the last key.
|
|
507
|
+
get disallowEmptySelection() {
|
|
508
|
+
return !isMultiple();
|
|
509
|
+
},
|
|
510
|
+
allowDuplicateSelectionEvents: true,
|
|
307
511
|
get selectedKeys() {
|
|
308
|
-
|
|
309
|
-
return Array.from(selectedKeys());
|
|
310
|
-
}
|
|
311
|
-
const key = selectedKey();
|
|
312
|
-
return key != null ? [key] : [];
|
|
512
|
+
return convertValue(displayValue());
|
|
313
513
|
},
|
|
314
514
|
onSelectionChange(keys) {
|
|
315
515
|
if (keys === "all") return;
|
|
316
516
|
|
|
317
517
|
if (isMultiple()) {
|
|
318
|
-
|
|
319
|
-
const newKeys = new Set(keys);
|
|
320
|
-
setSelectedKeys(newKeys);
|
|
518
|
+
setValue([...keys]);
|
|
321
519
|
return;
|
|
322
520
|
}
|
|
323
521
|
|
|
324
522
|
const key = keys.size > 0 ? Array.from(keys)[0] : null;
|
|
325
523
|
|
|
326
|
-
//
|
|
524
|
+
// RAC useComboBoxState.ts:265-270 — same key still notifies, then
|
|
525
|
+
// resets the input and closes (click on the already selected option).
|
|
327
526
|
if (key === selectedKey()) {
|
|
527
|
+
getProps().onSelectionChange?.(key);
|
|
328
528
|
resetInputValue();
|
|
329
529
|
closeMenu();
|
|
330
530
|
return;
|
|
331
531
|
}
|
|
332
532
|
|
|
333
|
-
|
|
533
|
+
setValue(key);
|
|
334
534
|
},
|
|
335
535
|
});
|
|
336
536
|
|
|
@@ -374,13 +574,16 @@ export function createComboBoxState<T = unknown>(
|
|
|
374
574
|
// Initialize input value from selected item if not already set
|
|
375
575
|
// This runs once on creation
|
|
376
576
|
if (!inputInitialized && !isInputControlled()) {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
const
|
|
381
|
-
if (
|
|
382
|
-
|
|
383
|
-
|
|
577
|
+
// RAC getDefaultInputValue: an explicit empty defaultInputValue wins.
|
|
578
|
+
// Multiple mode does not seed the input from the first selected item.
|
|
579
|
+
if (getProps().defaultInputValue == null && !isMultiple()) {
|
|
580
|
+
const key = selectedKey();
|
|
581
|
+
if (key != null) {
|
|
582
|
+
const item = originalCollection().getItem(key);
|
|
583
|
+
if (item) {
|
|
584
|
+
setInternalInputValue(item.textValue);
|
|
585
|
+
setLastValue(item.textValue);
|
|
586
|
+
}
|
|
384
587
|
}
|
|
385
588
|
}
|
|
386
589
|
inputInitialized = true;
|
|
@@ -388,8 +591,8 @@ export function createComboBoxState<T = unknown>(
|
|
|
388
591
|
|
|
389
592
|
// ---- Helper Functions ----
|
|
390
593
|
const resetInputValue = () => {
|
|
391
|
-
const
|
|
392
|
-
const textValue =
|
|
594
|
+
const key = selectedKey();
|
|
595
|
+
const textValue = key != null ? (originalCollection().getItem(key)?.textValue ?? "") : "";
|
|
393
596
|
setLastValue(textValue);
|
|
394
597
|
setInputValue(textValue);
|
|
395
598
|
};
|
|
@@ -483,21 +686,35 @@ export function createComboBoxState<T = unknown>(
|
|
|
483
686
|
|
|
484
687
|
// ---- Commit/Revert Logic ----
|
|
485
688
|
const commitCustomValue = () => {
|
|
486
|
-
|
|
689
|
+
if (isMultiple()) {
|
|
690
|
+
// Custom text is independent of the selected items.
|
|
691
|
+
setLastValue(inputValue());
|
|
692
|
+
closeMenu();
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
setValue(null);
|
|
487
696
|
closeMenu();
|
|
488
697
|
};
|
|
489
698
|
|
|
490
|
-
const commitSelection = () => {
|
|
491
|
-
//
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
const
|
|
699
|
+
const commitSelection = (shouldForceSelectionChange = false) => {
|
|
700
|
+
// Both the value and the input are controlled: notify, then close.
|
|
701
|
+
// Otherwise reset the input for the caller.
|
|
702
|
+
if (isValueControlled() && isInputControlled()) {
|
|
703
|
+
const key = selectedKey();
|
|
704
|
+
const itemText = key != null ? (originalCollection().getItem(key)?.textValue ?? "") : "";
|
|
705
|
+
if (shouldForceSelectionChange || isMultiple() || inputValue() !== itemText) {
|
|
706
|
+
getProps().onSelectionChange?.(key);
|
|
707
|
+
const current = displayValue();
|
|
708
|
+
const changeValue = (
|
|
709
|
+
isKeyList(current) ? [...current] : current
|
|
710
|
+
) as ComboBoxChangeValueType<M>;
|
|
711
|
+
getProps().onChange?.(changeValue);
|
|
712
|
+
}
|
|
495
713
|
// RAC useComboBoxState.ts:543-545 — stop the auto-open-on-input effect
|
|
496
714
|
// from reopening after this close.
|
|
497
|
-
setLastValue(
|
|
715
|
+
setLastValue(itemText);
|
|
498
716
|
closeMenu();
|
|
499
717
|
} else {
|
|
500
|
-
// Reset input to selected item's text
|
|
501
718
|
resetInputValue();
|
|
502
719
|
closeMenu();
|
|
503
720
|
}
|
|
@@ -520,21 +737,16 @@ export function createComboBoxState<T = unknown>(
|
|
|
520
737
|
const commit = () => {
|
|
521
738
|
const focusedKey = listState.focusedKey();
|
|
522
739
|
|
|
523
|
-
if (isMultiple()) {
|
|
524
|
-
// In multiple mode, toggle the focused item without closing
|
|
525
|
-
if (overlayState.isOpen() && focusedKey != null) {
|
|
526
|
-
select(focusedKey);
|
|
527
|
-
}
|
|
528
|
-
return;
|
|
529
|
-
}
|
|
530
|
-
|
|
531
740
|
if (overlayState.isOpen() && focusedKey != null) {
|
|
532
|
-
// RAC useComboBoxState.ts:564-572 — already-selected key goes
|
|
533
|
-
// commitSelection. A new key is `selectionManager.select()`
|
|
534
|
-
// close; the open/close effect then closes while the menu is
|
|
535
|
-
// open, so auto-open (input !== last && !isOpen) is skipped.
|
|
536
|
-
|
|
537
|
-
|
|
741
|
+
// RAC useComboBoxState.ts:564-572 — an already-selected single key goes
|
|
742
|
+
// through commitSelection. A new single key is `selectionManager.select()`
|
|
743
|
+
// without close; the open/close effect then closes while the menu is
|
|
744
|
+
// still open, so auto-open (input !== last && !isOpen) is skipped.
|
|
745
|
+
// Multiple toggles through setValue and leaves the menu open.
|
|
746
|
+
if (!isMultiple() && selectedKey() === focusedKey) {
|
|
747
|
+
commitSelection(true);
|
|
748
|
+
} else if (isMultiple()) {
|
|
749
|
+
select(focusedKey);
|
|
538
750
|
} else {
|
|
539
751
|
listState.selectionManager.select(focusedKey);
|
|
540
752
|
}
|
|
@@ -554,12 +766,29 @@ export function createComboBoxState<T = unknown>(
|
|
|
554
766
|
// ---- Focus Handling ----
|
|
555
767
|
const [isFocused, setIsFocused] = createInternalSignal(false);
|
|
556
768
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
769
|
+
let valueOnFocus: [string, Key | readonly Key[] | null] = [
|
|
770
|
+
untrack(inputValue),
|
|
771
|
+
untrack(displayValue) as Key | readonly Key[] | null,
|
|
772
|
+
];
|
|
773
|
+
|
|
774
|
+
const hasMoved = (a: Key | readonly Key[] | null, b: Key | readonly Key[] | null): boolean => {
|
|
775
|
+
if (a === b) return false;
|
|
776
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
777
|
+
if (a.length !== b.length) return true;
|
|
778
|
+
for (let i = 0; i < a.length; i++) {
|
|
779
|
+
if (a[i] !== b[i]) return true;
|
|
780
|
+
}
|
|
781
|
+
return false;
|
|
782
|
+
}
|
|
783
|
+
return true;
|
|
784
|
+
};
|
|
785
|
+
|
|
786
|
+
// RAC useComboBoxState.ts:578-593 keeps `valueOnFocus = useRef([inputValue, displayValue])`
|
|
787
|
+
// and, on blur, calls `validation.commitValidation()` when the input value or the display
|
|
788
|
+
// value moved while focused.
|
|
561
789
|
const setFocused = (focused: boolean) => {
|
|
562
790
|
if (focused) {
|
|
791
|
+
valueOnFocus = [inputValue(), displayValue() as Key | readonly Key[] | null];
|
|
563
792
|
if (menuTrigger() === "focus" && !getProps().isReadOnly) {
|
|
564
793
|
open(null, "focus");
|
|
565
794
|
}
|
|
@@ -567,6 +796,12 @@ export function createComboBoxState<T = unknown>(
|
|
|
567
796
|
if (shouldCloseOnBlur()) {
|
|
568
797
|
commitValue();
|
|
569
798
|
}
|
|
799
|
+
if (
|
|
800
|
+
inputValue() !== valueOnFocus[0] ||
|
|
801
|
+
hasMoved(displayValue() as Key | readonly Key[] | null, valueOnFocus[1])
|
|
802
|
+
) {
|
|
803
|
+
validation.commitValidation();
|
|
804
|
+
}
|
|
570
805
|
}
|
|
571
806
|
setIsFocused(focused);
|
|
572
807
|
};
|
|
@@ -593,7 +828,8 @@ export function createComboBoxState<T = unknown>(
|
|
|
593
828
|
trigger: menuTrigger(),
|
|
594
829
|
showingAll: showAllItems(),
|
|
595
830
|
inputControlled: isInputControlled(),
|
|
596
|
-
selectionControlled:
|
|
831
|
+
selectionControlled: isValueControlled(),
|
|
832
|
+
multiple: isMultiple(),
|
|
597
833
|
}),
|
|
598
834
|
({
|
|
599
835
|
input,
|
|
@@ -607,6 +843,7 @@ export function createComboBoxState<T = unknown>(
|
|
|
607
843
|
showingAll,
|
|
608
844
|
inputControlled,
|
|
609
845
|
selectionControlled,
|
|
846
|
+
multiple,
|
|
610
847
|
}) => {
|
|
611
848
|
// Auto-open when typing
|
|
612
849
|
if (
|
|
@@ -634,9 +871,10 @@ export function createComboBoxState<T = unknown>(
|
|
|
634
871
|
listState.setFocusedKey(null);
|
|
635
872
|
setShowAllItems(false);
|
|
636
873
|
|
|
637
|
-
// Clear selection when input is cleared
|
|
638
|
-
|
|
639
|
-
|
|
874
|
+
// Clear selection when the input is cleared. Fully controlled single
|
|
875
|
+
// values, and every multiple value, stay put.
|
|
876
|
+
if (!multiple && input === "" && (!inputControlled || !selectionControlled)) {
|
|
877
|
+
setValue(null);
|
|
640
878
|
}
|
|
641
879
|
|
|
642
880
|
setLastValue(input);
|
|
@@ -671,7 +909,7 @@ export function createComboBoxState<T = unknown>(
|
|
|
671
909
|
key,
|
|
672
910
|
textValue: item?.textValue ?? "",
|
|
673
911
|
inputControlled: isInputControlled(),
|
|
674
|
-
selectionControlled:
|
|
912
|
+
selectionControlled: isValueControlled(),
|
|
675
913
|
};
|
|
676
914
|
},
|
|
677
915
|
(data) => {
|
|
@@ -691,49 +929,51 @@ export function createComboBoxState<T = unknown>(
|
|
|
691
929
|
// These methods allow createOption to work with ComboBoxState
|
|
692
930
|
const select = (key: Key) => {
|
|
693
931
|
if (isMultiple()) {
|
|
694
|
-
// Toggle selection in multiple mode
|
|
695
932
|
const current = new Set(selectedKeys());
|
|
696
933
|
if (current.has(key)) {
|
|
697
934
|
current.delete(key);
|
|
698
935
|
} else {
|
|
699
936
|
current.add(key);
|
|
700
937
|
}
|
|
701
|
-
|
|
702
|
-
// Don't close menu and don't reset input in multi-select mode
|
|
938
|
+
setValue([...current]);
|
|
703
939
|
} else {
|
|
704
|
-
|
|
940
|
+
setValue(key);
|
|
705
941
|
closeMenu();
|
|
706
942
|
}
|
|
707
943
|
};
|
|
708
944
|
|
|
945
|
+
const setSelectedKeys = (keys: Iterable<Key>) => {
|
|
946
|
+
setValue([...keys]);
|
|
947
|
+
};
|
|
948
|
+
|
|
709
949
|
const removeSelectedKey = (key: Key) => {
|
|
710
|
-
if (isMultiple())
|
|
711
|
-
|
|
712
|
-
current.delete(key);
|
|
713
|
-
setSelectedKeys(current);
|
|
714
|
-
}
|
|
950
|
+
if (!isMultiple()) return;
|
|
951
|
+
setValue([...selectedKeys()].filter((selected) => selected !== key));
|
|
715
952
|
};
|
|
716
953
|
|
|
717
|
-
const selectionMode: Accessor<
|
|
954
|
+
const selectionMode: Accessor<M> = () => (getProps().selectionMode ?? "single") as M;
|
|
718
955
|
|
|
719
|
-
const isSelected = (key: Key) =>
|
|
720
|
-
if (isMultiple()) {
|
|
721
|
-
return selectedKeys().has(key);
|
|
722
|
-
}
|
|
723
|
-
return selectedKey() === key;
|
|
724
|
-
};
|
|
956
|
+
const isSelected = (key: Key) => selectedKeys().has(key);
|
|
725
957
|
|
|
726
958
|
// ---- Return State ----
|
|
727
959
|
return {
|
|
960
|
+
realtimeValidation: validation.realtimeValidation,
|
|
961
|
+
displayValidation: validation.displayValidation,
|
|
962
|
+
updateValidation: validation.updateValidation,
|
|
963
|
+
resetValidation: validation.resetValidation,
|
|
964
|
+
commitValidation: validation.commitValidation,
|
|
728
965
|
collection: displayedCollection,
|
|
729
966
|
isOpen: overlayState.isOpen,
|
|
730
967
|
open,
|
|
731
968
|
close: commitValue,
|
|
732
969
|
toggle,
|
|
733
970
|
selectedKey,
|
|
734
|
-
defaultSelectedKey
|
|
971
|
+
defaultSelectedKey,
|
|
735
972
|
selectedItem,
|
|
736
973
|
setSelectedKey,
|
|
974
|
+
value: () => displayValue() as ComboBoxValueType<M>,
|
|
975
|
+
defaultValue,
|
|
976
|
+
setValue,
|
|
737
977
|
selectedKeys,
|
|
738
978
|
setSelectedKeys,
|
|
739
979
|
selectedItems,
|
|
@@ -766,6 +1006,16 @@ export function createComboBoxState<T = unknown>(
|
|
|
766
1006
|
};
|
|
767
1007
|
}
|
|
768
1008
|
|
|
1009
|
+
function isKeyList(value: Key | readonly Key[] | null): value is readonly Key[] {
|
|
1010
|
+
return value !== null && typeof value !== "string" && typeof value !== "number";
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
function convertValue(value: Key | readonly Key[] | null | undefined): readonly Key[] {
|
|
1014
|
+
if (value == null) return [];
|
|
1015
|
+
if (typeof value === "string" || typeof value === "number") return [value];
|
|
1016
|
+
return value;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
769
1019
|
/**
|
|
770
1020
|
* Filter a collection based on input value.
|
|
771
1021
|
* RAC `useComboBoxState.ts:644-672` copies nodes into a new `ListCollection`,
|