@kenos-ui/react-select 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/LICENSE +21 -0
- package/README.md +63 -0
- package/dist/index.cjs +1162 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +320 -0
- package/dist/index.d.ts +320 -0
- package/dist/index.js +1158 -0
- package/dist/index.js.map +1 -0
- package/package.json +65 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1158 @@
|
|
|
1
|
+
import { createContext, useContext, useId, useMemo, useRef, useEffect, useCallback, useLayoutEffect, useState, useSyncExternalStore, Children, isValidElement } from 'react';
|
|
2
|
+
import { restoreFocus, useFloating, usePresence, useClickOutside, useEscapeKey, useFocusTrap, useListNavigation, useTypeahead } from '@kenos-ui/utils';
|
|
3
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
4
|
+
import { createPortal } from 'react-dom';
|
|
5
|
+
|
|
6
|
+
var __defProp = Object.defineProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/index.parts.ts
|
|
13
|
+
var index_parts_exports = {};
|
|
14
|
+
__export(index_parts_exports, {
|
|
15
|
+
Backdrop: () => Backdrop,
|
|
16
|
+
ClearTrigger: () => ClearTrigger,
|
|
17
|
+
Content: () => Content,
|
|
18
|
+
Group: () => Group,
|
|
19
|
+
GroupLabel: () => GroupLabel,
|
|
20
|
+
HiddenSelect: () => HiddenSelect,
|
|
21
|
+
Icon: () => Icon,
|
|
22
|
+
Item: () => Item,
|
|
23
|
+
ItemIndicator: () => ItemIndicator,
|
|
24
|
+
ItemText: () => ItemText,
|
|
25
|
+
Label: () => Label,
|
|
26
|
+
List: () => List,
|
|
27
|
+
Portal: () => Portal,
|
|
28
|
+
Positioner: () => Positioner,
|
|
29
|
+
Root: () => Root,
|
|
30
|
+
ScrollDownButton: () => ScrollDownButton,
|
|
31
|
+
ScrollUpButton: () => ScrollUpButton,
|
|
32
|
+
Trigger: () => Trigger,
|
|
33
|
+
Value: () => Value
|
|
34
|
+
});
|
|
35
|
+
function arraysEqual(a, b) {
|
|
36
|
+
if (a.length !== b.length) return false;
|
|
37
|
+
for (let i = 0; i < a.length; i++) {
|
|
38
|
+
if (a[i] !== b[i]) return false;
|
|
39
|
+
}
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
var SelectStore = class {
|
|
43
|
+
state;
|
|
44
|
+
listeners = /* @__PURE__ */ new Set();
|
|
45
|
+
constructor(initial = {}) {
|
|
46
|
+
this.state = {
|
|
47
|
+
open: false,
|
|
48
|
+
openSource: null,
|
|
49
|
+
value: null,
|
|
50
|
+
highlightedValue: null,
|
|
51
|
+
items: /* @__PURE__ */ new Map(),
|
|
52
|
+
...initial
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
getState() {
|
|
56
|
+
return this.state;
|
|
57
|
+
}
|
|
58
|
+
subscribe(listener) {
|
|
59
|
+
this.listeners.add(listener);
|
|
60
|
+
return () => this.listeners.delete(listener);
|
|
61
|
+
}
|
|
62
|
+
notify() {
|
|
63
|
+
for (const listener of this.listeners) {
|
|
64
|
+
listener();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// ── Mutations ────────────────────────────────────────────────────────────
|
|
68
|
+
setOpen(open, source = null) {
|
|
69
|
+
if (this.state.open === open) return;
|
|
70
|
+
this.state = {
|
|
71
|
+
...this.state,
|
|
72
|
+
open,
|
|
73
|
+
openSource: open ? source : null,
|
|
74
|
+
// Reset highlight when closing
|
|
75
|
+
highlightedValue: open ? this.state.highlightedValue : null
|
|
76
|
+
};
|
|
77
|
+
this.notify();
|
|
78
|
+
}
|
|
79
|
+
setValue(value) {
|
|
80
|
+
if (this.state.value === value) return;
|
|
81
|
+
this.state = { ...this.state, value };
|
|
82
|
+
this.notify();
|
|
83
|
+
}
|
|
84
|
+
setValues(values) {
|
|
85
|
+
const current = this.state.value;
|
|
86
|
+
if (Array.isArray(current) && arraysEqual(current, values)) return;
|
|
87
|
+
this.state = { ...this.state, value: values };
|
|
88
|
+
this.notify();
|
|
89
|
+
}
|
|
90
|
+
toggleValue(value, comparator) {
|
|
91
|
+
const current = this.state.value;
|
|
92
|
+
const next = Array.isArray(current) ? [...current] : [];
|
|
93
|
+
const index = next.findIndex((item) => comparator(item, value));
|
|
94
|
+
if (index >= 0) {
|
|
95
|
+
next.splice(index, 1);
|
|
96
|
+
} else {
|
|
97
|
+
next.push(value);
|
|
98
|
+
}
|
|
99
|
+
if (Array.isArray(current) && arraysEqual(current, next)) return;
|
|
100
|
+
this.state = { ...this.state, value: next };
|
|
101
|
+
this.notify();
|
|
102
|
+
}
|
|
103
|
+
clearValue(multiple) {
|
|
104
|
+
const next = multiple ? [] : null;
|
|
105
|
+
const current = this.state.value;
|
|
106
|
+
if (multiple) {
|
|
107
|
+
if (Array.isArray(current) && current.length === 0) return;
|
|
108
|
+
} else if (current === null) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
this.state = { ...this.state, value: next };
|
|
112
|
+
this.notify();
|
|
113
|
+
}
|
|
114
|
+
isSelected(value, multiple, comparator) {
|
|
115
|
+
const current = this.state.value;
|
|
116
|
+
if (multiple) {
|
|
117
|
+
return Array.isArray(current) && current.some((item) => comparator(item, value));
|
|
118
|
+
}
|
|
119
|
+
return typeof current === "string" && comparator(current, value);
|
|
120
|
+
}
|
|
121
|
+
setHighlightedValue(value) {
|
|
122
|
+
if (this.state.highlightedValue === value) return;
|
|
123
|
+
this.state = { ...this.state, highlightedValue: value };
|
|
124
|
+
this.notify();
|
|
125
|
+
}
|
|
126
|
+
registerItem(record) {
|
|
127
|
+
const next = new Map(this.state.items);
|
|
128
|
+
next.set(record.value, record);
|
|
129
|
+
this.state = { ...this.state, items: next };
|
|
130
|
+
this.notify();
|
|
131
|
+
}
|
|
132
|
+
unregisterItem(value) {
|
|
133
|
+
if (!this.state.items.has(value)) return;
|
|
134
|
+
const next = new Map(this.state.items);
|
|
135
|
+
next.delete(value);
|
|
136
|
+
this.state = { ...this.state, items: next };
|
|
137
|
+
this.notify();
|
|
138
|
+
}
|
|
139
|
+
updateItemRef(value, ref) {
|
|
140
|
+
const item = this.state.items.get(value);
|
|
141
|
+
if (!item || item.ref === ref) return;
|
|
142
|
+
const next = new Map(this.state.items);
|
|
143
|
+
next.set(value, { ...item, ref });
|
|
144
|
+
this.state = { ...this.state, items: next };
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
function useSelectStore(store, selector) {
|
|
148
|
+
return useSyncExternalStore(
|
|
149
|
+
useCallback((cb) => store.subscribe(cb), [store]),
|
|
150
|
+
() => selector(store.getState()),
|
|
151
|
+
() => selector(store.getState())
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
var SelectContext = createContext(null);
|
|
155
|
+
function useSelectContext() {
|
|
156
|
+
const ctx = useContext(SelectContext);
|
|
157
|
+
if (!ctx) {
|
|
158
|
+
throw new Error("Select compound components must be rendered inside <Select.Root>.");
|
|
159
|
+
}
|
|
160
|
+
return ctx;
|
|
161
|
+
}
|
|
162
|
+
function extractItemTextLabel(children) {
|
|
163
|
+
let label = null;
|
|
164
|
+
Children.forEach(children, (child) => {
|
|
165
|
+
if (label != null || !isValidElement(child)) return;
|
|
166
|
+
const type = child.type;
|
|
167
|
+
const isItemText = type?.displayName === "Select.ItemText" || type?.name === "ItemText";
|
|
168
|
+
if (isItemText) {
|
|
169
|
+
const content = child.props.children;
|
|
170
|
+
if (typeof content === "string" || typeof content === "number") {
|
|
171
|
+
label = String(content);
|
|
172
|
+
}
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (child.props?.children != null) {
|
|
176
|
+
label = extractItemTextLabel(child.props.children);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
return label;
|
|
180
|
+
}
|
|
181
|
+
function isSelectItemElement(child) {
|
|
182
|
+
const type = child.type;
|
|
183
|
+
return type?.displayName === "Select.Item" || type?.name === "Item";
|
|
184
|
+
}
|
|
185
|
+
function extractItemsFromChildren(children) {
|
|
186
|
+
const items = {};
|
|
187
|
+
const walk = (node) => {
|
|
188
|
+
Children.forEach(node, (child) => {
|
|
189
|
+
if (!isValidElement(child)) return;
|
|
190
|
+
if (isSelectItemElement(child)) {
|
|
191
|
+
const value = child.props.value;
|
|
192
|
+
const label = extractItemTextLabel(child.props.children) ?? value;
|
|
193
|
+
if (value != null && label != null) {
|
|
194
|
+
items[value] = label;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (child.props?.children != null) {
|
|
198
|
+
walk(child.props.children);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
};
|
|
202
|
+
walk(children);
|
|
203
|
+
return items;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// src/utils/scroll-to-index.ts
|
|
207
|
+
function toScrollBlock(align) {
|
|
208
|
+
switch (align) {
|
|
209
|
+
case "start":
|
|
210
|
+
return "start";
|
|
211
|
+
case "end":
|
|
212
|
+
return "end";
|
|
213
|
+
case "center":
|
|
214
|
+
return "center";
|
|
215
|
+
case "nearest":
|
|
216
|
+
case "auto":
|
|
217
|
+
default:
|
|
218
|
+
return "nearest";
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function scrollToIndexInState(state, index, options = {}) {
|
|
222
|
+
const items = Array.from(state.items.values());
|
|
223
|
+
const item = items[index];
|
|
224
|
+
if (!item?.ref) return;
|
|
225
|
+
item.ref.scrollIntoView({
|
|
226
|
+
block: toScrollBlock(options.align ?? "nearest")
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
var defaultIsItemEqualToValue = (a, b) => a === b;
|
|
230
|
+
function valuesEqual(a, b) {
|
|
231
|
+
if (a === b) return true;
|
|
232
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
233
|
+
if (a.length !== b.length) return false;
|
|
234
|
+
for (let i = 0; i < a.length; i++) {
|
|
235
|
+
if (a[i] !== b[i]) return false;
|
|
236
|
+
}
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
function Root(props) {
|
|
242
|
+
const {
|
|
243
|
+
children,
|
|
244
|
+
value,
|
|
245
|
+
defaultValue,
|
|
246
|
+
onValueChange,
|
|
247
|
+
open,
|
|
248
|
+
defaultOpen,
|
|
249
|
+
onOpenChange,
|
|
250
|
+
onOpenChangeComplete,
|
|
251
|
+
name,
|
|
252
|
+
disabled = false,
|
|
253
|
+
required = false,
|
|
254
|
+
readOnly = false,
|
|
255
|
+
modal = false,
|
|
256
|
+
id,
|
|
257
|
+
items = {},
|
|
258
|
+
isItemEqualToValue = defaultIsItemEqualToValue,
|
|
259
|
+
multiple = false,
|
|
260
|
+
openOnFocus = false
|
|
261
|
+
} = props;
|
|
262
|
+
const uid = useId();
|
|
263
|
+
const prefix = id ?? `sel-${uid.replace(/:/g, "")}`;
|
|
264
|
+
const ids = useMemo(
|
|
265
|
+
() => ({
|
|
266
|
+
root: prefix,
|
|
267
|
+
label: `${prefix}-label`,
|
|
268
|
+
trigger: `${prefix}-trigger`,
|
|
269
|
+
content: `${prefix}-content`
|
|
270
|
+
}),
|
|
271
|
+
[prefix]
|
|
272
|
+
);
|
|
273
|
+
const triggerRef = useRef(null);
|
|
274
|
+
const contentRef = useRef(null);
|
|
275
|
+
const listRef = useRef(null);
|
|
276
|
+
const isControlledValue = value !== void 0;
|
|
277
|
+
const isControlledOpen = open !== void 0;
|
|
278
|
+
const initialValue = isControlledValue ? multiple ? value ?? [] : value ?? null : multiple ? defaultValue ?? [] : defaultValue ?? null;
|
|
279
|
+
const storeRef = useRef(null);
|
|
280
|
+
if (!storeRef.current) {
|
|
281
|
+
storeRef.current = new SelectStore({
|
|
282
|
+
value: initialValue,
|
|
283
|
+
open: isControlledOpen ? open ?? false : defaultOpen ?? false
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
const store = storeRef.current;
|
|
287
|
+
const prevControlledValue = useRef(value);
|
|
288
|
+
useEffect(() => {
|
|
289
|
+
if (!isControlledValue) return;
|
|
290
|
+
if (valuesEqual(value, prevControlledValue.current)) return;
|
|
291
|
+
prevControlledValue.current = value;
|
|
292
|
+
if (multiple) {
|
|
293
|
+
store.setValues(Array.isArray(value) ? value : []);
|
|
294
|
+
} else {
|
|
295
|
+
store.setValue(typeof value === "string" ? value : null);
|
|
296
|
+
}
|
|
297
|
+
}, [isControlledValue, value, store, multiple]);
|
|
298
|
+
const prevControlledOpen = useRef(open);
|
|
299
|
+
useEffect(() => {
|
|
300
|
+
if (!isControlledOpen) return;
|
|
301
|
+
if (open === prevControlledOpen.current) return;
|
|
302
|
+
prevControlledOpen.current = open;
|
|
303
|
+
store.setOpen(open ?? false);
|
|
304
|
+
}, [isControlledOpen, open, store]);
|
|
305
|
+
const onValueChangeRef = useRef(onValueChange);
|
|
306
|
+
onValueChangeRef.current = onValueChange;
|
|
307
|
+
const prevStoreValue = useRef(store.getState().value);
|
|
308
|
+
useEffect(() => {
|
|
309
|
+
return store.subscribe(() => {
|
|
310
|
+
const state = store.getState();
|
|
311
|
+
if (!valuesEqual(state.value, prevStoreValue.current)) {
|
|
312
|
+
prevStoreValue.current = state.value;
|
|
313
|
+
if (multiple) {
|
|
314
|
+
onValueChangeRef.current?.(
|
|
315
|
+
Array.isArray(state.value) ? state.value : []
|
|
316
|
+
);
|
|
317
|
+
} else {
|
|
318
|
+
onValueChangeRef.current?.(
|
|
319
|
+
typeof state.value === "string" ? state.value : null
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
}, [store, multiple]);
|
|
325
|
+
const onOpenChangeRef = useRef(onOpenChange);
|
|
326
|
+
onOpenChangeRef.current = onOpenChange;
|
|
327
|
+
const prevStoreOpen = useRef(store.getState().open);
|
|
328
|
+
useEffect(() => {
|
|
329
|
+
return store.subscribe(() => {
|
|
330
|
+
const state = store.getState();
|
|
331
|
+
if (state.open !== prevStoreOpen.current) {
|
|
332
|
+
prevStoreOpen.current = state.open;
|
|
333
|
+
onOpenChangeRef.current?.(state.open);
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
}, [store]);
|
|
337
|
+
const close = useCallback(() => {
|
|
338
|
+
const state = store.getState();
|
|
339
|
+
if (!state.open) return;
|
|
340
|
+
store.setOpen(false);
|
|
341
|
+
restoreFocus({
|
|
342
|
+
openSource: state.openSource ?? "trigger",
|
|
343
|
+
trigger: triggerRef.current
|
|
344
|
+
});
|
|
345
|
+
}, [store]);
|
|
346
|
+
const selectValue = useCallback(
|
|
347
|
+
(itemValue) => {
|
|
348
|
+
if (multiple) {
|
|
349
|
+
store.toggleValue(itemValue, isItemEqualToValue);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
store.setValue(itemValue);
|
|
353
|
+
close();
|
|
354
|
+
},
|
|
355
|
+
[store, close, multiple, isItemEqualToValue]
|
|
356
|
+
);
|
|
357
|
+
const clearValue = useCallback(() => {
|
|
358
|
+
store.clearValue(multiple);
|
|
359
|
+
}, [store, multiple]);
|
|
360
|
+
const scrollToIndex = useCallback(
|
|
361
|
+
(index, options) => {
|
|
362
|
+
scrollToIndexInState(store.getState(), index, options);
|
|
363
|
+
},
|
|
364
|
+
[store]
|
|
365
|
+
);
|
|
366
|
+
const discoveredItems = useMemo(() => extractItemsFromChildren(children), [children]);
|
|
367
|
+
const mergedItems = useMemo(
|
|
368
|
+
() => ({ ...discoveredItems, ...items }),
|
|
369
|
+
[discoveredItems, items]
|
|
370
|
+
);
|
|
371
|
+
const config = useMemo(
|
|
372
|
+
() => ({
|
|
373
|
+
disabled,
|
|
374
|
+
required,
|
|
375
|
+
readOnly,
|
|
376
|
+
modal,
|
|
377
|
+
name,
|
|
378
|
+
multiple,
|
|
379
|
+
items: mergedItems,
|
|
380
|
+
isItemEqualToValue,
|
|
381
|
+
openOnFocus
|
|
382
|
+
}),
|
|
383
|
+
[disabled, required, readOnly, modal, name, multiple, mergedItems, isItemEqualToValue, openOnFocus]
|
|
384
|
+
);
|
|
385
|
+
const ctx = useMemo(
|
|
386
|
+
() => ({
|
|
387
|
+
store,
|
|
388
|
+
ids,
|
|
389
|
+
refs: { triggerRef, contentRef, listRef },
|
|
390
|
+
config,
|
|
391
|
+
isControlledValue,
|
|
392
|
+
isControlledOpen,
|
|
393
|
+
onOpenChangeComplete,
|
|
394
|
+
close,
|
|
395
|
+
selectValue,
|
|
396
|
+
selectAndClose: selectValue,
|
|
397
|
+
clearValue,
|
|
398
|
+
scrollToIndex
|
|
399
|
+
}),
|
|
400
|
+
[
|
|
401
|
+
store,
|
|
402
|
+
ids,
|
|
403
|
+
config,
|
|
404
|
+
isControlledValue,
|
|
405
|
+
isControlledOpen,
|
|
406
|
+
onOpenChangeComplete,
|
|
407
|
+
close,
|
|
408
|
+
selectValue,
|
|
409
|
+
clearValue,
|
|
410
|
+
scrollToIndex
|
|
411
|
+
]
|
|
412
|
+
);
|
|
413
|
+
return /* @__PURE__ */ jsx(SelectContext.Provider, { value: ctx, children });
|
|
414
|
+
}
|
|
415
|
+
function Label({ children, ...props }) {
|
|
416
|
+
const { ids } = useSelectContext();
|
|
417
|
+
return /* @__PURE__ */ jsx("label", { id: ids.label, htmlFor: ids.trigger, ...props, children });
|
|
418
|
+
}
|
|
419
|
+
function Trigger({
|
|
420
|
+
children,
|
|
421
|
+
onClick,
|
|
422
|
+
onFocus,
|
|
423
|
+
disabled,
|
|
424
|
+
openOnFocus: openOnFocusProp,
|
|
425
|
+
...props
|
|
426
|
+
}) {
|
|
427
|
+
const { store, ids, refs, config } = useSelectContext();
|
|
428
|
+
const open = useSelectStore(store, (s) => s.open);
|
|
429
|
+
const highlightedValue = useSelectStore(store, (s) => s.highlightedValue);
|
|
430
|
+
const isDisabled = disabled ?? config.disabled;
|
|
431
|
+
const isReadOnly = config.readOnly;
|
|
432
|
+
const openOnFocus = openOnFocusProp ?? config.openOnFocus;
|
|
433
|
+
const activeDescendantId = open && highlightedValue != null ? `${ids.content}-opt-${highlightedValue}` : void 0;
|
|
434
|
+
return /* @__PURE__ */ jsx(
|
|
435
|
+
"button",
|
|
436
|
+
{
|
|
437
|
+
ref: refs.triggerRef,
|
|
438
|
+
type: "button",
|
|
439
|
+
id: ids.trigger,
|
|
440
|
+
role: "combobox",
|
|
441
|
+
"aria-haspopup": "listbox",
|
|
442
|
+
"aria-expanded": open,
|
|
443
|
+
"aria-controls": open ? ids.content : void 0,
|
|
444
|
+
"aria-labelledby": ids.label ? `${ids.label} ${ids.trigger}` : void 0,
|
|
445
|
+
"aria-activedescendant": activeDescendantId,
|
|
446
|
+
"aria-disabled": isDisabled || isReadOnly || void 0,
|
|
447
|
+
"data-disabled": isDisabled || isReadOnly ? "true" : void 0,
|
|
448
|
+
"data-open": open ? "true" : void 0,
|
|
449
|
+
"data-state": open ? "open" : "closed",
|
|
450
|
+
disabled: isDisabled || isReadOnly,
|
|
451
|
+
onClick: (e) => {
|
|
452
|
+
if (isDisabled || isReadOnly) return;
|
|
453
|
+
store.setOpen(!open, "trigger");
|
|
454
|
+
onClick?.(e);
|
|
455
|
+
},
|
|
456
|
+
onFocus: (e) => {
|
|
457
|
+
if (openOnFocus && !isDisabled && !isReadOnly && !open) {
|
|
458
|
+
store.setOpen(true, "trigger");
|
|
459
|
+
}
|
|
460
|
+
onFocus?.(e);
|
|
461
|
+
},
|
|
462
|
+
...props,
|
|
463
|
+
children
|
|
464
|
+
}
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// src/utils/labels.ts
|
|
469
|
+
function resolveItemLabel(value, registry, staticItems) {
|
|
470
|
+
return registry.get(value)?.label ?? staticItems[value] ?? value;
|
|
471
|
+
}
|
|
472
|
+
function mergeOptionValues(registry, staticItems) {
|
|
473
|
+
const seen = /* @__PURE__ */ new Set();
|
|
474
|
+
const values = [];
|
|
475
|
+
for (const value of registry.keys()) {
|
|
476
|
+
if (!seen.has(value)) {
|
|
477
|
+
seen.add(value);
|
|
478
|
+
values.push(value);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
for (const value of Object.keys(staticItems)) {
|
|
482
|
+
if (!seen.has(value)) {
|
|
483
|
+
seen.add(value);
|
|
484
|
+
values.push(value);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return values;
|
|
488
|
+
}
|
|
489
|
+
function Value({ placeholder = "", ...props }) {
|
|
490
|
+
const { store, config } = useSelectContext();
|
|
491
|
+
const value = useSelectStore(store, (s) => s.value);
|
|
492
|
+
const registry = useSelectStore(store, (s) => s.items);
|
|
493
|
+
const label = useMemo(() => {
|
|
494
|
+
if (config.multiple) {
|
|
495
|
+
const values = Array.isArray(value) ? value : [];
|
|
496
|
+
if (values.length === 0) return null;
|
|
497
|
+
return values.map((itemValue) => resolveItemLabel(itemValue, registry, config.items)).join(", ");
|
|
498
|
+
}
|
|
499
|
+
if (typeof value !== "string") return null;
|
|
500
|
+
return resolveItemLabel(value, registry, config.items);
|
|
501
|
+
}, [config.multiple, config.items, registry, value]);
|
|
502
|
+
return /* @__PURE__ */ jsx("span", { "data-placeholder": label == null ? "true" : void 0, ...props, children: label ?? placeholder });
|
|
503
|
+
}
|
|
504
|
+
function Icon({ children, ...props }) {
|
|
505
|
+
return /* @__PURE__ */ jsx("span", { "aria-hidden": "true", ...props, children: children ?? "\u25BE" });
|
|
506
|
+
}
|
|
507
|
+
var PositionerContext = createContext(null);
|
|
508
|
+
function usePositionerContext() {
|
|
509
|
+
return useContext(PositionerContext);
|
|
510
|
+
}
|
|
511
|
+
var PortalContext = createContext(false);
|
|
512
|
+
function usePortalContext() {
|
|
513
|
+
return useContext(PortalContext);
|
|
514
|
+
}
|
|
515
|
+
function resolvePortalContainer(container) {
|
|
516
|
+
if (container == null) {
|
|
517
|
+
return typeof document !== "undefined" ? document.body : null;
|
|
518
|
+
}
|
|
519
|
+
if (typeof HTMLElement !== "undefined" && container instanceof HTMLElement) {
|
|
520
|
+
return container;
|
|
521
|
+
}
|
|
522
|
+
if (typeof container === "object" && "current" in container) {
|
|
523
|
+
return container.current;
|
|
524
|
+
}
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
function Portal({ children, container = null }) {
|
|
528
|
+
const mountNode = resolvePortalContainer(container);
|
|
529
|
+
if (!mountNode) {
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
return /* @__PURE__ */ jsx(PortalContext.Provider, { value: true, children: createPortal(children, mountNode) });
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// src/utils/align-with-trigger.ts
|
|
536
|
+
function getAlignItemWithTriggerOffset(alignItemWithTrigger, side, triggerHeight, sideOffset) {
|
|
537
|
+
if (!alignItemWithTrigger || triggerHeight <= 0) {
|
|
538
|
+
return sideOffset;
|
|
539
|
+
}
|
|
540
|
+
if (side === "bottom" || side === "top") {
|
|
541
|
+
return -triggerHeight + sideOffset;
|
|
542
|
+
}
|
|
543
|
+
return sideOffset;
|
|
544
|
+
}
|
|
545
|
+
function shouldDisableFlipForAlign(alignItemWithTrigger) {
|
|
546
|
+
return alignItemWithTrigger;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// src/utils/use-align-item-with-trigger.ts
|
|
550
|
+
function useAlignItemWithTrigger({
|
|
551
|
+
alignItemWithTrigger,
|
|
552
|
+
side,
|
|
553
|
+
sideOffset,
|
|
554
|
+
open,
|
|
555
|
+
refs
|
|
556
|
+
}) {
|
|
557
|
+
const [triggerHeight, setTriggerHeight] = useState(0);
|
|
558
|
+
useLayoutEffect(() => {
|
|
559
|
+
if (!alignItemWithTrigger || !open) {
|
|
560
|
+
setTriggerHeight(0);
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
const trigger = refs.triggerRef.current;
|
|
564
|
+
if (!trigger) return;
|
|
565
|
+
const update = () => setTriggerHeight(trigger.offsetHeight);
|
|
566
|
+
update();
|
|
567
|
+
const observer = new ResizeObserver(update);
|
|
568
|
+
observer.observe(trigger);
|
|
569
|
+
return () => observer.disconnect();
|
|
570
|
+
}, [alignItemWithTrigger, open, refs.triggerRef]);
|
|
571
|
+
const effectiveSideOffset = getAlignItemWithTriggerOffset(
|
|
572
|
+
alignItemWithTrigger,
|
|
573
|
+
side,
|
|
574
|
+
triggerHeight,
|
|
575
|
+
sideOffset
|
|
576
|
+
);
|
|
577
|
+
const avoidCollisionsOverride = shouldDisableFlipForAlign(alignItemWithTrigger) ? false : void 0;
|
|
578
|
+
return {
|
|
579
|
+
alignItemWithTriggerActive: alignItemWithTrigger && triggerHeight > 0,
|
|
580
|
+
effectiveSideOffset,
|
|
581
|
+
avoidCollisionsOverride
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
function Content({
|
|
585
|
+
children,
|
|
586
|
+
forceMount,
|
|
587
|
+
side = "bottom",
|
|
588
|
+
align = "start",
|
|
589
|
+
sideOffset = 4,
|
|
590
|
+
alignOffset = 0,
|
|
591
|
+
avoidCollisions = true,
|
|
592
|
+
collisionPadding = 8,
|
|
593
|
+
portal = false,
|
|
594
|
+
container = null,
|
|
595
|
+
sameWidth = false,
|
|
596
|
+
alignItemWithTrigger = false,
|
|
597
|
+
lazyMount = true,
|
|
598
|
+
unmountOnExit = false,
|
|
599
|
+
onOpenChangeComplete: onOpenChangeCompleteProp,
|
|
600
|
+
style,
|
|
601
|
+
onKeyDown,
|
|
602
|
+
...props
|
|
603
|
+
}) {
|
|
604
|
+
const {
|
|
605
|
+
store,
|
|
606
|
+
ids,
|
|
607
|
+
refs,
|
|
608
|
+
config,
|
|
609
|
+
close,
|
|
610
|
+
selectValue,
|
|
611
|
+
onOpenChangeComplete: onOpenChangeCompleteRoot
|
|
612
|
+
} = useSelectContext();
|
|
613
|
+
const open = useSelectStore(store, (s) => s.open);
|
|
614
|
+
const highlightedValue = useSelectStore(store, (s) => s.highlightedValue);
|
|
615
|
+
const items = useSelectStore(store, (s) => s.items);
|
|
616
|
+
const positionerContext = usePositionerContext();
|
|
617
|
+
const isInsidePortal = usePortalContext();
|
|
618
|
+
const onOpenChangeComplete = onOpenChangeCompleteProp ?? onOpenChangeCompleteRoot;
|
|
619
|
+
const ownAlign = useAlignItemWithTrigger({
|
|
620
|
+
alignItemWithTrigger: positionerContext ? false : alignItemWithTrigger,
|
|
621
|
+
side,
|
|
622
|
+
sideOffset,
|
|
623
|
+
open,
|
|
624
|
+
refs
|
|
625
|
+
});
|
|
626
|
+
const ownFloating = useFloating({
|
|
627
|
+
open: positionerContext ? false : open,
|
|
628
|
+
side,
|
|
629
|
+
align,
|
|
630
|
+
sideOffset: ownAlign.effectiveSideOffset,
|
|
631
|
+
alignOffset,
|
|
632
|
+
avoidCollisions: ownAlign.avoidCollisionsOverride ?? avoidCollisions,
|
|
633
|
+
collisionPadding,
|
|
634
|
+
sameWidth
|
|
635
|
+
});
|
|
636
|
+
const { setReference } = ownFloating;
|
|
637
|
+
const setFloating = positionerContext?.setFloating ?? ownFloating.setFloating;
|
|
638
|
+
const floatingStyles = positionerContext?.floatingStyles ?? ownFloating.floatingStyles;
|
|
639
|
+
const isPositioned = positionerContext?.isPositioned ?? ownFloating.isPositioned;
|
|
640
|
+
const alignItemWithTriggerActive = positionerContext?.alignItemWithTriggerActive ?? ownAlign.alignItemWithTriggerActive;
|
|
641
|
+
useLayoutEffect(() => {
|
|
642
|
+
if (positionerContext || !open) return;
|
|
643
|
+
setReference(refs.triggerRef.current);
|
|
644
|
+
}, [open, positionerContext, refs.triggerRef, setReference]);
|
|
645
|
+
const mergedRef = useCallback(
|
|
646
|
+
(node) => {
|
|
647
|
+
refs.contentRef.current = node;
|
|
648
|
+
setFloating(node);
|
|
649
|
+
},
|
|
650
|
+
[refs.contentRef, setFloating]
|
|
651
|
+
);
|
|
652
|
+
const { present } = usePresence({
|
|
653
|
+
open,
|
|
654
|
+
lazyMount,
|
|
655
|
+
unmountOnExit,
|
|
656
|
+
onOpenChangeComplete
|
|
657
|
+
});
|
|
658
|
+
useClickOutside([refs.contentRef, refs.triggerRef], close, open);
|
|
659
|
+
useEscapeKey({
|
|
660
|
+
enabled: open,
|
|
661
|
+
stopPropagation: true,
|
|
662
|
+
onEscape: close
|
|
663
|
+
});
|
|
664
|
+
useFocusTrap(refs.contentRef, open && config.modal);
|
|
665
|
+
const navItems = Array.from(items.values()).map((item) => ({
|
|
666
|
+
value: item.value,
|
|
667
|
+
disabled: item.disabled
|
|
668
|
+
}));
|
|
669
|
+
const { onKeyDown: onNavKeyDown } = useListNavigation({
|
|
670
|
+
enabled: open && !config.disabled && !config.readOnly,
|
|
671
|
+
items: navItems,
|
|
672
|
+
highlightedValue,
|
|
673
|
+
onHighlight: (v) => store.setHighlightedValue(v),
|
|
674
|
+
loop: true
|
|
675
|
+
});
|
|
676
|
+
const typeaheadItems = Array.from(items.values()).map((item) => ({
|
|
677
|
+
value: item.value,
|
|
678
|
+
disabled: item.disabled,
|
|
679
|
+
textValue: item.textValue
|
|
680
|
+
}));
|
|
681
|
+
const { onKeyDown: onTypeaheadKeyDown } = useTypeahead({
|
|
682
|
+
enabled: open && !config.disabled && !config.readOnly,
|
|
683
|
+
items: typeaheadItems,
|
|
684
|
+
onMatch: (v) => store.setHighlightedValue(v)
|
|
685
|
+
});
|
|
686
|
+
const handleKeyDown = useCallback(
|
|
687
|
+
(e) => {
|
|
688
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
689
|
+
if (highlightedValue != null) {
|
|
690
|
+
e.preventDefault();
|
|
691
|
+
const item = items.get(highlightedValue);
|
|
692
|
+
if (item && !item.disabled) {
|
|
693
|
+
selectValue(highlightedValue);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
onNavKeyDown(e);
|
|
699
|
+
onTypeaheadKeyDown(e);
|
|
700
|
+
onKeyDown?.(e);
|
|
701
|
+
},
|
|
702
|
+
[highlightedValue, items, selectValue, onNavKeyDown, onTypeaheadKeyDown, onKeyDown]
|
|
703
|
+
);
|
|
704
|
+
const [transitionsReady, setTransitionsReady] = useState(false);
|
|
705
|
+
useEffect(() => {
|
|
706
|
+
if (!open || !isPositioned) {
|
|
707
|
+
setTransitionsReady(false);
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
const raf = requestAnimationFrame(() => setTransitionsReady(true));
|
|
711
|
+
return () => cancelAnimationFrame(raf);
|
|
712
|
+
}, [open, isPositioned]);
|
|
713
|
+
useEffect(() => {
|
|
714
|
+
if (!open || !refs.contentRef.current) return;
|
|
715
|
+
const state = store.getState();
|
|
716
|
+
if (state.highlightedValue == null) {
|
|
717
|
+
const first = navItems.find((i) => !i.disabled);
|
|
718
|
+
if (first) store.setHighlightedValue(first.value);
|
|
719
|
+
}
|
|
720
|
+
refs.contentRef.current.focus({ preventScroll: true });
|
|
721
|
+
}, [open]);
|
|
722
|
+
useEffect(() => {
|
|
723
|
+
if (!open || !highlightedValue) return;
|
|
724
|
+
const item = items.get(highlightedValue);
|
|
725
|
+
if (typeof item?.ref?.scrollIntoView === "function") {
|
|
726
|
+
item.ref.scrollIntoView({ block: "nearest" });
|
|
727
|
+
}
|
|
728
|
+
}, [highlightedValue, items, open]);
|
|
729
|
+
if (!present && !forceMount) return null;
|
|
730
|
+
const content = /* @__PURE__ */ jsx(
|
|
731
|
+
"div",
|
|
732
|
+
{
|
|
733
|
+
ref: mergedRef,
|
|
734
|
+
id: ids.content,
|
|
735
|
+
"data-state": open ? "open" : "closed",
|
|
736
|
+
"data-open": open ? "true" : void 0,
|
|
737
|
+
"data-align-trigger": alignItemWithTriggerActive ? "true" : void 0,
|
|
738
|
+
"aria-modal": config.modal ? "true" : void 0,
|
|
739
|
+
tabIndex: -1,
|
|
740
|
+
style: {
|
|
741
|
+
...floatingStyles,
|
|
742
|
+
...!open ? { display: "none" } : void 0,
|
|
743
|
+
...open && !isPositioned ? { opacity: 0, pointerEvents: "none" } : void 0,
|
|
744
|
+
...open && !transitionsReady ? { transition: "none" } : void 0,
|
|
745
|
+
...style
|
|
746
|
+
},
|
|
747
|
+
onKeyDown: handleKeyDown,
|
|
748
|
+
...props,
|
|
749
|
+
children
|
|
750
|
+
}
|
|
751
|
+
);
|
|
752
|
+
if (isInsidePortal || !portal) {
|
|
753
|
+
return content;
|
|
754
|
+
}
|
|
755
|
+
const mountNode = resolvePortalContainer(container);
|
|
756
|
+
if (!mountNode) {
|
|
757
|
+
return content;
|
|
758
|
+
}
|
|
759
|
+
return createPortal(content, mountNode);
|
|
760
|
+
}
|
|
761
|
+
function Positioner({
|
|
762
|
+
children,
|
|
763
|
+
side = "bottom",
|
|
764
|
+
align = "start",
|
|
765
|
+
sideOffset = 4,
|
|
766
|
+
alignOffset = 0,
|
|
767
|
+
avoidCollisions = true,
|
|
768
|
+
collisionPadding = 8,
|
|
769
|
+
sameWidth = false,
|
|
770
|
+
alignItemWithTrigger = false
|
|
771
|
+
}) {
|
|
772
|
+
const { store, refs } = useSelectContext();
|
|
773
|
+
const open = useSelectStore(store, (s) => s.open);
|
|
774
|
+
const { alignItemWithTriggerActive, effectiveSideOffset, avoidCollisionsOverride } = useAlignItemWithTrigger({
|
|
775
|
+
alignItemWithTrigger,
|
|
776
|
+
side,
|
|
777
|
+
sideOffset,
|
|
778
|
+
open,
|
|
779
|
+
refs
|
|
780
|
+
});
|
|
781
|
+
const { setReference, setFloating, floatingStyles, isPositioned } = useFloating({
|
|
782
|
+
open,
|
|
783
|
+
side,
|
|
784
|
+
align,
|
|
785
|
+
sideOffset: effectiveSideOffset,
|
|
786
|
+
alignOffset,
|
|
787
|
+
avoidCollisions: avoidCollisionsOverride ?? avoidCollisions,
|
|
788
|
+
collisionPadding,
|
|
789
|
+
sameWidth
|
|
790
|
+
});
|
|
791
|
+
useLayoutEffect(() => {
|
|
792
|
+
if (!open) return;
|
|
793
|
+
setReference(refs.triggerRef.current);
|
|
794
|
+
}, [open, refs.triggerRef, setReference]);
|
|
795
|
+
const contextValue = useMemo(
|
|
796
|
+
() => ({
|
|
797
|
+
floatingStyles,
|
|
798
|
+
isPositioned,
|
|
799
|
+
setFloating,
|
|
800
|
+
alignItemWithTriggerActive
|
|
801
|
+
}),
|
|
802
|
+
[floatingStyles, isPositioned, setFloating, alignItemWithTriggerActive]
|
|
803
|
+
);
|
|
804
|
+
return /* @__PURE__ */ jsx(PositionerContext.Provider, { value: contextValue, children });
|
|
805
|
+
}
|
|
806
|
+
function Backdrop({ style, ...props }) {
|
|
807
|
+
const { store, config } = useSelectContext();
|
|
808
|
+
const open = useSelectStore(store, (s) => s.open);
|
|
809
|
+
if (!config.modal) {
|
|
810
|
+
return null;
|
|
811
|
+
}
|
|
812
|
+
return /* @__PURE__ */ jsx(
|
|
813
|
+
"div",
|
|
814
|
+
{
|
|
815
|
+
role: "presentation",
|
|
816
|
+
"aria-hidden": "true",
|
|
817
|
+
"data-state": open ? "open" : "closed",
|
|
818
|
+
style: {
|
|
819
|
+
position: "fixed",
|
|
820
|
+
inset: 0,
|
|
821
|
+
pointerEvents: open ? void 0 : "none",
|
|
822
|
+
...style
|
|
823
|
+
},
|
|
824
|
+
...props
|
|
825
|
+
}
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
function ClearTrigger({ onClick, ...props }) {
|
|
829
|
+
const { store, config, clearValue } = useSelectContext();
|
|
830
|
+
const value = useSelectStore(store, (s) => s.value);
|
|
831
|
+
const hasValue = config.multiple ? Array.isArray(value) && value.length > 0 : value != null;
|
|
832
|
+
const handleActivate = useCallback(
|
|
833
|
+
(e) => {
|
|
834
|
+
e.preventDefault();
|
|
835
|
+
e.stopPropagation();
|
|
836
|
+
clearValue();
|
|
837
|
+
onClick?.(e);
|
|
838
|
+
},
|
|
839
|
+
[clearValue, onClick]
|
|
840
|
+
);
|
|
841
|
+
const handleKeyDown = useCallback(
|
|
842
|
+
(e) => {
|
|
843
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
844
|
+
handleActivate(e);
|
|
845
|
+
}
|
|
846
|
+
},
|
|
847
|
+
[handleActivate]
|
|
848
|
+
);
|
|
849
|
+
if (!hasValue || config.disabled || config.readOnly) {
|
|
850
|
+
return null;
|
|
851
|
+
}
|
|
852
|
+
return /* @__PURE__ */ jsx(
|
|
853
|
+
"span",
|
|
854
|
+
{
|
|
855
|
+
role: "button",
|
|
856
|
+
tabIndex: 0,
|
|
857
|
+
"aria-label": "Clear selection",
|
|
858
|
+
onClick: handleActivate,
|
|
859
|
+
onKeyDown: handleKeyDown,
|
|
860
|
+
...props
|
|
861
|
+
}
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
function List({ children, scrollToIndex, ...props }) {
|
|
865
|
+
const { ids, refs, config, scrollToIndex: scrollToIndexFn } = useSelectContext();
|
|
866
|
+
useEffect(() => {
|
|
867
|
+
if (scrollToIndex == null || scrollToIndex < 0) return;
|
|
868
|
+
scrollToIndexFn(scrollToIndex);
|
|
869
|
+
}, [scrollToIndex, scrollToIndexFn]);
|
|
870
|
+
return /* @__PURE__ */ jsx(
|
|
871
|
+
"ul",
|
|
872
|
+
{
|
|
873
|
+
ref: refs.listRef,
|
|
874
|
+
role: "listbox",
|
|
875
|
+
id: `${ids.content}-list`,
|
|
876
|
+
"aria-labelledby": ids.label,
|
|
877
|
+
"aria-multiselectable": config.multiple ? true : void 0,
|
|
878
|
+
...props,
|
|
879
|
+
children
|
|
880
|
+
}
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
Item.displayName = "Select.Item";
|
|
884
|
+
function Item({
|
|
885
|
+
value,
|
|
886
|
+
disabled = false,
|
|
887
|
+
textValue,
|
|
888
|
+
children,
|
|
889
|
+
onClick,
|
|
890
|
+
onPointerMove,
|
|
891
|
+
...props
|
|
892
|
+
}) {
|
|
893
|
+
const { store, ids, config, selectValue } = useSelectContext();
|
|
894
|
+
const selectedValue = useSelectStore(store, (s) => s.value);
|
|
895
|
+
const highlightedValue = useSelectStore(store, (s) => s.highlightedValue);
|
|
896
|
+
const liRef = useRef(null);
|
|
897
|
+
const isSelected = config.multiple ? Array.isArray(selectedValue) && selectedValue.some((item) => config.isItemEqualToValue(item, value)) : typeof selectedValue === "string" && config.isItemEqualToValue(selectedValue, value);
|
|
898
|
+
const isHighlighted = highlightedValue === value;
|
|
899
|
+
const isDisabled = disabled || config.disabled || config.readOnly;
|
|
900
|
+
useLayoutEffect(() => {
|
|
901
|
+
const el = liRef.current;
|
|
902
|
+
const label = textValue ?? (el?.textContent ?? value);
|
|
903
|
+
store.registerItem({
|
|
904
|
+
value,
|
|
905
|
+
label,
|
|
906
|
+
textValue: textValue ?? label,
|
|
907
|
+
disabled: isDisabled,
|
|
908
|
+
ref: el,
|
|
909
|
+
groupId: null
|
|
910
|
+
});
|
|
911
|
+
return () => store.unregisterItem(value);
|
|
912
|
+
}, [value, isDisabled, store, textValue, children]);
|
|
913
|
+
useLayoutEffect(() => {
|
|
914
|
+
store.updateItemRef(value, liRef.current);
|
|
915
|
+
});
|
|
916
|
+
const handleClick = useCallback(
|
|
917
|
+
(e) => {
|
|
918
|
+
if (isDisabled) return;
|
|
919
|
+
selectValue(value);
|
|
920
|
+
onClick?.(e);
|
|
921
|
+
},
|
|
922
|
+
[isDisabled, selectValue, value, onClick]
|
|
923
|
+
);
|
|
924
|
+
const handlePointerMove = useCallback(
|
|
925
|
+
(e) => {
|
|
926
|
+
if (!isDisabled) store.setHighlightedValue(value);
|
|
927
|
+
onPointerMove?.(e);
|
|
928
|
+
},
|
|
929
|
+
[isDisabled, store, value, onPointerMove]
|
|
930
|
+
);
|
|
931
|
+
return /* @__PURE__ */ jsx(
|
|
932
|
+
"li",
|
|
933
|
+
{
|
|
934
|
+
ref: liRef,
|
|
935
|
+
id: `${ids.content}-opt-${value}`,
|
|
936
|
+
role: "option",
|
|
937
|
+
"aria-selected": isSelected,
|
|
938
|
+
"aria-disabled": isDisabled || void 0,
|
|
939
|
+
"data-highlighted": isHighlighted ? "true" : void 0,
|
|
940
|
+
"data-selected": isSelected ? "true" : void 0,
|
|
941
|
+
"data-disabled": isDisabled ? "true" : void 0,
|
|
942
|
+
tabIndex: -1,
|
|
943
|
+
onClick: handleClick,
|
|
944
|
+
onPointerMove: handlePointerMove,
|
|
945
|
+
...props,
|
|
946
|
+
children
|
|
947
|
+
}
|
|
948
|
+
);
|
|
949
|
+
}
|
|
950
|
+
function ItemText({ children, ...props }) {
|
|
951
|
+
return /* @__PURE__ */ jsx("span", { ...props, children });
|
|
952
|
+
}
|
|
953
|
+
function ItemIndicator({ value, children, style, ...props }) {
|
|
954
|
+
const { store } = useSelectContext();
|
|
955
|
+
const selectedValue = useSelectStore(store, (s) => s.value);
|
|
956
|
+
const isSelected = value != null ? selectedValue === value : false;
|
|
957
|
+
return /* @__PURE__ */ jsx(
|
|
958
|
+
"span",
|
|
959
|
+
{
|
|
960
|
+
"aria-hidden": "true",
|
|
961
|
+
"data-state": isSelected ? "checked" : "unchecked",
|
|
962
|
+
style: { visibility: isSelected ? void 0 : "hidden", ...style },
|
|
963
|
+
...props,
|
|
964
|
+
children
|
|
965
|
+
}
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
var GroupContext = createContext(null);
|
|
969
|
+
function useGroupContext() {
|
|
970
|
+
return useContext(GroupContext);
|
|
971
|
+
}
|
|
972
|
+
function Group({ children, ...props }) {
|
|
973
|
+
const groupId = useId();
|
|
974
|
+
const labelId = `${groupId}-label`;
|
|
975
|
+
return /* @__PURE__ */ jsx(GroupContext.Provider, { value: { labelId }, children: /* @__PURE__ */ jsx("div", { role: "group", "aria-labelledby": labelId, ...props, children }) });
|
|
976
|
+
}
|
|
977
|
+
function GroupLabel({ children, ...props }) {
|
|
978
|
+
const group = useGroupContext();
|
|
979
|
+
return /* @__PURE__ */ jsx("div", { id: group?.labelId, ...props, children });
|
|
980
|
+
}
|
|
981
|
+
function HiddenSelect() {
|
|
982
|
+
const { store, config } = useSelectContext();
|
|
983
|
+
const value = useSelectStore(store, (s) => s.value);
|
|
984
|
+
const registry = useSelectStore(store, (s) => s.items);
|
|
985
|
+
if (!config.name) return null;
|
|
986
|
+
const optionValues = mergeOptionValues(registry, config.items);
|
|
987
|
+
const selectedValues = config.multiple ? Array.isArray(value) ? value : [] : typeof value === "string" ? [value] : [];
|
|
988
|
+
if (config.multiple) {
|
|
989
|
+
const allOptionValues = [
|
|
990
|
+
...optionValues,
|
|
991
|
+
...selectedValues.filter(
|
|
992
|
+
(selected) => !optionValues.some((v) => config.isItemEqualToValue(v, selected))
|
|
993
|
+
)
|
|
994
|
+
];
|
|
995
|
+
return /* @__PURE__ */ jsx(
|
|
996
|
+
"select",
|
|
997
|
+
{
|
|
998
|
+
"aria-hidden": "true",
|
|
999
|
+
tabIndex: -1,
|
|
1000
|
+
name: config.name,
|
|
1001
|
+
multiple: true,
|
|
1002
|
+
required: config.required,
|
|
1003
|
+
disabled: config.disabled,
|
|
1004
|
+
value: selectedValues,
|
|
1005
|
+
onChange: () => void 0,
|
|
1006
|
+
style: {
|
|
1007
|
+
position: "absolute",
|
|
1008
|
+
border: 0,
|
|
1009
|
+
width: 1,
|
|
1010
|
+
height: 1,
|
|
1011
|
+
padding: 0,
|
|
1012
|
+
margin: 0,
|
|
1013
|
+
overflow: "hidden",
|
|
1014
|
+
clip: "rect(0,0,0,0)",
|
|
1015
|
+
whiteSpace: "nowrap",
|
|
1016
|
+
pointerEvents: "none",
|
|
1017
|
+
opacity: 0
|
|
1018
|
+
},
|
|
1019
|
+
"data-part": "hidden-select",
|
|
1020
|
+
children: allOptionValues.map((optionValue) => {
|
|
1021
|
+
const record = registry.get(optionValue);
|
|
1022
|
+
return /* @__PURE__ */ jsx("option", { value: optionValue, disabled: record?.disabled, children: resolveItemLabel(optionValue, registry, config.items) }, optionValue);
|
|
1023
|
+
})
|
|
1024
|
+
}
|
|
1025
|
+
);
|
|
1026
|
+
}
|
|
1027
|
+
const singleValue = typeof value === "string" ? value : "";
|
|
1028
|
+
const hasValueOption = singleValue !== "" && optionValues.some((v) => config.isItemEqualToValue(v, singleValue));
|
|
1029
|
+
return /* @__PURE__ */ jsxs(
|
|
1030
|
+
"select",
|
|
1031
|
+
{
|
|
1032
|
+
"aria-hidden": "true",
|
|
1033
|
+
tabIndex: -1,
|
|
1034
|
+
name: config.name,
|
|
1035
|
+
required: config.required,
|
|
1036
|
+
disabled: config.disabled,
|
|
1037
|
+
value: singleValue,
|
|
1038
|
+
onChange: () => void 0,
|
|
1039
|
+
style: {
|
|
1040
|
+
position: "absolute",
|
|
1041
|
+
border: 0,
|
|
1042
|
+
width: 1,
|
|
1043
|
+
height: 1,
|
|
1044
|
+
padding: 0,
|
|
1045
|
+
margin: 0,
|
|
1046
|
+
overflow: "hidden",
|
|
1047
|
+
clip: "rect(0,0,0,0)",
|
|
1048
|
+
whiteSpace: "nowrap",
|
|
1049
|
+
pointerEvents: "none",
|
|
1050
|
+
opacity: 0
|
|
1051
|
+
},
|
|
1052
|
+
"data-part": "hidden-select",
|
|
1053
|
+
children: [
|
|
1054
|
+
/* @__PURE__ */ jsx("option", { value: "", disabled: true, hidden: true }),
|
|
1055
|
+
optionValues.map((optionValue) => {
|
|
1056
|
+
const record = registry.get(optionValue);
|
|
1057
|
+
return /* @__PURE__ */ jsx("option", { value: optionValue, disabled: record?.disabled, children: resolveItemLabel(optionValue, registry, config.items) }, optionValue);
|
|
1058
|
+
}),
|
|
1059
|
+
singleValue !== "" && !hasValueOption && /* @__PURE__ */ jsx("option", { value: singleValue, children: singleValue })
|
|
1060
|
+
]
|
|
1061
|
+
}
|
|
1062
|
+
);
|
|
1063
|
+
}
|
|
1064
|
+
function useListScrollOverflow(listRef, enabled) {
|
|
1065
|
+
const [canScrollUp, setCanScrollUp] = useState(false);
|
|
1066
|
+
const [canScrollDown, setCanScrollDown] = useState(false);
|
|
1067
|
+
const update = useCallback(() => {
|
|
1068
|
+
const list = listRef.current;
|
|
1069
|
+
if (!list) {
|
|
1070
|
+
setCanScrollUp(false);
|
|
1071
|
+
setCanScrollDown(false);
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
const { scrollTop, scrollHeight, clientHeight } = list;
|
|
1075
|
+
setCanScrollUp(scrollTop > 0);
|
|
1076
|
+
setCanScrollDown(scrollTop + clientHeight < scrollHeight - 1);
|
|
1077
|
+
}, [listRef]);
|
|
1078
|
+
useLayoutEffect(() => {
|
|
1079
|
+
if (!enabled) {
|
|
1080
|
+
setCanScrollUp(false);
|
|
1081
|
+
setCanScrollDown(false);
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
update();
|
|
1085
|
+
const list = listRef.current;
|
|
1086
|
+
if (!list) return;
|
|
1087
|
+
list.addEventListener("scroll", update, { passive: true });
|
|
1088
|
+
const observer = new ResizeObserver(update);
|
|
1089
|
+
observer.observe(list);
|
|
1090
|
+
return () => {
|
|
1091
|
+
list.removeEventListener("scroll", update);
|
|
1092
|
+
observer.disconnect();
|
|
1093
|
+
};
|
|
1094
|
+
}, [enabled, listRef, update]);
|
|
1095
|
+
return { canScrollUp, canScrollDown, update };
|
|
1096
|
+
}
|
|
1097
|
+
var SCROLL_STEP_PX = 40;
|
|
1098
|
+
function ScrollButton({
|
|
1099
|
+
direction,
|
|
1100
|
+
keepMounted = false,
|
|
1101
|
+
onClick,
|
|
1102
|
+
children,
|
|
1103
|
+
style,
|
|
1104
|
+
...props
|
|
1105
|
+
}) {
|
|
1106
|
+
const { store, refs } = useSelectContext();
|
|
1107
|
+
const open = useSelectStore(store, (s) => s.open);
|
|
1108
|
+
const isUp = direction === "up";
|
|
1109
|
+
const { canScrollUp, canScrollDown, update } = useListScrollOverflow(refs.listRef, open);
|
|
1110
|
+
const visible = isUp ? canScrollUp : canScrollDown;
|
|
1111
|
+
const handleClick = useCallback(
|
|
1112
|
+
(event) => {
|
|
1113
|
+
const list = refs.listRef.current;
|
|
1114
|
+
if (list) {
|
|
1115
|
+
const delta = isUp ? -SCROLL_STEP_PX : SCROLL_STEP_PX;
|
|
1116
|
+
if (typeof list.scrollBy === "function") {
|
|
1117
|
+
list.scrollBy({ top: delta, behavior: "smooth" });
|
|
1118
|
+
} else {
|
|
1119
|
+
list.scrollTop += delta;
|
|
1120
|
+
}
|
|
1121
|
+
list.dispatchEvent(new Event("scroll"));
|
|
1122
|
+
update();
|
|
1123
|
+
}
|
|
1124
|
+
onClick?.(event);
|
|
1125
|
+
},
|
|
1126
|
+
[isUp, onClick, refs.listRef, update]
|
|
1127
|
+
);
|
|
1128
|
+
if (!visible && !keepMounted) {
|
|
1129
|
+
return null;
|
|
1130
|
+
}
|
|
1131
|
+
return /* @__PURE__ */ jsx(
|
|
1132
|
+
"button",
|
|
1133
|
+
{
|
|
1134
|
+
type: "button",
|
|
1135
|
+
"aria-hidden": true,
|
|
1136
|
+
tabIndex: -1,
|
|
1137
|
+
"data-direction": direction,
|
|
1138
|
+
"data-visible": visible ? "true" : "false",
|
|
1139
|
+
style: {
|
|
1140
|
+
display: visible || keepMounted ? void 0 : "none",
|
|
1141
|
+
...style
|
|
1142
|
+
},
|
|
1143
|
+
onClick: handleClick,
|
|
1144
|
+
...props,
|
|
1145
|
+
children: children ?? (isUp ? "\u25B2" : "\u25BC")
|
|
1146
|
+
}
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
function ScrollUpButton(props) {
|
|
1150
|
+
return /* @__PURE__ */ jsx(ScrollButton, { direction: "up", ...props });
|
|
1151
|
+
}
|
|
1152
|
+
function ScrollDownButton(props) {
|
|
1153
|
+
return /* @__PURE__ */ jsx(ScrollButton, { direction: "down", ...props });
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
export { index_parts_exports as Select, scrollToIndexInState, useSelectContext };
|
|
1157
|
+
//# sourceMappingURL=index.js.map
|
|
1158
|
+
//# sourceMappingURL=index.js.map
|