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