@proyecto-viviana/solid-stately 0.2.2 → 0.2.4
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/index.js +43 -43
- package/dist/index.js.map +1 -1
- package/package.json +7 -5
- package/src/autocomplete/createAutocompleteState.d.ts +46 -0
- package/src/autocomplete/createAutocompleteState.d.ts.map +1 -0
- package/src/autocomplete/createAutocompleteState.ts +90 -0
- package/src/autocomplete/index.d.ts +2 -0
- package/src/autocomplete/index.d.ts.map +1 -0
- package/src/autocomplete/index.ts +5 -0
- package/src/calendar/createCalendarState.d.ts +130 -0
- package/src/calendar/createCalendarState.d.ts.map +1 -0
- package/src/calendar/createCalendarState.ts +461 -0
- package/src/calendar/createDateFieldState.d.ts +110 -0
- package/src/calendar/createDateFieldState.d.ts.map +1 -0
- package/src/calendar/createDateFieldState.ts +562 -0
- package/src/calendar/createRangeCalendarState.d.ts +146 -0
- package/src/calendar/createRangeCalendarState.d.ts.map +1 -0
- package/src/calendar/createRangeCalendarState.ts +535 -0
- package/src/calendar/createTimeFieldState.d.ts +95 -0
- package/src/calendar/createTimeFieldState.d.ts.map +1 -0
- package/src/calendar/createTimeFieldState.ts +483 -0
- package/src/calendar/index.d.ts +7 -0
- package/src/calendar/index.d.ts.map +1 -0
- package/src/calendar/index.ts +81 -0
- package/src/checkbox/createCheckboxGroupState.d.ts +71 -0
- package/src/checkbox/createCheckboxGroupState.d.ts.map +1 -0
- package/src/checkbox/createCheckboxGroupState.ts +193 -0
- package/src/checkbox/index.d.ts +2 -0
- package/src/checkbox/index.d.ts.map +1 -0
- package/src/checkbox/index.ts +5 -0
- package/src/collections/ListCollection.d.ts +37 -0
- package/src/collections/ListCollection.d.ts.map +1 -0
- package/src/collections/ListCollection.ts +146 -0
- package/src/collections/createListState.d.ts +79 -0
- package/src/collections/createListState.d.ts.map +1 -0
- package/src/collections/createListState.ts +264 -0
- package/src/collections/createMenuState.d.ts +50 -0
- package/src/collections/createMenuState.d.ts.map +1 -0
- package/src/collections/createMenuState.ts +106 -0
- package/src/collections/createSelectionState.d.ts +76 -0
- package/src/collections/createSelectionState.d.ts.map +1 -0
- package/src/collections/createSelectionState.ts +336 -0
- package/src/collections/index.d.ts +6 -0
- package/src/collections/index.d.ts.map +1 -0
- package/src/collections/index.ts +46 -0
- package/src/collections/types.d.ts +147 -0
- package/src/collections/types.d.ts.map +1 -0
- package/src/collections/types.ts +169 -0
- package/src/color/Color.d.ts +28 -0
- package/src/color/Color.d.ts.map +1 -0
- package/src/color/Color.ts +951 -0
- package/src/color/createColorAreaState.d.ts +76 -0
- package/src/color/createColorAreaState.d.ts.map +1 -0
- package/src/color/createColorAreaState.ts +293 -0
- package/src/color/createColorFieldState.d.ts +55 -0
- package/src/color/createColorFieldState.d.ts.map +1 -0
- package/src/color/createColorFieldState.ts +292 -0
- package/src/color/createColorSliderState.d.ts +67 -0
- package/src/color/createColorSliderState.d.ts.map +1 -0
- package/src/color/createColorSliderState.ts +241 -0
- package/src/color/createColorWheelState.d.ts +51 -0
- package/src/color/createColorWheelState.d.ts.map +1 -0
- package/src/color/createColorWheelState.ts +211 -0
- package/src/color/index.d.ts +10 -0
- package/src/color/index.d.ts.map +1 -0
- package/src/color/index.ts +47 -0
- package/src/color/types.d.ts +106 -0
- package/src/color/types.d.ts.map +1 -0
- package/src/color/types.ts +127 -0
- package/src/combobox/createComboBoxState.d.ts +125 -0
- package/src/combobox/createComboBoxState.d.ts.map +1 -0
- package/src/combobox/createComboBoxState.ts +703 -0
- package/src/combobox/index.d.ts +5 -0
- package/src/combobox/index.d.ts.map +1 -0
- package/src/combobox/index.ts +13 -0
- package/src/disclosure/createDisclosureState.d.ts +64 -0
- package/src/disclosure/createDisclosureState.d.ts.map +1 -0
- package/src/disclosure/createDisclosureState.ts +193 -0
- package/src/disclosure/index.d.ts +2 -0
- package/src/disclosure/index.d.ts.map +1 -0
- package/src/disclosure/index.ts +9 -0
- package/src/dnd/createDragState.d.ts +59 -0
- package/src/dnd/createDragState.d.ts.map +1 -0
- package/src/dnd/createDragState.ts +153 -0
- package/src/dnd/createDraggableCollectionState.d.ts +57 -0
- package/src/dnd/createDraggableCollectionState.d.ts.map +1 -0
- package/src/dnd/createDraggableCollectionState.ts +165 -0
- package/src/dnd/createDropState.d.ts +61 -0
- package/src/dnd/createDropState.d.ts.map +1 -0
- package/src/dnd/createDropState.ts +212 -0
- package/src/dnd/createDroppableCollectionState.d.ts +78 -0
- package/src/dnd/createDroppableCollectionState.d.ts.map +1 -0
- package/src/dnd/createDroppableCollectionState.ts +357 -0
- package/src/dnd/index.d.ts +11 -0
- package/src/dnd/index.d.ts.map +1 -0
- package/src/dnd/index.ts +76 -0
- package/src/dnd/types.d.ts +264 -0
- package/src/dnd/types.d.ts.map +1 -0
- package/src/dnd/types.ts +317 -0
- package/src/form/createFormValidationState.d.ts +100 -0
- package/src/form/createFormValidationState.d.ts.map +1 -0
- package/src/form/createFormValidationState.ts +389 -0
- package/src/form/index.d.ts +2 -0
- package/src/form/index.d.ts.map +1 -0
- package/src/form/index.ts +15 -0
- package/src/grid/createGridState.d.ts +12 -0
- package/src/grid/createGridState.d.ts.map +1 -0
- package/src/grid/createGridState.ts +327 -0
- package/src/grid/index.d.ts +7 -0
- package/src/grid/index.d.ts.map +1 -0
- package/src/grid/index.ts +13 -0
- package/src/grid/types.d.ts +156 -0
- package/src/grid/types.d.ts.map +1 -0
- package/src/grid/types.ts +179 -0
- package/src/index.d.ts +26 -0
- package/src/index.d.ts.map +1 -0
- package/src/index.ts +383 -0
- package/src/numberfield/createNumberFieldState.d.ts +65 -0
- package/src/numberfield/createNumberFieldState.d.ts.map +1 -0
- package/src/numberfield/createNumberFieldState.ts +383 -0
- package/src/numberfield/index.d.ts +2 -0
- package/src/numberfield/index.d.ts.map +1 -0
- package/src/numberfield/index.ts +5 -0
- package/src/overlays/createOverlayTriggerState.d.ts +32 -0
- package/src/overlays/createOverlayTriggerState.d.ts.map +1 -0
- package/src/overlays/createOverlayTriggerState.ts +67 -0
- package/src/overlays/index.d.ts +2 -0
- package/src/overlays/index.d.ts.map +1 -0
- package/src/overlays/index.ts +5 -0
- package/src/radio/createRadioGroupState.d.ts +77 -0
- package/src/radio/createRadioGroupState.d.ts.map +1 -0
- package/src/radio/createRadioGroupState.ts +201 -0
- package/src/radio/index.d.ts +2 -0
- package/src/radio/index.d.ts.map +1 -0
- package/src/radio/index.ts +6 -0
- package/src/searchfield/createSearchFieldState.d.ts +25 -0
- package/src/searchfield/createSearchFieldState.d.ts.map +1 -0
- package/src/searchfield/createSearchFieldState.ts +62 -0
- package/src/searchfield/index.d.ts +3 -0
- package/src/searchfield/index.d.ts.map +1 -0
- package/src/searchfield/index.ts +5 -0
- package/src/select/createSelectState.d.ts +73 -0
- package/src/select/createSelectState.d.ts.map +1 -0
- package/src/select/createSelectState.ts +181 -0
- package/src/select/index.d.ts +2 -0
- package/src/select/index.d.ts.map +1 -0
- package/src/select/index.ts +5 -0
- package/src/slider/createSliderState.d.ts +72 -0
- package/src/slider/createSliderState.d.ts.map +1 -0
- package/src/slider/createSliderState.ts +211 -0
- package/src/slider/index.d.ts +3 -0
- package/src/slider/index.d.ts.map +1 -0
- package/src/slider/index.ts +6 -0
- package/src/ssr/index.d.ts +28 -0
- package/src/ssr/index.d.ts.map +1 -0
- package/src/ssr/index.ts +41 -0
- package/src/table/TableCollection.d.ts +52 -0
- package/src/table/TableCollection.d.ts.map +1 -0
- package/src/table/TableCollection.ts +388 -0
- package/src/table/createTableState.d.ts +12 -0
- package/src/table/createTableState.d.ts.map +1 -0
- package/src/table/createTableState.ts +127 -0
- package/src/table/index.d.ts +8 -0
- package/src/table/index.d.ts.map +1 -0
- package/src/table/index.ts +18 -0
- package/src/table/types.d.ts +139 -0
- package/src/table/types.d.ts.map +1 -0
- package/src/table/types.ts +150 -0
- package/src/tabs/createTabListState.d.ts +68 -0
- package/src/tabs/createTabListState.d.ts.map +1 -0
- package/src/tabs/createTabListState.ts +240 -0
- package/src/tabs/index.d.ts +2 -0
- package/src/tabs/index.d.ts.map +1 -0
- package/src/tabs/index.ts +7 -0
- package/src/textfield/createTextFieldState.d.ts +30 -0
- package/src/textfield/createTextFieldState.d.ts.map +1 -0
- package/src/textfield/createTextFieldState.ts +75 -0
- package/src/textfield/index.d.ts +2 -0
- package/src/textfield/index.d.ts.map +1 -0
- package/src/textfield/index.ts +5 -0
- package/src/toast/createToastState.d.ts +118 -0
- package/src/toast/createToastState.d.ts.map +1 -0
- package/src/toast/createToastState.ts +316 -0
- package/src/toast/index.d.ts +2 -0
- package/src/toast/index.d.ts.map +1 -0
- package/src/toast/index.ts +11 -0
- package/src/toggle/createToggleState.d.ts +34 -0
- package/src/toggle/createToggleState.d.ts.map +1 -0
- package/src/toggle/createToggleState.ts +94 -0
- package/src/toggle/index.d.ts +2 -0
- package/src/toggle/index.d.ts.map +1 -0
- package/src/toggle/index.ts +5 -0
- package/src/tooltip/createTooltipTriggerState.d.ts +39 -0
- package/src/tooltip/createTooltipTriggerState.d.ts.map +1 -0
- package/src/tooltip/createTooltipTriggerState.ts +183 -0
- package/src/tooltip/index.d.ts +2 -0
- package/src/tooltip/index.d.ts.map +1 -0
- package/src/tooltip/index.ts +6 -0
- package/src/tree/TreeCollection.d.ts +40 -0
- package/src/tree/TreeCollection.d.ts.map +1 -0
- package/src/tree/TreeCollection.ts +175 -0
- package/src/tree/createTreeState.d.ts +14 -0
- package/src/tree/createTreeState.d.ts.map +1 -0
- package/src/tree/createTreeState.ts +392 -0
- package/src/tree/index.d.ts +7 -0
- package/src/tree/index.d.ts.map +1 -0
- package/src/tree/index.ts +13 -0
- package/src/tree/types.d.ts +157 -0
- package/src/tree/types.d.ts.map +1 -0
- package/src/tree/types.ts +174 -0
- package/src/utils/index.d.ts +2 -0
- package/src/utils/index.d.ts.map +1 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/reactivity.d.ts +28 -0
- package/src/utils/reactivity.d.ts.map +1 -0
- package/src/utils/reactivity.ts +36 -0
- package/dist/index.jsx +0 -6408
- package/dist/index.jsx.map +0 -7
|
@@ -0,0 +1,703 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State management for ComboBox components.
|
|
3
|
+
* Based on @react-stately/combobox useComboBoxState.
|
|
4
|
+
*
|
|
5
|
+
* ComboBox combines a text input with a dropdown list, allowing users to
|
|
6
|
+
* either type to filter options or select from a list.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { createSignal, createMemo, createEffect, type Accessor } from 'solid-js';
|
|
10
|
+
import { access, type MaybeAccessor } from '../utils';
|
|
11
|
+
import { createListState } from '../collections/createListState';
|
|
12
|
+
import { createOverlayTriggerState } from '../overlays';
|
|
13
|
+
import type { Key, CollectionNode, Collection, FocusStrategy } from '../collections/types';
|
|
14
|
+
|
|
15
|
+
// ============================================
|
|
16
|
+
// TYPES
|
|
17
|
+
// ============================================
|
|
18
|
+
|
|
19
|
+
export type MenuTriggerAction = 'focus' | 'input' | 'manual';
|
|
20
|
+
|
|
21
|
+
// Re-export FocusStrategy for convenience
|
|
22
|
+
export type { FocusStrategy } from '../collections/types';
|
|
23
|
+
|
|
24
|
+
export type FilterFn = (textValue: string, inputValue: string) => boolean;
|
|
25
|
+
|
|
26
|
+
export interface ComboBoxStateProps<T = unknown> {
|
|
27
|
+
/** The items to display in the combobox dropdown. */
|
|
28
|
+
items: T[];
|
|
29
|
+
/** Default items when uncontrolled. */
|
|
30
|
+
defaultItems?: T[];
|
|
31
|
+
/** Function to get the key for an item. */
|
|
32
|
+
getKey?: (item: T) => Key;
|
|
33
|
+
/** Function to get the text value for an item. */
|
|
34
|
+
getTextValue?: (item: T) => string;
|
|
35
|
+
/** Function to check if an item is disabled. */
|
|
36
|
+
getDisabled?: (item: T) => boolean;
|
|
37
|
+
/** Keys of disabled items. */
|
|
38
|
+
disabledKeys?: Iterable<Key>;
|
|
39
|
+
/** The currently selected key (controlled). */
|
|
40
|
+
selectedKey?: Key | null;
|
|
41
|
+
/** The default selected key (uncontrolled). */
|
|
42
|
+
defaultSelectedKey?: Key | null;
|
|
43
|
+
/** Handler called when the selection changes. */
|
|
44
|
+
onSelectionChange?: (key: Key | null) => void;
|
|
45
|
+
/** The current input value (controlled). */
|
|
46
|
+
inputValue?: string;
|
|
47
|
+
/** The default input value (uncontrolled). */
|
|
48
|
+
defaultInputValue?: string;
|
|
49
|
+
/** Handler called when the input value changes. */
|
|
50
|
+
onInputChange?: (value: string) => void;
|
|
51
|
+
/** Whether the combobox is open (controlled). */
|
|
52
|
+
isOpen?: boolean;
|
|
53
|
+
/** Whether the combobox is open by default (uncontrolled). */
|
|
54
|
+
defaultOpen?: boolean;
|
|
55
|
+
/** Handler called when the open state changes. */
|
|
56
|
+
onOpenChange?: (isOpen: boolean, trigger?: MenuTriggerAction) => void;
|
|
57
|
+
/** Whether the combobox is disabled. */
|
|
58
|
+
isDisabled?: boolean;
|
|
59
|
+
/** Whether the combobox is read-only. */
|
|
60
|
+
isReadOnly?: boolean;
|
|
61
|
+
/** Whether the combobox is required. */
|
|
62
|
+
isRequired?: boolean;
|
|
63
|
+
/** The filter function to use when filtering items. */
|
|
64
|
+
defaultFilter?: FilterFn;
|
|
65
|
+
/** Whether to allow the menu to open when there are no items. */
|
|
66
|
+
allowsEmptyCollection?: boolean;
|
|
67
|
+
/** Whether to allow custom values that don't match any option. */
|
|
68
|
+
allowsCustomValue?: boolean;
|
|
69
|
+
/** What triggers the menu to open. */
|
|
70
|
+
menuTrigger?: MenuTriggerAction;
|
|
71
|
+
/** Whether to close the menu on blur. */
|
|
72
|
+
shouldCloseOnBlur?: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface ComboBoxState<T = unknown> {
|
|
76
|
+
/** The collection of items (may be filtered). */
|
|
77
|
+
readonly collection: Accessor<Collection<T>>;
|
|
78
|
+
/** Whether the combobox dropdown is open. */
|
|
79
|
+
readonly isOpen: Accessor<boolean>;
|
|
80
|
+
/** Open the combobox dropdown. */
|
|
81
|
+
open(focusStrategy?: FocusStrategy | null, trigger?: MenuTriggerAction): void;
|
|
82
|
+
/** Close the combobox dropdown. */
|
|
83
|
+
close(): void;
|
|
84
|
+
/** Toggle the combobox dropdown. */
|
|
85
|
+
toggle(focusStrategy?: FocusStrategy | null, trigger?: MenuTriggerAction): void;
|
|
86
|
+
/** The currently selected key. */
|
|
87
|
+
readonly selectedKey: Accessor<Key | null>;
|
|
88
|
+
/** The default selected key. */
|
|
89
|
+
readonly defaultSelectedKey: Key | null;
|
|
90
|
+
/** The currently selected item. */
|
|
91
|
+
readonly selectedItem: Accessor<CollectionNode<T> | null>;
|
|
92
|
+
/** Set the selected key. */
|
|
93
|
+
setSelectedKey(key: Key | null): void;
|
|
94
|
+
/** The current input value. */
|
|
95
|
+
readonly inputValue: Accessor<string>;
|
|
96
|
+
/** The default input value. */
|
|
97
|
+
readonly defaultInputValue: string;
|
|
98
|
+
/** Set the input value. */
|
|
99
|
+
setInputValue(value: string): void;
|
|
100
|
+
/** The currently focused key in the list. */
|
|
101
|
+
readonly focusedKey: Accessor<Key | null>;
|
|
102
|
+
/** Set the focused key. */
|
|
103
|
+
setFocusedKey(key: Key | null): void;
|
|
104
|
+
/** Whether the combobox input has focus. */
|
|
105
|
+
readonly isFocused: Accessor<boolean>;
|
|
106
|
+
/** Set whether the combobox has focus. */
|
|
107
|
+
setFocused(isFocused: boolean): void;
|
|
108
|
+
/** The focus strategy to use when opening. */
|
|
109
|
+
readonly focusStrategy: Accessor<FocusStrategy | null>;
|
|
110
|
+
/** Commit the current selection (select focused item or custom value). */
|
|
111
|
+
commit(): void;
|
|
112
|
+
/** Revert input to the selected item's text and close menu. */
|
|
113
|
+
revert(): void;
|
|
114
|
+
/** Whether a specific key is disabled. */
|
|
115
|
+
isKeyDisabled(key: Key): boolean;
|
|
116
|
+
/** Select a key and close the menu (for ListState compatibility). */
|
|
117
|
+
select(key: Key): void;
|
|
118
|
+
/** The selection mode (always 'single' for combobox). */
|
|
119
|
+
readonly selectionMode: Accessor<'single'>;
|
|
120
|
+
/** Check if a key is selected. */
|
|
121
|
+
isSelected(key: Key): boolean;
|
|
122
|
+
/** Whether the combobox is disabled. */
|
|
123
|
+
readonly isDisabled: boolean;
|
|
124
|
+
/** Whether the combobox is read-only. */
|
|
125
|
+
readonly isReadOnly: boolean;
|
|
126
|
+
/** Whether the combobox is required. */
|
|
127
|
+
readonly isRequired: boolean;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ============================================
|
|
131
|
+
// DEFAULT FILTER
|
|
132
|
+
// ============================================
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Default filter function that does case-insensitive "contains" matching.
|
|
136
|
+
*/
|
|
137
|
+
export const defaultContainsFilter: FilterFn = (textValue, inputValue) => {
|
|
138
|
+
return textValue.toLowerCase().includes(inputValue.toLowerCase());
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// ============================================
|
|
142
|
+
// IMPLEMENTATION
|
|
143
|
+
// ============================================
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Creates state for a combobox component.
|
|
147
|
+
* Combines list state with input value management and filtering.
|
|
148
|
+
*/
|
|
149
|
+
export function createComboBoxState<T = unknown>(
|
|
150
|
+
props: MaybeAccessor<ComboBoxStateProps<T>>
|
|
151
|
+
): ComboBoxState<T> {
|
|
152
|
+
const getProps = () => access(props);
|
|
153
|
+
|
|
154
|
+
// Extract options with defaults
|
|
155
|
+
const menuTrigger = () => getProps().menuTrigger ?? 'input';
|
|
156
|
+
const allowsEmptyCollection = () => getProps().allowsEmptyCollection ?? false;
|
|
157
|
+
const allowsCustomValue = () => getProps().allowsCustomValue ?? false;
|
|
158
|
+
const shouldCloseOnBlur = () => getProps().shouldCloseOnBlur ?? true;
|
|
159
|
+
|
|
160
|
+
// Track focus strategy for list navigation
|
|
161
|
+
const [focusStrategy, setFocusStrategy] = createSignal<FocusStrategy | null>(null);
|
|
162
|
+
|
|
163
|
+
// Track whether we're showing all items (vs filtered)
|
|
164
|
+
const [showAllItems, setShowAllItems] = createSignal(false);
|
|
165
|
+
|
|
166
|
+
// Track the menu open trigger
|
|
167
|
+
let menuOpenTrigger: MenuTriggerAction = 'focus';
|
|
168
|
+
|
|
169
|
+
// ---- Selection State ----
|
|
170
|
+
// Note: Selection state is initialized first because input value may depend on it
|
|
171
|
+
const isSelectionControlled = () => getProps().selectedKey !== undefined;
|
|
172
|
+
const [internalSelectedKey, setInternalSelectedKey] = createSignal<Key | null>(
|
|
173
|
+
getProps().defaultSelectedKey ?? null
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
// ---- Input Value State ----
|
|
177
|
+
// Initialized after selection so we can derive from selected item if needed
|
|
178
|
+
const isInputControlled = () => getProps().inputValue !== undefined;
|
|
179
|
+
|
|
180
|
+
// We'll set the proper initial value after collection is created
|
|
181
|
+
const [internalInputValue, setInternalInputValue] = createSignal(
|
|
182
|
+
getProps().defaultInputValue ?? ''
|
|
183
|
+
);
|
|
184
|
+
// Track if we've initialized input from selection
|
|
185
|
+
let inputInitialized = false;
|
|
186
|
+
|
|
187
|
+
const inputValue: Accessor<string> = () => {
|
|
188
|
+
return isInputControlled() ? (getProps().inputValue ?? '') : internalInputValue();
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const setInputValue = (value: string) => {
|
|
192
|
+
if (!isInputControlled()) {
|
|
193
|
+
setInternalInputValue(value);
|
|
194
|
+
}
|
|
195
|
+
getProps().onInputChange?.(value);
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// Track last committed input value
|
|
199
|
+
const [lastValue, setLastValue] = createSignal(inputValue());
|
|
200
|
+
|
|
201
|
+
const selectedKey: Accessor<Key | null> = () => {
|
|
202
|
+
return isSelectionControlled() ? (getProps().selectedKey ?? null) : internalSelectedKey();
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const setSelectedKey = (key: Key | null) => {
|
|
206
|
+
if (!isSelectionControlled()) {
|
|
207
|
+
setInternalSelectedKey(key);
|
|
208
|
+
}
|
|
209
|
+
getProps().onSelectionChange?.(key);
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
// ---- Overlay State ----
|
|
213
|
+
const overlayState = createOverlayTriggerState({
|
|
214
|
+
get isOpen() {
|
|
215
|
+
return getProps().isOpen;
|
|
216
|
+
},
|
|
217
|
+
get defaultOpen() {
|
|
218
|
+
return getProps().defaultOpen;
|
|
219
|
+
},
|
|
220
|
+
onOpenChange(isOpen: boolean) {
|
|
221
|
+
getProps().onOpenChange?.(isOpen, isOpen ? menuOpenTrigger : undefined);
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// ---- List State (unfiltered collection) ----
|
|
226
|
+
const listState = createListState<T>({
|
|
227
|
+
get items() {
|
|
228
|
+
// Use items or defaultItems
|
|
229
|
+
return getProps().items ?? getProps().defaultItems ?? [];
|
|
230
|
+
},
|
|
231
|
+
get getKey() {
|
|
232
|
+
return getProps().getKey;
|
|
233
|
+
},
|
|
234
|
+
get getTextValue() {
|
|
235
|
+
return getProps().getTextValue;
|
|
236
|
+
},
|
|
237
|
+
get getDisabled() {
|
|
238
|
+
return getProps().getDisabled;
|
|
239
|
+
},
|
|
240
|
+
get disabledKeys() {
|
|
241
|
+
return getProps().disabledKeys;
|
|
242
|
+
},
|
|
243
|
+
selectionMode: 'single',
|
|
244
|
+
disallowEmptySelection: false,
|
|
245
|
+
get selectedKeys() {
|
|
246
|
+
const key = selectedKey();
|
|
247
|
+
return key != null ? [key] : [];
|
|
248
|
+
},
|
|
249
|
+
onSelectionChange(keys) {
|
|
250
|
+
if (keys === 'all') return;
|
|
251
|
+
const key = keys.size > 0 ? Array.from(keys)[0] : null;
|
|
252
|
+
|
|
253
|
+
// If same key selected, just reset input and close
|
|
254
|
+
if (key === selectedKey()) {
|
|
255
|
+
resetInputValue();
|
|
256
|
+
closeMenu();
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
setSelectedKey(key);
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// ---- Filtered Collection ----
|
|
265
|
+
const originalCollection = listState.collection;
|
|
266
|
+
|
|
267
|
+
const filteredCollection = createMemo<Collection<T>>(() => {
|
|
268
|
+
const collection = originalCollection();
|
|
269
|
+
const input = inputValue();
|
|
270
|
+
const filter = getProps().defaultFilter;
|
|
271
|
+
|
|
272
|
+
// If no filter function provided, return original collection
|
|
273
|
+
if (!filter) {
|
|
274
|
+
return collection;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Filter the collection based on input value
|
|
278
|
+
return filterCollection(collection, input, filter);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// The displayed collection depends on showAllItems flag
|
|
282
|
+
// Always show filtered collection (or all items if showAllItems is true)
|
|
283
|
+
const displayedCollection = createMemo<Collection<T>>(() => {
|
|
284
|
+
return showAllItems() ? originalCollection() : filteredCollection();
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// ---- Selected Item ----
|
|
288
|
+
const selectedItem: Accessor<CollectionNode<T> | null> = () => {
|
|
289
|
+
const key = selectedKey();
|
|
290
|
+
if (key == null) return null;
|
|
291
|
+
return originalCollection().getItem(key);
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
// Initialize input value from selected item if not already set
|
|
295
|
+
// This runs once on creation
|
|
296
|
+
if (!inputInitialized && !isInputControlled()) {
|
|
297
|
+
const defaultKey = getProps().defaultSelectedKey;
|
|
298
|
+
if (defaultKey != null && !getProps().defaultInputValue) {
|
|
299
|
+
// Get the text value from the collection for the default selected key
|
|
300
|
+
const item = originalCollection().getItem(defaultKey);
|
|
301
|
+
if (item) {
|
|
302
|
+
setInternalInputValue(item.textValue);
|
|
303
|
+
setLastValue(item.textValue);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
inputInitialized = true;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ---- Helper Functions ----
|
|
310
|
+
const resetInputValue = () => {
|
|
311
|
+
const item = selectedItem();
|
|
312
|
+
const textValue = item?.textValue ?? '';
|
|
313
|
+
setLastValue(textValue);
|
|
314
|
+
setInputValue(textValue);
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const closeMenu = () => {
|
|
318
|
+
if (overlayState.isOpen()) {
|
|
319
|
+
overlayState.close();
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
// ---- Open/Toggle Logic ----
|
|
324
|
+
const open = (strategy: FocusStrategy | null = null, trigger?: MenuTriggerAction) => {
|
|
325
|
+
const displayAll = trigger === 'manual' || (trigger === 'focus' && menuTrigger() === 'focus');
|
|
326
|
+
|
|
327
|
+
// Check if we should open
|
|
328
|
+
const filtered = filteredCollection();
|
|
329
|
+
const original = originalCollection();
|
|
330
|
+
const canOpen = allowsEmptyCollection() ||
|
|
331
|
+
filtered.size > 0 ||
|
|
332
|
+
(displayAll && original.size > 0);
|
|
333
|
+
|
|
334
|
+
if (!canOpen) return;
|
|
335
|
+
|
|
336
|
+
if (displayAll && !overlayState.isOpen()) {
|
|
337
|
+
setShowAllItems(true);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
menuOpenTrigger = trigger ?? 'focus';
|
|
341
|
+
setFocusStrategy(strategy);
|
|
342
|
+
overlayState.open();
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
const toggle = (strategy: FocusStrategy | null = null, trigger?: MenuTriggerAction) => {
|
|
346
|
+
const displayAll = trigger === 'manual' || (trigger === 'focus' && menuTrigger() === 'focus');
|
|
347
|
+
|
|
348
|
+
// Check if we can open (if closed)
|
|
349
|
+
const filtered = filteredCollection();
|
|
350
|
+
const original = originalCollection();
|
|
351
|
+
const canOpen = allowsEmptyCollection() ||
|
|
352
|
+
filtered.size > 0 ||
|
|
353
|
+
(displayAll && original.size > 0);
|
|
354
|
+
|
|
355
|
+
if (!canOpen && !overlayState.isOpen()) return;
|
|
356
|
+
|
|
357
|
+
if (displayAll && !overlayState.isOpen()) {
|
|
358
|
+
setShowAllItems(true);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (!overlayState.isOpen()) {
|
|
362
|
+
menuOpenTrigger = trigger ?? 'focus';
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
setFocusStrategy(strategy);
|
|
366
|
+
overlayState.toggle();
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
// ---- Commit/Revert Logic ----
|
|
370
|
+
const commitCustomValue = () => {
|
|
371
|
+
setSelectedKey(null);
|
|
372
|
+
closeMenu();
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
const commitSelection = () => {
|
|
376
|
+
// If both are controlled, just call onSelectionChange
|
|
377
|
+
if (isSelectionControlled() && isInputControlled()) {
|
|
378
|
+
getProps().onSelectionChange?.(selectedKey());
|
|
379
|
+
const item = selectedItem();
|
|
380
|
+
setLastValue(item?.textValue ?? '');
|
|
381
|
+
closeMenu();
|
|
382
|
+
} else {
|
|
383
|
+
// Reset input to selected item's text
|
|
384
|
+
resetInputValue();
|
|
385
|
+
closeMenu();
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
const commitValue = () => {
|
|
390
|
+
if (allowsCustomValue()) {
|
|
391
|
+
const item = selectedItem();
|
|
392
|
+
const itemText = item?.textValue ?? '';
|
|
393
|
+
if (inputValue() === itemText) {
|
|
394
|
+
commitSelection();
|
|
395
|
+
} else {
|
|
396
|
+
commitCustomValue();
|
|
397
|
+
}
|
|
398
|
+
} else {
|
|
399
|
+
commitSelection();
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
const commit = () => {
|
|
404
|
+
const focusedKey = listState.focusedKey();
|
|
405
|
+
|
|
406
|
+
if (overlayState.isOpen() && focusedKey != null) {
|
|
407
|
+
// If focused key is already selected, just commit
|
|
408
|
+
if (selectedKey() === focusedKey) {
|
|
409
|
+
commitSelection();
|
|
410
|
+
} else {
|
|
411
|
+
// Select the focused item
|
|
412
|
+
setSelectedKey(focusedKey);
|
|
413
|
+
}
|
|
414
|
+
} else {
|
|
415
|
+
commitValue();
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
const revert = () => {
|
|
420
|
+
if (allowsCustomValue() && selectedKey() == null) {
|
|
421
|
+
commitCustomValue();
|
|
422
|
+
} else {
|
|
423
|
+
commitSelection();
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
// ---- Focus Handling ----
|
|
428
|
+
const [isFocused, setIsFocused] = createSignal(false);
|
|
429
|
+
let valueOnFocus = '';
|
|
430
|
+
|
|
431
|
+
const setFocused = (focused: boolean) => {
|
|
432
|
+
if (focused) {
|
|
433
|
+
valueOnFocus = inputValue();
|
|
434
|
+
if (menuTrigger() === 'focus' && !getProps().isReadOnly) {
|
|
435
|
+
open(null, 'focus');
|
|
436
|
+
}
|
|
437
|
+
} else {
|
|
438
|
+
if (shouldCloseOnBlur()) {
|
|
439
|
+
commitValue();
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
setIsFocused(focused);
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
// ---- Effects for Auto Open/Close ----
|
|
446
|
+
createEffect(() => {
|
|
447
|
+
const input = inputValue();
|
|
448
|
+
const filtered = filteredCollection();
|
|
449
|
+
const isOpen = overlayState.isOpen();
|
|
450
|
+
const last = lastValue();
|
|
451
|
+
const focused = isFocused();
|
|
452
|
+
|
|
453
|
+
// Auto-open when typing
|
|
454
|
+
if (
|
|
455
|
+
focused &&
|
|
456
|
+
(filtered.size > 0 || allowsEmptyCollection()) &&
|
|
457
|
+
!isOpen &&
|
|
458
|
+
input !== last &&
|
|
459
|
+
menuTrigger() !== 'manual'
|
|
460
|
+
) {
|
|
461
|
+
open(null, 'input');
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Auto-close when empty (unless showing all)
|
|
465
|
+
if (
|
|
466
|
+
!showAllItems() &&
|
|
467
|
+
!allowsEmptyCollection() &&
|
|
468
|
+
isOpen &&
|
|
469
|
+
filtered.size === 0
|
|
470
|
+
) {
|
|
471
|
+
closeMenu();
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Clear focused key when input changes
|
|
475
|
+
if (input !== last) {
|
|
476
|
+
listState.setFocusedKey(null);
|
|
477
|
+
setShowAllItems(false);
|
|
478
|
+
|
|
479
|
+
// Clear selection when input is cleared (if not fully controlled)
|
|
480
|
+
if (input === '' && (!isInputControlled() || !isSelectionControlled())) {
|
|
481
|
+
setSelectedKey(null);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
setLastValue(input);
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
// Update input when selection changes
|
|
489
|
+
createEffect(() => {
|
|
490
|
+
const key = selectedKey();
|
|
491
|
+
const item = key != null ? originalCollection().getItem(key) : null;
|
|
492
|
+
const textValue = item?.textValue ?? '';
|
|
493
|
+
|
|
494
|
+
// Only update if selection changed and not fully controlled
|
|
495
|
+
if (!isInputControlled() || !isSelectionControlled()) {
|
|
496
|
+
if (key != null && textValue !== inputValue()) {
|
|
497
|
+
setInputValue(textValue);
|
|
498
|
+
setLastValue(textValue);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
// Close when selection changes
|
|
504
|
+
createEffect((prevKey: Key | null | undefined) => {
|
|
505
|
+
const key = selectedKey();
|
|
506
|
+
if (key != null && key !== prevKey) {
|
|
507
|
+
closeMenu();
|
|
508
|
+
}
|
|
509
|
+
return key;
|
|
510
|
+
}, undefined);
|
|
511
|
+
|
|
512
|
+
// ---- Selection Methods for ListState compatibility ----
|
|
513
|
+
// These methods allow createOption to work with ComboBoxState
|
|
514
|
+
const select = (key: Key) => {
|
|
515
|
+
setSelectedKey(key);
|
|
516
|
+
closeMenu();
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
const selectionMode: Accessor<'single'> = () => 'single';
|
|
520
|
+
const isSelected = (key: Key) => selectedKey() === key;
|
|
521
|
+
|
|
522
|
+
// ---- Return State ----
|
|
523
|
+
return {
|
|
524
|
+
collection: displayedCollection,
|
|
525
|
+
isOpen: overlayState.isOpen,
|
|
526
|
+
open,
|
|
527
|
+
close: commitValue,
|
|
528
|
+
toggle,
|
|
529
|
+
selectedKey,
|
|
530
|
+
defaultSelectedKey: getProps().defaultSelectedKey ?? null,
|
|
531
|
+
selectedItem,
|
|
532
|
+
setSelectedKey,
|
|
533
|
+
inputValue,
|
|
534
|
+
defaultInputValue: getProps().defaultInputValue ?? '',
|
|
535
|
+
setInputValue,
|
|
536
|
+
focusedKey: listState.focusedKey,
|
|
537
|
+
setFocusedKey: listState.setFocusedKey,
|
|
538
|
+
isFocused,
|
|
539
|
+
setFocused,
|
|
540
|
+
focusStrategy,
|
|
541
|
+
commit,
|
|
542
|
+
revert,
|
|
543
|
+
// Selection state methods for ListState compatibility
|
|
544
|
+
select,
|
|
545
|
+
selectionMode,
|
|
546
|
+
isSelected,
|
|
547
|
+
isKeyDisabled: (key: Key) => listState.isDisabled(key),
|
|
548
|
+
get isDisabled() {
|
|
549
|
+
return getProps().isDisabled ?? false;
|
|
550
|
+
},
|
|
551
|
+
get isReadOnly() {
|
|
552
|
+
return getProps().isReadOnly ?? false;
|
|
553
|
+
},
|
|
554
|
+
get isRequired() {
|
|
555
|
+
return getProps().isRequired ?? false;
|
|
556
|
+
},
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// ============================================
|
|
561
|
+
// COLLECTION FILTERING
|
|
562
|
+
// ============================================
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Filter a collection based on input value.
|
|
566
|
+
*/
|
|
567
|
+
function filterCollection<T>(
|
|
568
|
+
collection: Collection<T>,
|
|
569
|
+
inputValue: string,
|
|
570
|
+
filter: FilterFn
|
|
571
|
+
): Collection<T> {
|
|
572
|
+
if (!inputValue) {
|
|
573
|
+
return collection;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const filteredItems: CollectionNode<T>[] = [];
|
|
577
|
+
|
|
578
|
+
for (const item of collection) {
|
|
579
|
+
if (item.type === 'section') {
|
|
580
|
+
// Filter section children
|
|
581
|
+
const filteredChildren: CollectionNode<T>[] = [];
|
|
582
|
+
if (item.childNodes) {
|
|
583
|
+
for (const child of item.childNodes) {
|
|
584
|
+
if (child.type === 'item' && filter(child.textValue, inputValue)) {
|
|
585
|
+
filteredChildren.push(child);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
// Only include section if it has matching children
|
|
590
|
+
if (filteredChildren.length > 0) {
|
|
591
|
+
filteredItems.push({
|
|
592
|
+
...item,
|
|
593
|
+
childNodes: filteredChildren,
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
} else if (item.type === 'item') {
|
|
597
|
+
if (filter(item.textValue, inputValue)) {
|
|
598
|
+
filteredItems.push(item);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Create a new collection from filtered items
|
|
604
|
+
return createFilteredCollection(filteredItems, collection);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Create a filtered collection wrapper.
|
|
609
|
+
*/
|
|
610
|
+
function createFilteredCollection<T>(
|
|
611
|
+
items: CollectionNode<T>[],
|
|
612
|
+
original: Collection<T>
|
|
613
|
+
): Collection<T> {
|
|
614
|
+
const itemMap = new Map<Key, CollectionNode<T>>();
|
|
615
|
+
|
|
616
|
+
for (const item of items) {
|
|
617
|
+
itemMap.set(item.key, item);
|
|
618
|
+
if (item.childNodes) {
|
|
619
|
+
for (const child of item.childNodes) {
|
|
620
|
+
itemMap.set(child.key, child);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
return {
|
|
626
|
+
get size() {
|
|
627
|
+
let count = 0;
|
|
628
|
+
for (const item of items) {
|
|
629
|
+
if (item.type === 'item') {
|
|
630
|
+
count++;
|
|
631
|
+
} else if (item.childNodes) {
|
|
632
|
+
count += Array.from(item.childNodes).filter(c => c.type === 'item').length;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
return count;
|
|
636
|
+
},
|
|
637
|
+
getItem(key: Key) {
|
|
638
|
+
return itemMap.get(key) ?? null;
|
|
639
|
+
},
|
|
640
|
+
getKeys() {
|
|
641
|
+
return itemMap.keys();
|
|
642
|
+
},
|
|
643
|
+
getFirstKey() {
|
|
644
|
+
for (const item of items) {
|
|
645
|
+
if (item.type === 'item') return item.key;
|
|
646
|
+
if (item.childNodes) {
|
|
647
|
+
for (const child of item.childNodes) {
|
|
648
|
+
if (child.type === 'item') return child.key;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return null;
|
|
653
|
+
},
|
|
654
|
+
getLastKey() {
|
|
655
|
+
for (let i = items.length - 1; i >= 0; i--) {
|
|
656
|
+
const item = items[i];
|
|
657
|
+
if (item.type === 'item') return item.key;
|
|
658
|
+
if (item.childNodes) {
|
|
659
|
+
const children = Array.from(item.childNodes);
|
|
660
|
+
for (let j = children.length - 1; j >= 0; j--) {
|
|
661
|
+
if (children[j].type === 'item') return children[j].key;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
return null;
|
|
666
|
+
},
|
|
667
|
+
getKeyBefore(key: Key) {
|
|
668
|
+
return original.getKeyBefore(key);
|
|
669
|
+
},
|
|
670
|
+
getKeyAfter(key: Key) {
|
|
671
|
+
return original.getKeyAfter(key);
|
|
672
|
+
},
|
|
673
|
+
at(index: number) {
|
|
674
|
+
// Flatten items for indexing
|
|
675
|
+
let currentIndex = 0;
|
|
676
|
+
for (const item of items) {
|
|
677
|
+
if (item.type === 'item') {
|
|
678
|
+
if (currentIndex === index) return item;
|
|
679
|
+
currentIndex++;
|
|
680
|
+
} else if (item.childNodes) {
|
|
681
|
+
for (const child of item.childNodes) {
|
|
682
|
+
if (child.type === 'item') {
|
|
683
|
+
if (currentIndex === index) return child;
|
|
684
|
+
currentIndex++;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
return null;
|
|
690
|
+
},
|
|
691
|
+
getChildren(key: Key) {
|
|
692
|
+
const item = itemMap.get(key);
|
|
693
|
+
return item?.childNodes ?? [];
|
|
694
|
+
},
|
|
695
|
+
getTextValue(key: Key) {
|
|
696
|
+
const item = itemMap.get(key);
|
|
697
|
+
return item?.textValue ?? '';
|
|
698
|
+
},
|
|
699
|
+
[Symbol.iterator]() {
|
|
700
|
+
return items[Symbol.iterator]();
|
|
701
|
+
},
|
|
702
|
+
};
|
|
703
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ComboBox state management for Solid Stately.
|
|
3
|
+
*/
|
|
4
|
+
export { createComboBoxState, defaultContainsFilter, type ComboBoxState, type ComboBoxStateProps, type FilterFn, type MenuTriggerAction, type FocusStrategy, } from './createComboBoxState';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACL,mBAAmB,EACnB,qBAAqB,EACrB,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACvB,KAAK,QAAQ,EACb,KAAK,iBAAiB,EACtB,KAAK,aAAa,GACnB,MAAM,uBAAuB,CAAC"}
|