@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/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