@bento/listbox 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,664 @@
1
+ import React, { createContext, useContext, useMemo, ForwardedRef, RefObject } from 'react';
2
+ import {
3
+ FocusScope,
4
+ mergeProps,
5
+ useCollator,
6
+ useLocale,
7
+ useListBox,
8
+ useFocusRing,
9
+ ListKeyboardDelegate
10
+ } from 'react-aria';
11
+ import { ListState, SelectionBehavior, useListState, Orientation, Node } from 'react-stately';
12
+ import { CollectionBuilder, Collection as AriaCollection } from '@react-aria/collections';
13
+ import { AriaListBoxProps } from '@react-types/listbox';
14
+ import { useDataAttributes } from '@bento/use-data-attributes';
15
+ import { useProps } from '@bento/use-props';
16
+ import { withSlots, Slots } from '@bento/slots';
17
+ import { ListBoxItemImpl } from './listbox-item';
18
+ import { ListBoxSectionInner } from './listbox-section';
19
+ import { useSafeObjectRef } from './utils';
20
+
21
+ /**
22
+ * Render props provided to ListBox render functions and empty state renderers.
23
+ * @interface ListBoxRenderProps
24
+ */
25
+ export interface ListBoxRenderProps {
26
+ /**
27
+ * Whether the listbox has no items and should display its empty state.
28
+ * @selector [data-empty]
29
+ */
30
+ readonly isEmpty: boolean;
31
+ /**
32
+ * Whether the listbox is currently focused.
33
+ * @selector [data-focused]
34
+ */
35
+ readonly isFocused: boolean;
36
+ /**
37
+ * Whether the listbox is currently keyboard focused.
38
+ * @selector [data-focus-visible]
39
+ */
40
+ readonly isFocusVisible: boolean;
41
+ /**
42
+ * Whether the listbox is currently the active drop target.
43
+ * @selector [data-drop-target]
44
+ */
45
+ readonly isDropTarget: boolean;
46
+ /**
47
+ * Whether the items are arranged in a stack or grid.
48
+ * @selector [data-layout="stack | grid"]
49
+ */
50
+ readonly layout?: 'stack' | 'grid';
51
+ /**
52
+ * State of the listbox.
53
+ */
54
+ readonly state: ListState<unknown>;
55
+ /**
56
+ * The items array when using dynamic collections.
57
+ */
58
+ readonly items?: Iterable<unknown>;
59
+ }
60
+
61
+ /**
62
+ * Props for the ListBox component.
63
+ * @interface ListBoxProps
64
+ * @template T The type of items in the collection
65
+ */
66
+ export interface ListBoxProps<T>
67
+ extends Omit<AriaListBoxProps<T>, 'label' | 'children'>,
68
+ Omit<React.ComponentProps<'div'>, keyof AriaListBoxProps<T> | 'children'>,
69
+ Slots {
70
+ /**
71
+ * How multiple selection should behave in the collection.
72
+ */
73
+ readonly selectionBehavior?: SelectionBehavior;
74
+ /**
75
+ * Provides content to display when there are no items in the list.
76
+ */
77
+ readonly renderEmptyState?: (props: ListBoxRenderProps) => React.ReactNode;
78
+ /**
79
+ * Whether the items are arranged in a stack layout.
80
+ * @default 'stack'
81
+ */
82
+ readonly layout?: 'stack';
83
+ /**
84
+ * The primary orientation of the items. Usually this is the direction that the collection scrolls.
85
+ * @default 'vertical'
86
+ */
87
+ readonly orientation?: Orientation;
88
+ /**
89
+ * Static children or render function for the ListBox.
90
+ * When items prop is provided, children receives individual items for React Aria compatibility.
91
+ * When no items prop is provided, children receives Bento render props { isEmpty, isFocused, state, etc. }.
92
+ */
93
+ readonly children?:
94
+ | React.ReactNode
95
+ | ((item: T) => React.ReactNode)
96
+ | ((props: ListBoxRenderProps) => React.ReactNode);
97
+ }
98
+
99
+ /**
100
+ * React context for sharing ListBox state across components.
101
+ * This context provides the ListBox state to child components like ListBoxItem and ListBoxSection,
102
+ * enabling them to access selection state, collection data, and other shared functionality.
103
+ *
104
+ * @context
105
+ * @internal
106
+ */
107
+ const ListStateContext = createContext<ListState<unknown> | null>(null);
108
+
109
+ /**
110
+ * Custom hook to manage ListBox state creation and context handling.
111
+ * This hook either uses an existing state from context or creates a new one.
112
+ * It's designed to work both as a standalone component and within a parent component
113
+ * that provides ListBox state through context.
114
+ *
115
+ * @param {Record<string, unknown>} props - Configuration object for the ListBox state
116
+ * @returns {object} An object containing the state instance and context state flag
117
+ * @returns {ListState<unknown>} returns.state - The ListBox state instance
118
+ * @returns {ListState<unknown> | null} returns.contextState - Existing context state, if any
119
+ * @internal
120
+ */
121
+ function useListBoxState(props: Record<string, unknown>) {
122
+ const contextState = useContext(ListStateContext);
123
+
124
+ const stateProps = {
125
+ ...props,
126
+ children: undefined,
127
+ items: undefined
128
+ };
129
+
130
+ const state = contextState ?? useListState(stateProps);
131
+
132
+ return { state, contextState };
133
+ }
134
+
135
+ /**
136
+ * Renders content with optional context provider wrapper.
137
+ * If no context state exists, wraps the content in a ListStateContext.Provider.
138
+ * This allows the ListBox to work both standalone and as part of a larger component tree.
139
+ *
140
+ * @param {React.ReactNode} content - The React content to render
141
+ * @param {ListState<unknown>} state - The ListBox state to provide via context
142
+ * @param {ListState<unknown> | null} contextState - Existing context state, if any
143
+ * @returns {React.ReactNode} The content, optionally wrapped in a context provider
144
+ * @internal
145
+ */
146
+ function renderWithOptionalContext(
147
+ content: React.ReactNode,
148
+ state: ListState<unknown>,
149
+ contextState: ListState<unknown> | null
150
+ ): React.ReactNode {
151
+ /* v8 ignore next */
152
+ return contextState ? content : <ListStateContext.Provider value={state}>{content}</ListStateContext.Provider>;
153
+ }
154
+
155
+ /**
156
+ * Creates and memoizes a keyboard delegate for the ListBox.
157
+ * The keyboard delegate handles keyboard navigation logic, including
158
+ * arrow key navigation, home/end keys, and type-ahead functionality.
159
+ *
160
+ * @param {object} config - Configuration object for the keyboard delegate
161
+ * @param {ListState<unknown>['collection']} config.collection - The collection of items in the ListBox
162
+ * @param {Intl.Collator} config.collator - Intl collator for string comparison in type-ahead
163
+ * @param {React.RefObject<HTMLDivElement>} config.listBoxRef - Reference to the ListBox DOM element
164
+ * @param {ListState<unknown>['selectionManager']} config.selectionManager - Selection manager from the state
165
+ * @param {'stack' | 'grid'} [config.layout] - Layout mode (stack or grid)
166
+ * @param {Orientation} [config.orientation] - Primary orientation of the items
167
+ * @param {'ltr' | 'rtl'} config.direction - Text direction (ltr or rtl)
168
+ * @param {ListKeyboardDelegate<unknown>} [config.keyboardDelegate] - Custom keyboard delegate to use instead of default
169
+ * @returns {ListKeyboardDelegate<unknown>} A keyboard delegate instance for handling keyboard interactions
170
+ * @internal
171
+ */
172
+ function useKeyboardDelegate({
173
+ collection,
174
+ collator,
175
+ listBoxRef,
176
+ selectionManager,
177
+ layout,
178
+ orientation,
179
+ direction,
180
+ keyboardDelegate: providedDelegate
181
+ }: {
182
+ readonly collection: ListState<unknown>['collection'];
183
+ readonly collator: Intl.Collator;
184
+ readonly listBoxRef: React.RefObject<HTMLDivElement>;
185
+ readonly selectionManager: ListState<unknown>['selectionManager'];
186
+ readonly layout?: 'stack' | 'grid';
187
+ readonly orientation?: Orientation;
188
+ readonly direction: 'ltr' | 'rtl';
189
+ readonly keyboardDelegate?: ListKeyboardDelegate<unknown>;
190
+ }): ListKeyboardDelegate<unknown> {
191
+ const { disabledBehavior, disabledKeys } = selectionManager;
192
+
193
+ return useMemo(
194
+ function createKeyboardDelegate() {
195
+ return (
196
+ providedDelegate ||
197
+ new ListKeyboardDelegate({
198
+ collection,
199
+ collator,
200
+ ref: listBoxRef,
201
+ disabledKeys,
202
+ disabledBehavior,
203
+ layout,
204
+ orientation,
205
+ direction
206
+ })
207
+ );
208
+ },
209
+ [collection, collator, listBoxRef, selectionManager, orientation, direction, layout, providedDelegate]
210
+ );
211
+ }
212
+
213
+ /**
214
+ * Generates data attributes for the ListBox element based on its current state.
215
+ * These attributes are used for styling selectors and accessibility indicators.
216
+ *
217
+ * @param {object} config - Configuration object containing ListBox state flags
218
+ * @param {boolean} config.isEmpty - Whether the listbox has no items
219
+ * @param {boolean} config.isFocused - Whether the listbox is currently focused
220
+ * @param {boolean} config.isFocusVisible - Whether focus should be visually indicated
221
+ * @param {'stack' | 'grid'} [config.layout] - Layout mode (stack or grid)
222
+ * @param {Orientation} [config.orientation] - Primary orientation of the items
223
+ * @param {ListState<unknown>['selectionManager']} config.selectionManager - Selection manager containing selection state
224
+ * @param {boolean} [config.allowsTabNavigation] - Whether tab navigation is enabled
225
+ * @param {boolean} [config.shouldFocusWrap] - Whether focus wraps at boundaries
226
+ * @param {SelectionBehavior} [config.originalSelectionBehavior] - Original selection behavior setting
227
+ * @returns {Record<string, unknown>} Object with data attributes for the ListBox element
228
+ * @internal
229
+ */
230
+ function useListBoxDataAttributes({
231
+ isEmpty,
232
+ isFocused,
233
+ isFocusVisible,
234
+ layout,
235
+ orientation,
236
+ selectionManager,
237
+ allowsTabNavigation,
238
+ shouldFocusWrap,
239
+ originalSelectionBehavior
240
+ }: {
241
+ readonly isEmpty: boolean;
242
+ readonly isFocused: boolean;
243
+ readonly isFocusVisible: boolean;
244
+ readonly layout?: 'stack' | 'grid';
245
+ readonly orientation?: Orientation;
246
+ readonly selectionManager: ListState<unknown>['selectionManager'];
247
+ readonly allowsTabNavigation?: boolean;
248
+ readonly shouldFocusWrap?: boolean;
249
+ readonly originalSelectionBehavior?: SelectionBehavior;
250
+ }) {
251
+ return useDataAttributes({
252
+ empty: isEmpty,
253
+ focused: isFocused,
254
+ 'focus-visible': isFocusVisible,
255
+ layout,
256
+ orientation,
257
+ 'selection-mode': selectionManager.selectionMode !== 'none' ? selectionManager.selectionMode : undefined,
258
+ 'selection-behavior': originalSelectionBehavior !== undefined ? selectionManager.selectionBehavior : undefined,
259
+ 'allows-tab-navigation': allowsTabNavigation,
260
+ 'focus-wrap': shouldFocusWrap
261
+ });
262
+ }
263
+
264
+ /**
265
+ * Composes all props for the ListBox element including DOM props, ARIA props,
266
+ * focus props, and data attributes. Handles prop application through useProps
267
+ * and manages ref assignment to avoid proxy extensibility issues.
268
+ *
269
+ * @param {object} config - Configuration object containing all props to compose
270
+ * @param {Record<string, unknown>} config.otherProps - Additional props from the component
271
+ * @param {ListBoxRenderProps} config.renderValues - Values available to render functions
272
+ * @param {Record<string, unknown>} config.listBoxProps - Props from useListBox hook
273
+ * @param {Record<string, unknown>} config.focusProps - Props from useFocusRing hook
274
+ * @param {Record<string, unknown>} config.dataAttributes - Data attributes for styling/selectors
275
+ * @param {ListState<unknown>['selectionManager']} config.selectionManager - Selection manager for ARIA attributes
276
+ * @param {React.RefObject<HTMLDivElement>} config.listBoxRef - Reference to attach to the final element
277
+ * @returns {Record<string, unknown>} Composed props object ready for the ListBox element
278
+ * @internal
279
+ */
280
+ function useComposedProps({
281
+ otherProps,
282
+ renderValues,
283
+ listBoxProps,
284
+ focusProps,
285
+ dataAttributes,
286
+ selectionManager,
287
+ listBoxRef
288
+ }: {
289
+ readonly otherProps: Record<string, unknown>;
290
+ readonly renderValues: ListBoxRenderProps;
291
+ readonly listBoxProps: Record<string, unknown>;
292
+ readonly focusProps: Record<string, unknown>;
293
+ readonly dataAttributes: Record<string, unknown>;
294
+ readonly selectionManager: ListState<unknown>['selectionManager'];
295
+ readonly listBoxRef: React.RefObject<HTMLDivElement>;
296
+ }) {
297
+ const { apply } = useProps(otherProps, renderValues);
298
+
299
+ const propsToExclude = [
300
+ 'renderEmptyState',
301
+ 'selectionMode',
302
+ 'defaultSelectedKeys',
303
+ 'disabledKeys',
304
+ 'disallowEmptySelection',
305
+ 'shouldFocusWrap',
306
+ 'items',
307
+ 'children',
308
+ 'selectionBehavior',
309
+ 'keyboardDelegate'
310
+ ];
311
+
312
+ // Apply user props directly (preserves className, style, etc.)
313
+ const appliedUserProps = apply(otherProps, propsToExclude);
314
+
315
+ // React Aria and Bento props
316
+ const baseProps = {
317
+ ...mergeProps(listBoxProps, focusProps),
318
+ ...dataAttributes,
319
+ ...(selectionManager.selectionMode !== 'none' && {
320
+ 'aria-multiselectable': selectionManager.selectionMode === 'multiple'
321
+ })
322
+ };
323
+
324
+ //
325
+ // Merge all props together with user props taking precedence
326
+ //
327
+ const finalProps = {
328
+ ...baseProps,
329
+ ...appliedUserProps,
330
+ ref: listBoxRef // Set ref directly to avoid extensibility issues
331
+ };
332
+
333
+ return finalProps;
334
+ }
335
+
336
+ /**
337
+ * Renders the empty state content for the ListBox when no items are present.
338
+ * Handles both function-based render props and direct JSX elements.
339
+ * If a function is provided, calls it with render values; otherwise returns as-is.
340
+ *
341
+ * @param {(props: ListBoxRenderProps) => React.ReactNode} renderEmptyStateFn - Function or JSX element to render for empty state
342
+ * @param {ListBoxRenderProps} renderValues - Current render values to pass to render function
343
+ * @returns {React.ReactNode} Rendered empty state content
344
+ * @internal
345
+ */
346
+ function renderEmptyState(
347
+ renderEmptyStateFn: (props: ListBoxRenderProps) => React.ReactNode,
348
+ renderValues: ListBoxRenderProps
349
+ ): React.ReactNode {
350
+ //
351
+ // Handle cases where renderEmptyState is not a function (e.g., JSX element passed directly)
352
+ //
353
+ if (typeof renderEmptyStateFn === 'function') {
354
+ return renderEmptyStateFn(renderValues);
355
+ }
356
+ //
357
+ // If it's not a function, just return it as-is (likely a JSX element)
358
+ //
359
+ return renderEmptyStateFn as React.ReactNode;
360
+ }
361
+
362
+ /**
363
+ * Renders all items in the collection as React elements.
364
+ * Handles both regular items and section items, using the appropriate
365
+ * components (ListBoxItemImpl for items, ListBoxSectionInner for sections).
366
+ *
367
+ * @param {ListState<unknown>['collection']} collection - The collection of items to render
368
+ * @returns {React.ReactElement[]} Array of rendered React elements for all collection items
369
+ * @internal
370
+ */
371
+ function renderCollectionItems(collection: ListState<unknown>['collection']): React.ReactElement[] {
372
+ return [...collection].map(function renderCollectionItem(item: Node<unknown>) {
373
+ return item.type === 'section' ? (
374
+ <ListBoxSectionInner key={item.key} section={item} />
375
+ ) : (
376
+ <ListBoxItemImpl key={item.key} __node={item as Node<object>}>
377
+ {item.rendered}
378
+ </ListBoxItemImpl>
379
+ );
380
+ });
381
+ }
382
+
383
+ /**
384
+ * Determines what content to render inside the ListBox based on its configuration.
385
+ * Handles three cases:
386
+ * 1. Function children without items (Bento render prop pattern with full render props)
387
+ * 2. Empty state when no items and renderEmptyState is provided
388
+ * 3. Normal collection rendering (including items with children functions for React Aria compatibility)
389
+ *
390
+ * @param {object} config - Configuration object for rendering
391
+ * @param {React.ReactNode | ((item: unknown) => React.ReactNode) | ((props: ListBoxRenderProps) => React.ReactNode)} [config.children] - Children prop (static, item render function, or ListBox render prop)
392
+ * @param {Iterable<unknown>} [config.items] - Items array for dynamic collections
393
+ * @param {boolean} config.isEmpty - Whether the collection is empty
394
+ * @param {(props: ListBoxRenderProps) => React.ReactNode} [config.renderEmptyStateProp] - Function to render empty state
395
+ * @param {ListBoxRenderProps} config.renderValues - Current render values for render functions
396
+ * @param {ListState<unknown>['collection']} config.collection - The collection state to render
397
+ * @returns {React.ReactNode} The appropriate content to render inside the ListBox
398
+ * @internal
399
+ */
400
+ function renderListBoxContent({
401
+ children,
402
+ items,
403
+ isEmpty,
404
+ renderEmptyStateProp,
405
+ renderValues,
406
+ collection
407
+ }: {
408
+ readonly children?:
409
+ | React.ReactNode
410
+ | ((item: unknown) => React.ReactNode)
411
+ | ((props: ListBoxRenderProps) => React.ReactNode);
412
+ readonly items?: Iterable<unknown>;
413
+ readonly isEmpty: boolean;
414
+ readonly renderEmptyStateProp?: (props: ListBoxRenderProps) => React.ReactNode;
415
+ readonly renderValues: ListBoxRenderProps;
416
+ readonly collection: ListState<unknown>['collection'];
417
+ }): React.ReactNode {
418
+ // If children is a function AND no items provided, use Bento render prop pattern
419
+ /* v8 ignore next */
420
+ const hasRenderChildren = typeof children === 'function' && !items;
421
+
422
+ /* v8 ignore next 3 */
423
+ if (hasRenderChildren) {
424
+ return (children as (props: ListBoxRenderProps) => React.ReactNode)(renderValues);
425
+ }
426
+
427
+ // Handle empty state
428
+ if (isEmpty && renderEmptyStateProp) {
429
+ return renderEmptyState(renderEmptyStateProp, renderValues);
430
+ }
431
+
432
+ // Render collection items (React Aria handles items + children function internally)
433
+ return renderCollectionItems(collection);
434
+ }
435
+
436
+ /**
437
+ * Internal ListBox component that handles the core rendering logic.
438
+ * This component manages all the hooks, state, and prop composition needed
439
+ * for a fully functional ListBox. It's wrapped by the main ListBox component
440
+ * which handles collection building.
441
+ *
442
+ * @param {object} props - Component props
443
+ * @param {ListState<unknown>} props.state - The ListBox state instance
444
+ * @param {(props: ListBoxRenderProps) => React.ReactNode} [props.renderEmptyState] - Function to render when no items are present
445
+ * @param {React.ReactNode | ((item: unknown) => React.ReactNode) | ((props: ListBoxRenderProps) => React.ReactNode)} [props.children] - Static children, item render function, or ListBox render function
446
+ * @param {Iterable<unknown>} [props.items] - Items array for dynamic collections
447
+ * @param {React.RefObject<HTMLDivElement>} props.listBoxRef - Reference to the ListBox DOM element
448
+ * @param {'stack'} [props.layout] - Layout mode (stack or grid)
449
+ * @param {Orientation} [props.orientation] - Primary orientation of the items
450
+ * @param {boolean} [props.shouldSelectOnPressUp] - Whether selection occurs on pointer up
451
+ * @param {ListKeyboardDelegate<unknown>} [props.keyboardDelegate] - Custom keyboard navigation delegate
452
+ * @param {boolean} [props.allowsTabNavigation] - Whether tab key navigates between items
453
+ * @param {boolean} [props.shouldFocusWrap] - Whether focus wraps at boundaries
454
+ * @param {'none' | 'single' | 'multiple'} [props.selectionMode] - Selection mode (none, single, multiple)
455
+ * @param {SelectionBehavior} [props.selectionBehavior] - Selection behavior (toggle, replace)
456
+ * @returns {React.ReactElement} A fully functional ListBox element with focus scope
457
+ * @internal
458
+ */
459
+ const ListBoxInner: React.FC<{
460
+ readonly state: ListState<unknown>;
461
+ readonly renderEmptyState?: (props: ListBoxRenderProps) => React.ReactNode;
462
+ readonly children?:
463
+ | React.ReactNode
464
+ | ((item: unknown) => React.ReactNode)
465
+ | ((props: ListBoxRenderProps) => React.ReactNode);
466
+ readonly items?: Iterable<unknown>;
467
+ readonly listBoxRef: RefObject<HTMLDivElement>;
468
+ readonly layout?: 'stack';
469
+ readonly orientation?: Orientation;
470
+ readonly shouldSelectOnPressUp?: boolean;
471
+ readonly keyboardDelegate?: ListKeyboardDelegate<unknown>;
472
+ readonly allowsTabNavigation?: boolean;
473
+ readonly shouldFocusWrap?: boolean;
474
+ readonly selectionMode?: 'none' | 'single' | 'multiple';
475
+ readonly selectionBehavior?: SelectionBehavior;
476
+ }> = function ListBoxInner({
477
+ state,
478
+ renderEmptyState: renderEmptyStateProp,
479
+ children,
480
+ items,
481
+ listBoxRef,
482
+ ...otherProps
483
+ }) {
484
+ const { layout = 'stack', orientation = 'vertical', shouldSelectOnPressUp, selectionBehavior } = otherProps;
485
+
486
+ const { collection, selectionManager } = state;
487
+ const { direction } = useLocale();
488
+ const collator = useCollator({ usage: 'search', sensitivity: 'base' });
489
+
490
+ const keyboardDelegate = useKeyboardDelegate({
491
+ collection,
492
+ collator,
493
+ listBoxRef,
494
+ selectionManager,
495
+ layout,
496
+ orientation,
497
+ direction,
498
+ keyboardDelegate: otherProps.keyboardDelegate
499
+ });
500
+
501
+ const { listBoxProps } = useListBox(
502
+ {
503
+ ...otherProps,
504
+ shouldSelectOnPressUp,
505
+ keyboardDelegate
506
+ },
507
+ state,
508
+ listBoxRef
509
+ );
510
+
511
+ const { focusProps, isFocused, isFocusVisible } = useFocusRing();
512
+ const isEmpty = state.collection.size === 0;
513
+
514
+ const renderValues: ListBoxRenderProps = {
515
+ isEmpty,
516
+ isFocused,
517
+ isFocusVisible,
518
+ isDropTarget: false,
519
+ layout,
520
+ state,
521
+ items
522
+ };
523
+
524
+ const dataAttributes = useListBoxDataAttributes({
525
+ isEmpty,
526
+ isFocused,
527
+ isFocusVisible,
528
+ layout,
529
+ orientation,
530
+ selectionManager,
531
+ allowsTabNavigation: otherProps.allowsTabNavigation,
532
+ shouldFocusWrap: otherProps.shouldFocusWrap,
533
+ originalSelectionBehavior: selectionBehavior
534
+ });
535
+
536
+ const composedProps = useComposedProps({
537
+ otherProps,
538
+ renderValues,
539
+ listBoxProps: listBoxProps as Record<string, unknown>,
540
+ focusProps: focusProps as Record<string, unknown>,
541
+ dataAttributes,
542
+ selectionManager,
543
+ listBoxRef
544
+ });
545
+
546
+ return (
547
+ <FocusScope>
548
+ <div {...composedProps}>
549
+ {renderListBoxContent({
550
+ children,
551
+ items,
552
+ isEmpty,
553
+ renderEmptyStateProp,
554
+ renderValues,
555
+ collection
556
+ })}
557
+ </div>
558
+ </FocusScope>
559
+ );
560
+ };
561
+
562
+ /**
563
+ * A complete ListBox component providing accessible selection lists with keyboard navigation.
564
+ * Supports both static children and dynamic collections, with single/multiple selection modes.
565
+ * Built on React Aria with full ARIA compliance and keyboard accessibility.
566
+ *
567
+ * @component
568
+ * @template T The type of items in the collection
569
+ * @param {ListBoxProps<T>} args - The properties passed to the ListBox component
570
+ * @param {React.ForwardedRef<HTMLDivElement>} ref - The ref to the listbox container
571
+ * @returns {React.ReactElement} A ListBox component
572
+ *
573
+ * @example
574
+ * ```tsx
575
+ * <ListBox aria-label="Fruits" selectionMode="single">
576
+ * <ListBoxItem id="apple" textValue="Apple">Apple</ListBoxItem>
577
+ * <ListBoxItem id="banana" textValue="Banana">Banana</ListBoxItem>
578
+ * </ListBox>
579
+ * ```
580
+ * @public
581
+ */
582
+ function ListBoxComponent<T>(args: ListBoxProps<T>, ref: React.ForwardedRef<HTMLDivElement>): React.ReactElement {
583
+ return (
584
+ <CollectionBuilder content={<AriaCollection {...(args as unknown as Parameters<typeof AriaCollection>[0])} />}>
585
+ {function buildCollection(collection: unknown) {
586
+ return <StandaloneListBox props={args as ListBoxProps<unknown>} listBoxRef={ref} collection={collection} />;
587
+ }}
588
+ </CollectionBuilder>
589
+ );
590
+ }
591
+
592
+ /**
593
+ * Standalone ListBox component that manages its own state and collection.
594
+ * This component is used internally by the main ListBox component after
595
+ * collection building is complete. It handles prop processing, state creation,
596
+ * and context management.
597
+ *
598
+ * @param {object} props - Component props
599
+ * @param {ListBoxProps<unknown>} props.props - The original ListBox props
600
+ * @param {React.ForwardedRef<HTMLDivElement>} props.listBoxRef - Reference to forward to the ListBox element
601
+ * @param {unknown} props.collection - Built collection from CollectionBuilder
602
+ * @returns {React.ReactElement} A complete ListBox with state management and optional context wrapping
603
+ * @internal
604
+ */
605
+ const StandaloneListBox: React.FC<{
606
+ readonly props: ListBoxProps<unknown>;
607
+ readonly listBoxRef: ForwardedRef<HTMLDivElement>;
608
+ readonly collection: unknown;
609
+ }> = function StandaloneListBox({ props, listBoxRef, collection }) {
610
+ //
611
+ // Extract renderEmptyState before useProps processes it to avoid render prop corruption
612
+ //
613
+ const originalRenderEmptyState = props.renderEmptyState;
614
+
615
+ const { props: processedProps } = useProps(props);
616
+ const processedRef = useSafeObjectRef(listBoxRef);
617
+ const { state, contextState } = useListBoxState({ ...processedProps, collection });
618
+
619
+ const { renderEmptyState: _, ...cleanProcessedProps } = processedProps as ListBoxProps<unknown> & {
620
+ renderEmptyState?: unknown;
621
+ };
622
+
623
+ const content = (
624
+ <ListBoxInner
625
+ state={state}
626
+ listBoxRef={processedRef}
627
+ renderEmptyState={originalRenderEmptyState}
628
+ {...cleanProcessedProps}
629
+ />
630
+ );
631
+
632
+ return renderWithOptionalContext(content, state, contextState);
633
+ };
634
+
635
+ /**
636
+ * A complete ListBox component providing accessible selection lists with keyboard navigation.
637
+ * Supports both static children and dynamic collections, with single/multiple selection modes.
638
+ * Built on React Aria with full ARIA compliance and keyboard accessibility.
639
+ *
640
+ * @component
641
+ * @example
642
+ * ```tsx
643
+ * <ListBox aria-label="Fruits" selectionMode="single">
644
+ * <ListBoxItem id="apple" textValue="Apple">Apple</ListBoxItem>
645
+ * <ListBoxItem id="banana" textValue="Banana">Banana</ListBoxItem>
646
+ * </ListBox>
647
+ * ```
648
+ * @public
649
+ */
650
+ export const ListBox = withSlots('BentoListBox', ListBoxComponent);
651
+
652
+ /**
653
+ * Collection component for building dynamic collections in ListBox.
654
+ * Re-exported from React Aria Collections.
655
+ * @public
656
+ */
657
+ export { AriaCollection as Collection };
658
+
659
+ /**
660
+ * Context for sharing ListBox state across components.
661
+ * Used internally by ListBoxItem and ListBoxSection components.
662
+ * @public
663
+ */
664
+ export { ListStateContext };