@proyecto-viviana/solid-stately 0.6.0-rc.0 → 0.6.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/README.md +59 -29
  2. package/dist/calendar/createCalendarState.d.ts +3 -1
  3. package/dist/calendar/createCalendarState.d.ts.map +1 -1
  4. package/dist/calendar/createDatePickerState.d.ts +2 -0
  5. package/dist/calendar/createDatePickerState.d.ts.map +1 -1
  6. package/dist/calendar/createRangeCalendarState.d.ts +2 -0
  7. package/dist/calendar/createRangeCalendarState.d.ts.map +1 -1
  8. package/dist/calendar/createTimeFieldState.d.ts +1 -1
  9. package/dist/calendar/createTimeFieldState.d.ts.map +1 -1
  10. package/dist/checkbox/createCheckboxGroupState.d.ts +2 -0
  11. package/dist/checkbox/createCheckboxGroupState.d.ts.map +1 -1
  12. package/dist/collections/createListState.d.ts.map +1 -1
  13. package/dist/collections/createMenuState.d.ts +46 -2
  14. package/dist/collections/createMenuState.d.ts.map +1 -1
  15. package/dist/collections/createSelectionState.d.ts.map +1 -1
  16. package/dist/collections/index.d.ts +1 -1
  17. package/dist/collections/index.d.ts.map +1 -1
  18. package/dist/color/createColorAreaState.d.ts +4 -0
  19. package/dist/color/createColorAreaState.d.ts.map +1 -1
  20. package/dist/color/createColorFieldState.d.ts +2 -0
  21. package/dist/color/createColorFieldState.d.ts.map +1 -1
  22. package/dist/color/createColorSliderState.d.ts +2 -0
  23. package/dist/color/createColorSliderState.d.ts.map +1 -1
  24. package/dist/color/createColorWheelState.d.ts.map +1 -1
  25. package/dist/combobox/createComboBoxState.d.ts +90 -23
  26. package/dist/combobox/createComboBoxState.d.ts.map +1 -1
  27. package/dist/combobox/index.d.ts +1 -1
  28. package/dist/combobox/index.d.ts.map +1 -1
  29. package/dist/data/createAsyncList.d.ts.map +1 -1
  30. package/dist/data/createListData.d.ts.map +1 -1
  31. package/dist/data/createTreeData.d.ts.map +1 -1
  32. package/dist/dnd/createDraggableCollectionState.d.ts +5 -1
  33. package/dist/dnd/createDraggableCollectionState.d.ts.map +1 -1
  34. package/dist/dnd/createDroppableCollectionState.d.ts +6 -0
  35. package/dist/dnd/createDroppableCollectionState.d.ts.map +1 -1
  36. package/dist/form/createFormValidationState.d.ts.map +1 -1
  37. package/dist/grid/createGridState.d.ts +2 -2
  38. package/dist/grid/createGridState.d.ts.map +1 -1
  39. package/dist/index.d.ts +5 -5
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +1044 -478
  42. package/dist/index.js.map +1 -1
  43. package/dist/menu/index.d.ts +2 -1
  44. package/dist/menu/index.d.ts.map +1 -1
  45. package/dist/numberfield/createNumberFieldState.d.ts +6 -3
  46. package/dist/numberfield/createNumberFieldState.d.ts.map +1 -1
  47. package/dist/private/flags/flags.d.ts.map +1 -0
  48. package/dist/private/flags/flags.js +1 -1
  49. package/dist/private/flags/flags.js.map +1 -1
  50. package/dist/radio/createRadioGroupState.d.ts.map +1 -1
  51. package/dist/select/createSelectState.d.ts +85 -29
  52. package/dist/select/createSelectState.d.ts.map +1 -1
  53. package/dist/select/index.d.ts +1 -1
  54. package/dist/select/index.d.ts.map +1 -1
  55. package/dist/selection/SelectionManager.d.ts.map +1 -1
  56. package/dist/selection/createMultipleSelectionState.d.ts.map +1 -1
  57. package/dist/slider/createSliderState.d.ts +35 -9
  58. package/dist/slider/createSliderState.d.ts.map +1 -1
  59. package/dist/ssr/index.d.ts +0 -6
  60. package/dist/ssr/index.d.ts.map +1 -1
  61. package/dist/steplist/createStepListState.d.ts +8 -13
  62. package/dist/steplist/createStepListState.d.ts.map +1 -1
  63. package/dist/table/createTableColumnResizeState.d.ts.map +1 -1
  64. package/dist/table/createTableState.d.ts +2 -2
  65. package/dist/table/createTableState.d.ts.map +1 -1
  66. package/dist/table/createTreeGridState.d.ts +2 -2
  67. package/dist/table/createTreeGridState.d.ts.map +1 -1
  68. package/dist/table/types.d.ts +2 -2
  69. package/dist/table/types.d.ts.map +1 -1
  70. package/dist/tabs/createTabListState.d.ts.map +1 -1
  71. package/dist/textfield/createTextFieldState.d.ts.map +1 -1
  72. package/dist/toggle/createToggleGroupState.d.ts +0 -8
  73. package/dist/toggle/createToggleGroupState.d.ts.map +1 -1
  74. package/dist/toggle/createToggleState.d.ts.map +1 -1
  75. package/dist/tokenfield/TokenFieldValue.d.ts +28 -4
  76. package/dist/tokenfield/TokenFieldValue.d.ts.map +1 -1
  77. package/dist/tokenfield/index.d.ts +1 -1
  78. package/dist/tokenfield/index.d.ts.map +1 -1
  79. package/dist/tree/createTreeState.d.ts +2 -2
  80. package/dist/tree/createTreeState.d.ts.map +1 -1
  81. package/dist/utils/index.d.ts +1 -1
  82. package/dist/utils/index.d.ts.map +1 -1
  83. package/dist/utils/reactivity.d.ts +5 -0
  84. package/dist/utils/reactivity.d.ts.map +1 -1
  85. package/package.json +2 -2
  86. package/src/calendar/createCalendarState.ts +139 -58
  87. package/src/calendar/createDatePickerState.ts +14 -11
  88. package/src/calendar/createRangeCalendarState.ts +43 -17
  89. package/src/calendar/createTimeFieldState.ts +1 -1
  90. package/src/checkbox/createCheckboxGroupState.ts +16 -0
  91. package/src/collections/createListState.ts +4 -4
  92. package/src/collections/createMenuState.ts +134 -4
  93. package/src/collections/createSelectionState.ts +2 -2
  94. package/src/collections/index.ts +4 -0
  95. package/src/color/createColorAreaState.ts +19 -0
  96. package/src/color/createColorFieldState.ts +76 -26
  97. package/src/color/createColorSliderState.ts +25 -2
  98. package/src/color/createColorWheelState.ts +9 -5
  99. package/src/combobox/createComboBoxState.ts +371 -121
  100. package/src/combobox/index.ts +4 -0
  101. package/src/data/createAsyncList.ts +19 -4
  102. package/src/data/createListData.ts +8 -2
  103. package/src/data/createTreeData.ts +11 -3
  104. package/src/dnd/createDraggableCollectionState.ts +8 -1
  105. package/src/dnd/createDroppableCollectionState.ts +10 -0
  106. package/src/form/createFormValidationState.ts +23 -5
  107. package/src/grid/createGridState.ts +15 -8
  108. package/src/index.ts +18 -2
  109. package/src/menu/index.ts +5 -0
  110. package/src/numberfield/createNumberFieldState.ts +97 -35
  111. package/src/radio/createRadioGroupState.ts +4 -1
  112. package/src/select/createSelectState.ts +253 -131
  113. package/src/select/index.ts +8 -1
  114. package/src/selection/SelectionManager.ts +3 -1
  115. package/src/selection/createMultipleSelectionState.ts +2 -2
  116. package/src/slider/createSliderState.ts +227 -103
  117. package/src/ssr/index.ts +11 -4
  118. package/src/steplist/createStepListState.ts +156 -123
  119. package/src/table/createTableColumnResizeState.ts +51 -11
  120. package/src/table/createTableState.ts +5 -4
  121. package/src/table/createTreeGridState.ts +3 -4
  122. package/src/table/types.ts +2 -2
  123. package/src/tabs/createTabListState.ts +8 -31
  124. package/src/textfield/createTextFieldState.ts +6 -3
  125. package/src/toggle/createToggleGroupState.ts +4 -2
  126. package/src/toggle/createToggleState.ts +5 -4
  127. package/src/tokenfield/TokenFieldValue.ts +75 -16
  128. package/src/tokenfield/index.ts +1 -0
  129. package/src/tree/createTreeState.ts +15 -8
  130. package/src/utils/index.ts +1 -0
  131. package/src/utils/reactivity.ts +8 -0
  132. package/dist/flags/flags.d.ts.map +0 -1
  133. /package/dist/{flags → private/flags}/flags.d.ts +0 -0
  134. /package/src/{flags → private/flags}/flags.ts +0 -0
@@ -17,13 +17,12 @@
17
17
  * Based on @react-stately/select useSelectState.
18
18
  */
19
19
 
20
- import { createMemo } from "solid-js";
20
+ import { createMemo, untrack } from "solid-js";
21
21
  import type { Accessor } from "solid-js";
22
- import { createInternalSignal, access, type MaybeAccessor } from "../utils";
22
+ import { createInternalSignal, propsAccessor, type MaybeAccessor } from "../utils";
23
23
  import { createListState } from "../collections/createListState";
24
24
  import { createOverlayTriggerState } from "../overlays";
25
- import type { Key, CollectionNode, Collection } from "../collections/types";
26
- import type { SelectionMode, Selection } from "../collections/types";
25
+ import type { Key, CollectionNode, Collection, FocusStrategy } from "../collections/types";
27
26
  import type { SelectionManager } from "../selection/SelectionManager";
28
27
  import {
29
28
  createFormValidationState,
@@ -31,7 +30,22 @@ import {
31
30
  type ValidationFunction,
32
31
  } from "../form";
33
32
 
34
- export interface SelectStateProps<T = unknown> {
33
+ /** Select never uses the collection `"none"` mode. */
34
+ export type SelectSelectionMode = "single" | "multiple";
35
+
36
+ /** Single mode is `Key | null`. Multiple mode is `readonly Key[]`. */
37
+ export type SelectValueType<M extends SelectSelectionMode> = M extends "single"
38
+ ? Key | null
39
+ : readonly Key[];
40
+
41
+ /** `onChange` receives a mutable `Key[]` in multiple mode. */
42
+ export type SelectChangeValueType<M extends SelectSelectionMode> = M extends "single"
43
+ ? Key | null
44
+ : Key[];
45
+
46
+ type SelectValidationType<M extends SelectSelectionMode> = M extends "single" ? Key : Key[];
47
+
48
+ export interface SelectStateProps<T = unknown, M extends SelectSelectionMode = "single"> {
35
49
  /** The items to display in the select. */
36
50
  items: T[];
37
51
  /** Function to get the key for an item. */
@@ -42,26 +56,48 @@ export interface SelectStateProps<T = unknown> {
42
56
  getDisabled?: (item: T) => boolean;
43
57
  /** Keys of disabled items. */
44
58
  disabledKeys?: Iterable<Key>;
45
- /** The currently selected key (controlled). */
59
+ /**
60
+ * Whether single or multiple selection is enabled.
61
+ * @default 'single'
62
+ */
63
+ selectionMode?: M;
64
+ /**
65
+ * The current value (controlled).
66
+ * Single mode is `Key | null`. Multiple mode is `readonly Key[]`.
67
+ */
68
+ value?: SelectValueType<M>;
69
+ /** The default value (uncontrolled). */
70
+ defaultValue?: SelectValueType<M>;
71
+ /** Handler called when the value changes. */
72
+ onChange?: (value: SelectChangeValueType<M>) => void;
73
+ /**
74
+ * The currently selected key in the collection (controlled).
75
+ * @deprecated Use `value`.
76
+ */
46
77
  selectedKey?: Key | null;
47
- /** The default selected key (uncontrolled). */
78
+ /**
79
+ * The initial selected key in the collection (uncontrolled).
80
+ * @deprecated Use `defaultValue`.
81
+ */
48
82
  defaultSelectedKey?: Key | null;
49
- /** The selected keys (controlled, for multiple selection mode). */
50
- selectedKeys?: "all" | Iterable<Key>;
51
- /** Default selected keys (uncontrolled, for multiple selection mode). */
52
- defaultSelectedKeys?: "all" | Iterable<Key>;
53
- /** Handler called when the selection changes. */
83
+ /**
84
+ * Handler that is called when the selection changes.
85
+ * @deprecated Use `onChange`.
86
+ */
54
87
  onSelectionChange?: (key: Key | null) => void;
55
- /** Handler called when selected keys change. */
56
- onSelectionChangeKeys?: (keys: Selection) => void;
57
- /** Selection mode for the select. */
58
- selectionMode?: Extract<SelectionMode, "single" | "multiple">;
59
88
  /** Whether the select is open (controlled). */
60
89
  isOpen?: boolean;
61
90
  /** Whether the select is open by default (uncontrolled). */
62
91
  defaultOpen?: boolean;
63
92
  /** Handler called when the open state changes. */
64
93
  onOpenChange?: (isOpen: boolean) => void;
94
+ /**
95
+ * Whether the Select should close when an item is selected. Defaults to true
96
+ * when selectionMode is single, false otherwise.
97
+ */
98
+ shouldCloseOnSelect?: boolean;
99
+ /** Whether the select should be allowed to be open when the collection is empty. */
100
+ allowsEmptyCollection?: boolean;
65
101
  /** Whether the select is disabled. */
66
102
  isDisabled?: boolean;
67
103
  /** Whether the select is required. */
@@ -71,7 +107,7 @@ export interface SelectStateProps<T = unknown> {
71
107
  /** @deprecated Use isInvalid instead. */
72
108
  validationState?: "valid" | "invalid";
73
109
  /** Custom validation function. */
74
- validate?: ValidationFunction<Key | null | Selection>;
110
+ validate?: ValidationFunction<SelectValidationType<M> | null>;
75
111
  /**
76
112
  * Whether to use native HTML form validation or ARIA validation semantics.
77
113
  * @default "native"
@@ -81,7 +117,10 @@ export interface SelectStateProps<T = unknown> {
81
117
  name?: string | string[];
82
118
  }
83
119
 
84
- export interface SelectState<T = unknown> extends FormValidationState {
120
+ export interface SelectState<
121
+ T = unknown,
122
+ M extends SelectSelectionMode = "single",
123
+ > extends FormValidationState {
85
124
  /** The collection of items. */
86
125
  readonly collection: Accessor<Collection<T>>;
87
126
  /**
@@ -92,23 +131,48 @@ export interface SelectState<T = unknown> extends FormValidationState {
92
131
  /** Whether the select dropdown is open. */
93
132
  readonly isOpen: Accessor<boolean>;
94
133
  /** Open the select dropdown. */
95
- open(): void;
134
+ open(focusStrategy?: FocusStrategy | null): void;
96
135
  /** Close the select dropdown. */
97
136
  close(): void;
98
137
  /** Toggle the select dropdown. */
99
- toggle(): void;
100
- /** The currently selected key. */
138
+ toggle(focusStrategy?: FocusStrategy | null): void;
139
+ /** Controls which item will be auto focused when the menu opens. */
140
+ readonly focusStrategy: Accessor<FocusStrategy | null>;
141
+ /**
142
+ * The key for the first selected item.
143
+ * @deprecated Use `value`.
144
+ */
101
145
  readonly selectedKey: Accessor<Key | null>;
102
- /** The selected keys. */
103
- readonly selectedKeys: Accessor<Selection>;
104
- /** The currently selected item. */
105
- readonly selectedItem: Accessor<CollectionNode<T> | null>;
106
- /** The currently selected items. */
107
- readonly selectedItems: Accessor<CollectionNode<T>[]>;
108
- /** Set the selected key. */
146
+ /**
147
+ * The default selected key.
148
+ * @deprecated Use `defaultValue`.
149
+ */
150
+ readonly defaultSelectedKey: Key | null;
151
+ /**
152
+ * Sets the selected key.
153
+ * @deprecated Use `setValue`.
154
+ */
109
155
  setSelectedKey(key: Key | null): void;
110
- /** Set selected keys. */
156
+ /** The current select value. */
157
+ readonly value: Accessor<SelectValueType<M>>;
158
+ /** The default select value. */
159
+ readonly defaultValue: SelectValueType<M>;
160
+ /** Sets the select value. */
161
+ setValue(value: Key | readonly Key[] | null): void;
162
+ /**
163
+ * Selected keys derived from `value`.
164
+ * Kept for list and hidden-select adapters. Never `"all"`.
165
+ */
166
+ readonly selectedKeys: Accessor<Set<Key>>;
167
+ /** Replace the selected keys. */
111
168
  setSelectedKeys(keys: Iterable<Key>): void;
169
+ /**
170
+ * The value of the first selected item.
171
+ * @deprecated Use `selectedItems`.
172
+ */
173
+ readonly selectedItem: Accessor<CollectionNode<T> | null>;
174
+ /** The value of the selected items. */
175
+ readonly selectedItems: Accessor<CollectionNode<T>[]>;
112
176
  /** The currently focused key. */
113
177
  readonly focusedKey: Accessor<Key | null>;
114
178
  /** Set the focused key. */
@@ -124,20 +188,29 @@ export interface SelectState<T = unknown> extends FormValidationState {
124
188
  /** Whether the select is required. */
125
189
  readonly isRequired: boolean;
126
190
  /** The selection mode. */
127
- readonly selectionMode: Accessor<"single" | "multiple">;
191
+ readonly selectionMode: Accessor<M>;
192
+ /** Whether selecting an item closes the menu. */
193
+ readonly shouldCloseOnSelect: Accessor<boolean>;
194
+ }
195
+
196
+ function convertValue(value: Key | readonly Key[] | null | undefined): readonly Key[] | undefined {
197
+ if (value === undefined) return undefined;
198
+ if (value === null || typeof value === "string" || typeof value === "number") {
199
+ return value == null ? [] : [value];
200
+ }
201
+ return value;
128
202
  }
129
203
 
130
204
  /**
131
205
  * Creates state for a select component.
132
206
  * Combines list state with overlay trigger state for dropdown behavior.
133
207
  */
134
- export function createSelectState<T = unknown>(
135
- props: MaybeAccessor<SelectStateProps<T>>,
136
- ): SelectState<T> {
137
- const getProps = () => access(props);
138
- const selectionMode: Accessor<"single" | "multiple"> = () => getProps().selectionMode ?? "single";
208
+ export function createSelectState<T = unknown, M extends SelectSelectionMode = "single">(
209
+ props: MaybeAccessor<SelectStateProps<T, M>>,
210
+ ): SelectState<T, M> {
211
+ const getProps = propsAccessor(props);
212
+ const selectionMode: Accessor<M> = () => (getProps().selectionMode ?? "single") as M;
139
213
 
140
- // Overlay trigger state for open/close
141
214
  const overlayState = createOverlayTriggerState({
142
215
  get isOpen() {
143
216
  return getProps().isOpen;
@@ -150,85 +223,137 @@ export function createSelectState<T = unknown>(
150
223
  },
151
224
  });
152
225
 
153
- // Track selected key
154
- const isControlledSingle = () => getProps().selectedKey !== undefined;
155
- const isControlledMultiple = () => getProps().selectedKeys !== undefined;
156
- const [internalSelectedKey, setInternalSelectedKey] = createInternalSignal<Key | null>(
157
- getProps().defaultSelectedKey ?? null,
158
- );
159
- const [internalSelectedKeys, setInternalSelectedKeys] = createInternalSignal<Selection>(
160
- getProps().defaultSelectedKeys === "all"
161
- ? "all"
162
- : new Set(getProps().defaultSelectedKeys ?? []),
163
- );
226
+ const resolvedDefault = untrack((): Key | readonly Key[] | null => {
227
+ const current = getProps();
228
+ if (current.defaultValue !== undefined) {
229
+ return current.defaultValue as Key | readonly Key[] | null;
230
+ }
231
+ const mode = current.selectionMode ?? "single";
232
+ return mode === "single" ? (current.defaultSelectedKey ?? null) : [];
233
+ });
164
234
 
165
- const selectedKey: Accessor<Key | null> = () => {
166
- if (selectionMode() === "multiple") {
167
- const keys = selectedKeys();
168
- if (keys === "all") return null;
169
- return keys.size > 0 ? Array.from(keys)[0] : null;
235
+ const controlledProp = (): SelectValueType<M> | undefined => {
236
+ const current = getProps();
237
+ if (current.value !== undefined) return current.value;
238
+ if (selectionMode() === "single" && current.selectedKey !== undefined) {
239
+ return (current.selectedKey ?? null) as SelectValueType<M>;
170
240
  }
241
+ return undefined;
242
+ };
243
+
244
+ const [internalValue, setInternalValue] = createInternalSignal<Key | readonly Key[] | null>(
245
+ resolvedDefault,
246
+ );
171
247
 
172
- return isControlledSingle() ? (getProps().selectedKey ?? null) : internalSelectedKey();
248
+ const readControlled = (): Key | readonly Key[] | null | undefined => {
249
+ const controlled = controlledProp();
250
+ return controlled === undefined ? undefined : (controlled as Key | readonly Key[] | null);
173
251
  };
174
252
 
175
- const setSelectedKey = (key: Key | null) => {
176
- if (selectionMode() === "multiple") {
177
- if (key == null) {
178
- if (!isControlledMultiple()) {
179
- setInternalSelectedKeys(new Set<Key>());
180
- }
181
- getProps().onSelectionChangeKeys?.(new Set<Key>());
182
- getProps().onSelectionChange?.(null);
183
- } else {
184
- const next = new Set([key]);
185
- if (!isControlledMultiple()) {
186
- setInternalSelectedKeys(next);
253
+ let valueRef: Key | readonly Key[] | null = untrack(() => {
254
+ const controlled = readControlled();
255
+ return controlled !== undefined ? controlled : internalValue();
256
+ });
257
+ let wroteThisTurn = false;
258
+
259
+ const syncValueRef = () => {
260
+ if (wroteThisTurn) return;
261
+ const controlled = readControlled();
262
+ valueRef = controlled !== undefined ? controlled : untrack(internalValue);
263
+ };
264
+
265
+ const displayValue = (): Key | readonly Key[] | null => {
266
+ const controlled = readControlled();
267
+ const raw = controlled !== undefined ? controlled : internalValue();
268
+ if (
269
+ selectionMode() === "single" &&
270
+ raw !== null &&
271
+ typeof raw !== "string" &&
272
+ typeof raw !== "number"
273
+ ) {
274
+ return raw[0] ?? null;
275
+ }
276
+ return raw;
277
+ };
278
+
279
+ const setValue = (next: Key | readonly Key[] | null) => {
280
+ syncValueRef();
281
+ if (selectionMode() === "single") {
282
+ const key: Key | null =
283
+ typeof next === "string" || typeof next === "number"
284
+ ? next
285
+ : next === null
286
+ ? null
287
+ : (next[0] ?? null);
288
+ // Both callbacks follow this request, not a comparison with displayValue.
289
+ // A refused controlled value stays stale for the turn, so a second
290
+ // same-key call would notify the legacy callback again. React Stately
291
+ // still compares displayValue; this is the Solid same-turn adaptation.
292
+ // A later turn resyncs valueRef, and a different key is its own request.
293
+ if (!Object.is(valueRef, key)) {
294
+ valueRef = key;
295
+ wroteThisTurn = true;
296
+ queueMicrotask(() => {
297
+ wroteThisTurn = false;
298
+ });
299
+ if (controlledProp() === undefined) {
300
+ setInternalValue(key);
187
301
  }
188
- getProps().onSelectionChangeKeys?.(next);
302
+ getProps().onChange?.(key as SelectChangeValueType<M>);
189
303
  getProps().onSelectionChange?.(key);
190
304
  }
191
305
  return;
192
306
  }
193
307
 
194
- if (!isControlledSingle()) {
195
- setInternalSelectedKey(key);
196
- }
197
- getProps().onSelectionChange?.(key);
198
- getProps().onSelectionChangeKeys?.(key != null ? new Set([key]) : new Set<Key>());
199
- };
200
-
201
- const selectedKeys: Accessor<Selection> = createMemo(() => {
202
- if (selectionMode() === "multiple") {
203
- if (isControlledMultiple()) {
204
- const keys = getProps().selectedKeys;
205
- return keys === "all" ? "all" : new Set<Key>(keys ?? []);
308
+ // Keep the readonly list for state. `onChange` receives its own mutable copy.
309
+ const keys: readonly Key[] =
310
+ typeof next === "string" || typeof next === "number" ? [next] : next === null ? [] : next;
311
+ if (!Object.is(valueRef, keys)) {
312
+ valueRef = keys;
313
+ wroteThisTurn = true;
314
+ queueMicrotask(() => {
315
+ wroteThisTurn = false;
316
+ });
317
+ if (controlledProp() === undefined) {
318
+ setInternalValue(keys);
206
319
  }
207
- return internalSelectedKeys();
320
+ getProps().onChange?.([...keys] as SelectChangeValueType<M>);
208
321
  }
209
-
210
- const key = selectedKey();
211
- return key != null ? new Set<Key>([key]) : new Set<Key>();
212
- });
322
+ };
213
323
 
214
324
  const setSelectedKeys = (keys: Iterable<Key>) => {
215
- const next = new Set(keys);
216
325
  if (selectionMode() === "multiple") {
217
- if (!isControlledMultiple()) {
218
- setInternalSelectedKeys(next);
219
- }
220
- getProps().onSelectionChangeKeys?.(next);
221
- getProps().onSelectionChange?.(next.size > 0 ? Array.from(next)[0] : null);
326
+ setValue([...keys]);
222
327
  return;
223
328
  }
224
-
225
- const key = next.size > 0 ? Array.from(next)[0] : null;
226
- setSelectedKey(key);
329
+ let key: Key | null = null;
330
+ for (const item of keys) {
331
+ key = item;
332
+ break;
333
+ }
334
+ setValue(key);
227
335
  };
228
336
 
337
+ const initialDisplay = untrack(displayValue);
338
+ const stateDefaultValue = (resolvedDefault ?? initialDisplay) as SelectValueType<M>;
339
+ const defaultSelectedKey =
340
+ untrack(() => getProps().defaultSelectedKey) ??
341
+ (untrack(() => getProps().selectionMode) === "single" ? (initialDisplay as Key) : null);
342
+
343
+ const shouldCloseOnSelect = () => getProps().shouldCloseOnSelect ?? selectionMode() === "single";
344
+
229
345
  const validation = createFormValidationState({
230
346
  get value() {
231
- return selectionMode() === "multiple" ? selectedKeys() : selectedKey();
347
+ const display = displayValue();
348
+ if (
349
+ display !== null &&
350
+ typeof display !== "string" &&
351
+ typeof display !== "number" &&
352
+ display.length === 0
353
+ ) {
354
+ return null;
355
+ }
356
+ return display;
232
357
  },
233
358
  get isInvalid() {
234
359
  return getProps().isInvalid;
@@ -237,7 +362,7 @@ export function createSelectState<T = unknown>(
237
362
  return getProps().validationState;
238
363
  },
239
364
  get validate() {
240
- return getProps().validate;
365
+ return getProps().validate as ValidationFunction<unknown> | undefined;
241
366
  },
242
367
  get name() {
243
368
  return getProps().name;
@@ -247,7 +372,6 @@ export function createSelectState<T = unknown>(
247
372
  },
248
373
  });
249
374
 
250
- // Create list state with select selection mode
251
375
  const listState = createListState<T>({
252
376
  get items() {
253
377
  return getProps().items;
@@ -274,39 +398,32 @@ export function createSelectState<T = unknown>(
274
398
  },
275
399
  allowDuplicateSelectionEvents: true,
276
400
  get selectedKeys() {
277
- const keys = selectedKeys();
278
- if (keys === "all") return "all";
279
- return keys;
401
+ return convertValue(displayValue()) ?? [];
280
402
  },
281
403
  onSelectionChange(keys) {
282
- if (selectionMode() === "multiple") {
283
- if (!isControlledMultiple()) {
284
- setInternalSelectedKeys(keys);
285
- }
286
- getProps().onSelectionChangeKeys?.(keys);
287
- if (keys !== "all") {
288
- getProps().onSelectionChange?.(keys.size > 0 ? Array.from(keys)[0] : null);
289
- }
290
- validation.commitValidation();
291
- return;
292
- }
293
-
294
- // Get the first (and only) selected key
295
404
  if (keys === "all") return;
296
- const key = keys.size > 0 ? Array.from(keys)[0] : null;
297
- setSelectedKey(key);
298
- overlayState.close();
405
+ if (selectionMode() === "single") {
406
+ setValue(keys.values().next().value ?? null);
407
+ } else {
408
+ setValue([...keys]);
409
+ }
410
+ if (shouldCloseOnSelect()) {
411
+ overlayState.close();
412
+ }
299
413
  validation.commitValidation();
300
414
  },
301
415
  });
302
416
 
417
+ const [focusStrategy, setFocusStrategy] = createInternalSignal<FocusStrategy | null>(null);
303
418
  // The select's own focus state (the trigger), separate from the collection's
304
419
  // focus-within state on the selection manager — upstream useSelectState keeps
305
420
  // these apart with a dedicated useState. Sharing the manager's signal makes
306
421
  // trigger focus re-arm the item roving-focus effect and steal focus back.
307
422
  const [isFocused, setFocused] = createInternalSignal(false);
308
423
 
309
- // Get the selected item from the collection (memoized)
424
+ const selectedKey: Accessor<Key | null> = () => listState.selectionManager.firstSelectedKey;
425
+ const selectedKeys: Accessor<Set<Key>> = () => new Set(convertValue(displayValue()) ?? []);
426
+
310
427
  const selectedItem: Accessor<CollectionNode<T> | null> = createMemo(() => {
311
428
  const key = selectedKey();
312
429
  if (key == null) return null;
@@ -314,48 +431,53 @@ export function createSelectState<T = unknown>(
314
431
  });
315
432
 
316
433
  const selectedItems: Accessor<CollectionNode<T>[]> = createMemo(() => {
317
- const keys = selectedKeys();
318
- if (keys === "all") {
319
- return Array.from(listState.collection());
320
- }
321
-
322
434
  const items: CollectionNode<T>[] = [];
323
- for (const key of keys) {
435
+ for (const key of listState.selectionManager.selectedKeys) {
324
436
  const item = listState.collection().getItem(key);
325
437
  if (item) items.push(item);
326
438
  }
327
439
  return items;
328
440
  });
329
441
 
442
+ const canOpenMenu = () =>
443
+ listState.collection().size !== 0 || getProps().allowsEmptyCollection === true;
444
+
330
445
  return {
331
446
  realtimeValidation: validation.realtimeValidation,
332
447
  displayValidation: validation.displayValidation,
333
448
  updateValidation: validation.updateValidation,
334
449
  resetValidation: validation.resetValidation,
335
450
  commitValidation: validation.commitValidation,
336
- // Collection
337
451
  collection: listState.collection,
338
452
  selectionManager: listState.selectionManager,
339
-
340
- // Focus management
341
453
  focusedKey: listState.focusedKey,
342
454
  setFocusedKey: listState.setFocusedKey,
343
455
  isFocused,
344
456
  setFocused,
345
-
346
- // Overlay state
347
457
  isOpen: overlayState.isOpen,
348
- open: overlayState.open,
458
+ open(strategy: FocusStrategy | null = null) {
459
+ if (!canOpenMenu()) return;
460
+ setFocusStrategy(strategy);
461
+ overlayState.open();
462
+ },
349
463
  close: overlayState.close,
350
- toggle: overlayState.toggle,
351
-
352
- // Select-specific
464
+ toggle(strategy: FocusStrategy | null = null) {
465
+ if (!canOpenMenu()) return;
466
+ setFocusStrategy(strategy);
467
+ overlayState.toggle();
468
+ },
469
+ focusStrategy,
353
470
  selectionMode,
471
+ shouldCloseOnSelect,
472
+ value: () => displayValue() as SelectValueType<M>,
473
+ defaultValue: stateDefaultValue,
474
+ setValue,
354
475
  selectedKey,
476
+ defaultSelectedKey,
477
+ setSelectedKey: setValue,
355
478
  selectedKeys,
356
479
  selectedItem,
357
480
  selectedItems,
358
- setSelectedKey,
359
481
  setSelectedKeys,
360
482
  isKeyDisabled: (key: Key) => listState.isDisabled(key),
361
483
  get isDisabled() {
@@ -1 +1,8 @@
1
- export { createSelectState, type SelectStateProps, type SelectState } from "./createSelectState";
1
+ export {
2
+ createSelectState,
3
+ type SelectStateProps,
4
+ type SelectState,
5
+ type SelectSelectionMode,
6
+ type SelectValueType,
7
+ type SelectChangeValueType,
8
+ } from "./createSelectState";
@@ -518,7 +518,9 @@ export class SelectionManager<T = unknown> {
518
518
  const item = this.collection.getItem(key);
519
519
  return (
520
520
  this.state.disabledBehavior === "all" &&
521
- (this.state.disabledKeys.has(key) || !!item?.props?.isDisabled) &&
521
+ (this.state.disabledKeys.has(key) ||
522
+ !!item?.props?.isDisabled ||
523
+ item?.isDisabled === true) &&
522
524
  item?.props?.disabledBehavior !== "selection"
523
525
  );
524
526
  }
@@ -25,7 +25,7 @@
25
25
 
26
26
  import { createMemo, createEffect } from "solid-js";
27
27
  import type { Accessor } from "solid-js";
28
- import { access, createInternalSignal, readNow, type MaybeAccessor } from "../utils";
28
+ import { createInternalSignal, propsAccessor, readNow, type MaybeAccessor } from "../utils";
29
29
  import type { SelectionStateProps } from "../collections/createSelectionState";
30
30
  import type {
31
31
  DisabledBehavior,
@@ -114,7 +114,7 @@ function convertSelection(
114
114
  export function createMultipleSelectionState(
115
115
  props: MaybeAccessor<SelectionStateProps> = {},
116
116
  ): MultipleSelectionState {
117
- const getProps = () => access(props);
117
+ const getProps = propsAccessor(props);
118
118
 
119
119
  // Focus is a plain signal in Solid; upstream needs a ref + state pair only
120
120
  // because React requires both a synchronous read and a re-render trigger.