@bento/listbox 0.2.1 → 0.2.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.
- package/README.md +0 -4
- package/dist/index.cjs +713 -349
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +156 -151
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +269 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +715 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +28 -20
- package/src/header.tsx +19 -20
- package/src/listbox-item.tsx +9 -8
- package/src/listbox-section.tsx +9 -4
- package/src/listbox.tsx +15 -13
- package/src/utils.ts +1 -1
- package/dist/index.d.ts +0 -264
- package/dist/index.js +0 -367
- package/dist/index.js.map +0 -1
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,715 @@
|
|
|
1
|
+
import React, { createContext, useContext, useEffect, useMemo, useRef } from "react";
|
|
2
|
+
import { Collection as AriaCollection, CollectionBuilder, createBranchComponent, createLeafComponent } from "@react-aria/collections";
|
|
3
|
+
import { useProps } from "@bento/use-props";
|
|
4
|
+
import { withSlots } from "@bento/slots";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
import { FocusScope, ListKeyboardDelegate, mergeProps, useCollator, useFocusRing, useHover, useListBox, useListBoxSection, useLocale, useOption } from "react-aria";
|
|
7
|
+
import { useDataAttributes } from "@bento/use-data-attributes";
|
|
8
|
+
import { useListState } from "react-stately";
|
|
9
|
+
import { CollectionRendererContext } from "react-aria-components";
|
|
10
|
+
//#region src/header.tsx
|
|
11
|
+
/**
|
|
12
|
+
* React context for providing header-related attributes and refs to Header components.
|
|
13
|
+
* Used internally by ListBoxSection to pass heading props to Header elements.
|
|
14
|
+
* @public
|
|
15
|
+
*/
|
|
16
|
+
const HeaderContext = createContext({});
|
|
17
|
+
/**
|
|
18
|
+
* Internal implementation of the BentoHeader component with slots support.
|
|
19
|
+
* This component handles prop processing and context integration.
|
|
20
|
+
* It merges props from useProps and HeaderContext while preserving styling props.
|
|
21
|
+
*
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
const BentoHeaderImpl = withSlots("BentoHeader", function BentoHeader(...args) {
|
|
25
|
+
const [props, ref] = args;
|
|
26
|
+
const { props: processedProps, apply } = useProps(props);
|
|
27
|
+
const contextProps = useContext(HeaderContext);
|
|
28
|
+
const appliedUserProps = apply(processedProps);
|
|
29
|
+
return /* @__PURE__ */ jsx("header", {
|
|
30
|
+
...contextProps,
|
|
31
|
+
...appliedUserProps,
|
|
32
|
+
ref: contextProps.ref || ref,
|
|
33
|
+
children: processedProps.children
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
/**
|
|
37
|
+
* Wrapper component that connects the BentoHeaderImpl to React Aria's collection system.
|
|
38
|
+
* This function serves as an adapter between the createLeafComponent system and
|
|
39
|
+
* the internal BentoHeaderImpl component, ensuring proper prop forwarding and ref handling.
|
|
40
|
+
*
|
|
41
|
+
* @param {HeaderProps} props - Header component props
|
|
42
|
+
* @param {React.ReactNode} [props.children] - React children to render inside the header
|
|
43
|
+
* @param {React.ForwardedRef<HTMLElement>} ref - Forwarded ref to the header element
|
|
44
|
+
* @returns {React.ReactElement} The BentoHeaderImpl component with forwarded props and ref
|
|
45
|
+
* @internal
|
|
46
|
+
*/
|
|
47
|
+
function HeaderWrapper(props, ref) {
|
|
48
|
+
return /* @__PURE__ */ jsx(BentoHeaderImpl, {
|
|
49
|
+
...props,
|
|
50
|
+
ref
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* A Header component for section headings within a ListBox.
|
|
55
|
+
* Provides semantic header structure with proper accessibility attributes
|
|
56
|
+
* and integrates with React Aria's collection system for automatic handling.
|
|
57
|
+
*
|
|
58
|
+
* This is the main public interface for creating headers in ListBox sections.
|
|
59
|
+
* It automatically receives heading props from the parent ListBoxSection via HeaderContext.
|
|
60
|
+
*
|
|
61
|
+
* @component
|
|
62
|
+
* @example
|
|
63
|
+
* ```tsx
|
|
64
|
+
* <ListBoxSection>
|
|
65
|
+
* <Header>Fruits</Header>
|
|
66
|
+
* <ListBoxItem>Apple</ListBoxItem>
|
|
67
|
+
* <ListBoxItem>Banana</ListBoxItem>
|
|
68
|
+
* </ListBoxSection>
|
|
69
|
+
* ```
|
|
70
|
+
* @public
|
|
71
|
+
*/
|
|
72
|
+
const Header = createLeafComponent("header", HeaderWrapper);
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region src/listbox-section.tsx
|
|
75
|
+
/**
|
|
76
|
+
* Internal implementation of the BentoListBoxSection component with slots support.
|
|
77
|
+
* This component handles the core logic for rendering a section within a ListBox,
|
|
78
|
+
* including title rendering, accessibility attributes, and child content management.
|
|
79
|
+
* It integrates with React Aria's useListBoxSection hook for proper ARIA compliance.
|
|
80
|
+
*
|
|
81
|
+
* @internal
|
|
82
|
+
*/
|
|
83
|
+
const BentoListBoxSectionImpl = withSlots("BentoListBoxSection", function BentoListBoxSectionImpl(...restArgs) {
|
|
84
|
+
const [{ __node, children, title: titleProp, ...rest }, ref] = restArgs;
|
|
85
|
+
const { props, apply } = useProps(rest);
|
|
86
|
+
const data = useDataAttributes({ level: __node?.level });
|
|
87
|
+
const headingRef = useRef(null);
|
|
88
|
+
const title = titleProp ?? props.title ?? __node?.rendered;
|
|
89
|
+
const { groupProps, headingProps } = useListBoxSection({
|
|
90
|
+
heading: title,
|
|
91
|
+
"aria-label": props["aria-label"]
|
|
92
|
+
});
|
|
93
|
+
const composed = mergeProps(apply({
|
|
94
|
+
...data,
|
|
95
|
+
...props
|
|
96
|
+
}, [
|
|
97
|
+
"children",
|
|
98
|
+
"title",
|
|
99
|
+
"slot"
|
|
100
|
+
]), groupProps);
|
|
101
|
+
const sectionContent = children || props.children;
|
|
102
|
+
return /* @__PURE__ */ jsx("section", {
|
|
103
|
+
...composed,
|
|
104
|
+
ref,
|
|
105
|
+
children: /* @__PURE__ */ jsxs(HeaderContext.Provider, {
|
|
106
|
+
value: {
|
|
107
|
+
...headingProps,
|
|
108
|
+
ref: headingRef
|
|
109
|
+
},
|
|
110
|
+
children: [title && /* @__PURE__ */ jsx("div", {
|
|
111
|
+
...headingProps,
|
|
112
|
+
children: title
|
|
113
|
+
}), sectionContent]
|
|
114
|
+
})
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
/**
|
|
118
|
+
* Wrapper component that connects BentoListBoxSectionImpl to React Aria's collection system.
|
|
119
|
+
* This function serves as an adapter between createBranchComponent and the internal
|
|
120
|
+
* BentoListBoxSectionImpl, ensuring proper prop forwarding and node injection for sections.
|
|
121
|
+
*
|
|
122
|
+
* @template T - The type of the section node data
|
|
123
|
+
* @param {ListBoxSectionProps} props - ListBoxSection component props
|
|
124
|
+
* @param {string} [props.slot] - Slot name for Bento's slot system
|
|
125
|
+
* @param {React.ReactNode} [props.title] - Title for the section
|
|
126
|
+
* @param {React.ReactNode} [props.children] - Children to render in the section
|
|
127
|
+
* @param {string} [props.aria-label] - ARIA label for accessibility
|
|
128
|
+
* @param {React.ForwardedRef<HTMLElement>} ref - Ref forwarded from the collection system
|
|
129
|
+
* @param {Node<T>} section - React Aria node containing section metadata and collection info
|
|
130
|
+
* @returns {React.ReactElement} The BentoListBoxSectionImpl component with proper node and ref wiring
|
|
131
|
+
* @internal
|
|
132
|
+
*/
|
|
133
|
+
/* v8 ignore start */
|
|
134
|
+
function ListBoxSectionWrapper(props, ref, section) {
|
|
135
|
+
return /* @__PURE__ */ jsx(BentoListBoxSectionImpl, {
|
|
136
|
+
...props,
|
|
137
|
+
__node: section,
|
|
138
|
+
ref
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
/* v8 ignore stop */
|
|
142
|
+
/**
|
|
143
|
+
* Base ListBoxSection component created through React Aria's collection system.
|
|
144
|
+
* This handles the connection to the parent ListBox's collection state and
|
|
145
|
+
* manages the branch structure for nested items.
|
|
146
|
+
* @internal
|
|
147
|
+
*/
|
|
148
|
+
const ListBoxSectionBase = createBranchComponent("section", ListBoxSectionWrapper);
|
|
149
|
+
/**
|
|
150
|
+
* Internal component for rendering dynamic collection sections.
|
|
151
|
+
* This component is used specifically for sections that are part of a dynamic collection,
|
|
152
|
+
* connecting to the ListStateContext and CollectionRendererContext to properly render
|
|
153
|
+
* nested items through React Aria's collection system.
|
|
154
|
+
*
|
|
155
|
+
* @component
|
|
156
|
+
* @param {object} props - The component props containing the section node
|
|
157
|
+
* @param {Node<unknown>} props.section - The React Aria node representing this section in the collection
|
|
158
|
+
* @throws {BentoError} Throws an error if used outside of a ListBox context
|
|
159
|
+
* @returns {React.ReactElement} JSX element representing a dynamically rendered listbox section
|
|
160
|
+
* @internal
|
|
161
|
+
*/
|
|
162
|
+
const ListBoxSectionInner = function ListBoxSectionInner({ section }) {
|
|
163
|
+
const state = useContext(ListStateContext);
|
|
164
|
+
const { CollectionBranch } = useContext(CollectionRendererContext);
|
|
165
|
+
return /* @__PURE__ */ jsx(BentoListBoxSectionImpl, {
|
|
166
|
+
...section.props,
|
|
167
|
+
__node: section,
|
|
168
|
+
children: CollectionBranch && state?.collection ? /* @__PURE__ */ jsx(CollectionBranch, {
|
|
169
|
+
collection: state.collection,
|
|
170
|
+
parent: section
|
|
171
|
+
}) : null
|
|
172
|
+
});
|
|
173
|
+
};
|
|
174
|
+
/**
|
|
175
|
+
* A section component for organizing related items within a ListBox.
|
|
176
|
+
*
|
|
177
|
+
* @component
|
|
178
|
+
* @example
|
|
179
|
+
* ```tsx
|
|
180
|
+
* <ListBoxSection title="Fruits">
|
|
181
|
+
* <ListBoxItem>Apple</ListBoxItem>
|
|
182
|
+
* <ListBoxItem>Banana</ListBoxItem>
|
|
183
|
+
* </ListBoxSection>
|
|
184
|
+
* ```
|
|
185
|
+
* @public
|
|
186
|
+
*/
|
|
187
|
+
const ListBoxSection = ListBoxSectionBase;
|
|
188
|
+
//#endregion
|
|
189
|
+
//#region src/utils.ts
|
|
190
|
+
/* v8 ignore next */
|
|
191
|
+
/**
|
|
192
|
+
* NOTE: This utility will be moved to the new use-collection package.
|
|
193
|
+
* Safe wrapper for React Aria's useObjectRef that handles test environments where refs are not extensible.
|
|
194
|
+
*
|
|
195
|
+
* **Critical for Vitest Browser Mode Testing**: When running tests in Vitest's browser mode with Playwright,
|
|
196
|
+
* the test environment can freeze or make objects non-extensible. React Aria's `useObjectRef` attempts to
|
|
197
|
+
* dynamically add properties to ref objects, which fails with "Cannot add property current, object is not extensible"
|
|
198
|
+
* in these constrained test environments.
|
|
199
|
+
*
|
|
200
|
+
* **Technical Details:**
|
|
201
|
+
* - Vitest browser mode uses Playwright's Chrome DevTools Protocol for test execution
|
|
202
|
+
* - The V8 engine's security model can freeze objects during test isolation
|
|
203
|
+
* - React Aria's useObjectRef uses `Object.defineProperty()` to add reactive properties to refs
|
|
204
|
+
* - This conflicts with frozen objects in browser testing scenarios
|
|
205
|
+
*
|
|
206
|
+
* **Why This Solution Works:**
|
|
207
|
+
* - Creates an internal ref that's always mutable (created in our controlled environment)
|
|
208
|
+
* - Safely forwards values to the external ref using try/catch for frozen object scenarios
|
|
209
|
+
* - Maintains the same ref forwarding behavior as React Aria's useObjectRef in normal environments
|
|
210
|
+
* - Gracefully degrades in test environments without breaking functionality
|
|
211
|
+
*
|
|
212
|
+
* **Production Impact**: Zero. Object freezing only occurs in specific test configurations.
|
|
213
|
+
* In production and development, this behaves identically to React Aria's useObjectRef.
|
|
214
|
+
*
|
|
215
|
+
* @template T - The type of the ref element
|
|
216
|
+
* @param {React.ForwardedRef<T>} ref - The forwarded ref to handle safely
|
|
217
|
+
* @returns {React.RefObject<T>} A safe ref object that works in all environments including frozen test contexts
|
|
218
|
+
* @public
|
|
219
|
+
*/
|
|
220
|
+
function useSafeObjectRef(ref) {
|
|
221
|
+
const internalRef = useRef(null);
|
|
222
|
+
useEffect(function updateForwardedRef() {
|
|
223
|
+
const current = internalRef.current;
|
|
224
|
+
if (typeof ref === "function") ref(current);
|
|
225
|
+
else if (ref && "current" in ref) try {
|
|
226
|
+
ref.current = current;
|
|
227
|
+
} catch {}
|
|
228
|
+
});
|
|
229
|
+
return internalRef;
|
|
230
|
+
}
|
|
231
|
+
//#endregion
|
|
232
|
+
//#region src/listbox.tsx
|
|
233
|
+
/**
|
|
234
|
+
* React context for sharing ListBox state across components.
|
|
235
|
+
* This context provides the ListBox state to child components like ListBoxItem and ListBoxSection,
|
|
236
|
+
* enabling them to access selection state, collection data, and other shared functionality.
|
|
237
|
+
*
|
|
238
|
+
* @context
|
|
239
|
+
* @internal
|
|
240
|
+
*/
|
|
241
|
+
const ListStateContext = createContext(null);
|
|
242
|
+
/**
|
|
243
|
+
* Custom hook to manage ListBox state creation and context handling.
|
|
244
|
+
* This hook either uses an existing state from context or creates a new one.
|
|
245
|
+
* It's designed to work both as a standalone component and within a parent component
|
|
246
|
+
* that provides ListBox state through context.
|
|
247
|
+
*
|
|
248
|
+
* @param {Record<string, unknown>} props - Configuration object for the ListBox state
|
|
249
|
+
* @returns {object} An object containing the state instance and context state flag
|
|
250
|
+
* @returns {ListState<unknown>} returns.state - The ListBox state instance
|
|
251
|
+
* @returns {ListState<unknown> | null} returns.contextState - Existing context state, if any
|
|
252
|
+
* @internal
|
|
253
|
+
*/
|
|
254
|
+
function useListBoxState(props) {
|
|
255
|
+
const contextState = useContext(ListStateContext);
|
|
256
|
+
const stateProps = {
|
|
257
|
+
...props,
|
|
258
|
+
children: void 0,
|
|
259
|
+
items: void 0
|
|
260
|
+
};
|
|
261
|
+
return {
|
|
262
|
+
state: contextState ?? useListState(stateProps),
|
|
263
|
+
contextState
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Renders content with optional context provider wrapper.
|
|
268
|
+
* If no context state exists, wraps the content in a ListStateContext.Provider.
|
|
269
|
+
* This allows the ListBox to work both standalone and as part of a larger component tree.
|
|
270
|
+
*
|
|
271
|
+
* @param {React.ReactNode} content - The React content to render
|
|
272
|
+
* @param {ListState<unknown>} state - The ListBox state to provide via context
|
|
273
|
+
* @param {ListState<unknown> | null} contextState - Existing context state, if any
|
|
274
|
+
* @returns {React.ReactNode} The content, optionally wrapped in a context provider
|
|
275
|
+
* @internal
|
|
276
|
+
*/
|
|
277
|
+
function renderWithOptionalContext(content, state, contextState) {
|
|
278
|
+
/* v8 ignore next */
|
|
279
|
+
return contextState ? content : /* @__PURE__ */ jsx(ListStateContext.Provider, {
|
|
280
|
+
value: state,
|
|
281
|
+
children: content
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Creates and memoizes a keyboard delegate for the ListBox.
|
|
286
|
+
* The keyboard delegate handles keyboard navigation logic, including
|
|
287
|
+
* arrow key navigation, home/end keys, and type-ahead functionality.
|
|
288
|
+
*
|
|
289
|
+
* @param {object} config - Configuration object for the keyboard delegate
|
|
290
|
+
* @param {ListState<unknown>['collection']} config.collection - The collection of items in the ListBox
|
|
291
|
+
* @param {Intl.Collator} config.collator - Intl collator for string comparison in type-ahead
|
|
292
|
+
* @param {React.RefObject<HTMLDivElement>} config.listBoxRef - Reference to the ListBox DOM element
|
|
293
|
+
* @param {ListState<unknown>['selectionManager']} config.selectionManager - Selection manager from the state
|
|
294
|
+
* @param {'stack' | 'grid'} [config.layout] - Layout mode (stack or grid)
|
|
295
|
+
* @param {Orientation} [config.orientation] - Primary orientation of the items
|
|
296
|
+
* @param {'ltr' | 'rtl'} config.direction - Text direction (ltr or rtl)
|
|
297
|
+
* @param {ListKeyboardDelegate<unknown>} [config.keyboardDelegate] - Custom keyboard delegate to use instead of default
|
|
298
|
+
* @returns {ListKeyboardDelegate<unknown>} A keyboard delegate instance for handling keyboard interactions
|
|
299
|
+
* @internal
|
|
300
|
+
*/
|
|
301
|
+
function useKeyboardDelegate({ collection, collator, listBoxRef, selectionManager, layout, orientation, direction, keyboardDelegate: providedDelegate }) {
|
|
302
|
+
const { disabledBehavior, disabledKeys } = selectionManager;
|
|
303
|
+
return useMemo(function createKeyboardDelegate() {
|
|
304
|
+
return providedDelegate || new ListKeyboardDelegate({
|
|
305
|
+
collection,
|
|
306
|
+
collator,
|
|
307
|
+
ref: listBoxRef,
|
|
308
|
+
disabledKeys,
|
|
309
|
+
disabledBehavior,
|
|
310
|
+
layout,
|
|
311
|
+
orientation,
|
|
312
|
+
direction
|
|
313
|
+
});
|
|
314
|
+
}, [
|
|
315
|
+
collection,
|
|
316
|
+
collator,
|
|
317
|
+
listBoxRef,
|
|
318
|
+
selectionManager,
|
|
319
|
+
orientation,
|
|
320
|
+
direction,
|
|
321
|
+
layout,
|
|
322
|
+
providedDelegate
|
|
323
|
+
]);
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Generates data attributes for the ListBox element based on its current state.
|
|
327
|
+
* These attributes are used for styling selectors and accessibility indicators.
|
|
328
|
+
*
|
|
329
|
+
* @param {object} config - Configuration object containing ListBox state flags
|
|
330
|
+
* @param {boolean} config.isEmpty - Whether the listbox has no items
|
|
331
|
+
* @param {boolean} config.isFocused - Whether the listbox is currently focused
|
|
332
|
+
* @param {boolean} config.isFocusVisible - Whether focus should be visually indicated
|
|
333
|
+
* @param {'stack' | 'grid'} [config.layout] - Layout mode (stack or grid)
|
|
334
|
+
* @param {Orientation} [config.orientation] - Primary orientation of the items
|
|
335
|
+
* @param {ListState<unknown>['selectionManager']} config.selectionManager - Selection manager containing selection state
|
|
336
|
+
* @param {boolean} [config.allowsTabNavigation] - Whether tab navigation is enabled
|
|
337
|
+
* @param {boolean} [config.shouldFocusWrap] - Whether focus wraps at boundaries
|
|
338
|
+
* @param {SelectionBehavior} [config.originalSelectionBehavior] - Original selection behavior setting
|
|
339
|
+
* @returns {Record<string, unknown>} Object with data attributes for the ListBox element
|
|
340
|
+
* @internal
|
|
341
|
+
*/
|
|
342
|
+
function useListBoxDataAttributes({ isEmpty, isFocused, isFocusVisible, layout, orientation, selectionManager, allowsTabNavigation, shouldFocusWrap, originalSelectionBehavior }) {
|
|
343
|
+
return useDataAttributes({
|
|
344
|
+
empty: isEmpty,
|
|
345
|
+
focused: isFocused,
|
|
346
|
+
"focus-visible": isFocusVisible,
|
|
347
|
+
layout,
|
|
348
|
+
orientation,
|
|
349
|
+
"selection-mode": selectionManager.selectionMode !== "none" ? selectionManager.selectionMode : void 0,
|
|
350
|
+
"selection-behavior": originalSelectionBehavior !== void 0 ? selectionManager.selectionBehavior : void 0,
|
|
351
|
+
"allows-tab-navigation": allowsTabNavigation,
|
|
352
|
+
"focus-wrap": shouldFocusWrap
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Composes all props for the ListBox element including DOM props, ARIA props,
|
|
357
|
+
* focus props, and data attributes. Handles prop application through useProps
|
|
358
|
+
* and manages ref assignment to avoid proxy extensibility issues.
|
|
359
|
+
*
|
|
360
|
+
* @param {object} config - Configuration object containing all props to compose
|
|
361
|
+
* @param {Record<string, unknown>} config.otherProps - Additional props from the component
|
|
362
|
+
* @param {ListBoxRenderProps} config.renderValues - Values available to render functions
|
|
363
|
+
* @param {Record<string, unknown>} config.listBoxProps - Props from useListBox hook
|
|
364
|
+
* @param {Record<string, unknown>} config.focusProps - Props from useFocusRing hook
|
|
365
|
+
* @param {Record<string, unknown>} config.dataAttributes - Data attributes for styling/selectors
|
|
366
|
+
* @param {ListState<unknown>['selectionManager']} config.selectionManager - Selection manager for ARIA attributes
|
|
367
|
+
* @param {React.RefObject<HTMLDivElement>} config.listBoxRef - Reference to attach to the final element
|
|
368
|
+
* @returns {Record<string, unknown>} Composed props object ready for the ListBox element
|
|
369
|
+
* @internal
|
|
370
|
+
*/
|
|
371
|
+
function useComposedProps({ otherProps, renderValues, listBoxProps, focusProps, dataAttributes, selectionManager, listBoxRef }) {
|
|
372
|
+
const { apply } = useProps(otherProps, renderValues);
|
|
373
|
+
const appliedUserProps = apply(otherProps, [
|
|
374
|
+
"renderEmptyState",
|
|
375
|
+
"selectionMode",
|
|
376
|
+
"defaultSelectedKeys",
|
|
377
|
+
"disabledKeys",
|
|
378
|
+
"disallowEmptySelection",
|
|
379
|
+
"shouldFocusWrap",
|
|
380
|
+
"items",
|
|
381
|
+
"children",
|
|
382
|
+
"selectionBehavior",
|
|
383
|
+
"keyboardDelegate"
|
|
384
|
+
]);
|
|
385
|
+
return {
|
|
386
|
+
...mergeProps(listBoxProps, focusProps),
|
|
387
|
+
...dataAttributes,
|
|
388
|
+
...selectionManager.selectionMode !== "none" && { "aria-multiselectable": selectionManager.selectionMode === "multiple" },
|
|
389
|
+
...appliedUserProps,
|
|
390
|
+
ref: listBoxRef
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Renders the empty state content for the ListBox when no items are present.
|
|
395
|
+
* Handles both function-based render props and direct JSX elements.
|
|
396
|
+
* If a function is provided, calls it with render values; otherwise returns as-is.
|
|
397
|
+
*
|
|
398
|
+
* @param {(props: ListBoxRenderProps) => React.ReactNode} renderEmptyStateFn - Function or JSX element to render for empty state
|
|
399
|
+
* @param {ListBoxRenderProps} renderValues - Current render values to pass to render function
|
|
400
|
+
* @returns {React.ReactNode} Rendered empty state content
|
|
401
|
+
* @internal
|
|
402
|
+
*/
|
|
403
|
+
function renderEmptyState(renderEmptyStateFn, renderValues) {
|
|
404
|
+
if (typeof renderEmptyStateFn === "function") return renderEmptyStateFn(renderValues);
|
|
405
|
+
return renderEmptyStateFn;
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Renders all items in the collection as React elements.
|
|
409
|
+
* Handles both regular items and section items, using the appropriate
|
|
410
|
+
* components (ListBoxItemImpl for items, ListBoxSectionInner for sections).
|
|
411
|
+
*
|
|
412
|
+
* @param {ListState<unknown>['collection']} collection - The collection of items to render
|
|
413
|
+
* @returns {React.ReactElement[]} Array of rendered React elements for all collection items
|
|
414
|
+
* @internal
|
|
415
|
+
*/
|
|
416
|
+
function renderCollectionItems(collection) {
|
|
417
|
+
return [...collection].map(function renderCollectionItem(item) {
|
|
418
|
+
return item.type === "section" ? /* @__PURE__ */ jsx(ListBoxSectionInner, { section: item }, item.key) : /* @__PURE__ */ jsx(ListBoxItemImpl, {
|
|
419
|
+
__node: item,
|
|
420
|
+
children: item.rendered
|
|
421
|
+
}, item.key);
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Determines what content to render inside the ListBox based on its configuration.
|
|
426
|
+
* Handles three cases:
|
|
427
|
+
* 1. Function children without items (Bento render prop pattern with full render props)
|
|
428
|
+
* 2. Empty state when no items and renderEmptyState is provided
|
|
429
|
+
* 3. Normal collection rendering (including items with children functions for React Aria compatibility)
|
|
430
|
+
*
|
|
431
|
+
* @param {object} config - Configuration object for rendering
|
|
432
|
+
* @param {React.ReactNode | ((item: unknown) => React.ReactNode) | ((props: ListBoxRenderProps) => React.ReactNode)} [config.children] - Children prop (static, item render function, or ListBox render prop)
|
|
433
|
+
* @param {Iterable<unknown>} [config.items] - Items array for dynamic collections
|
|
434
|
+
* @param {boolean} config.isEmpty - Whether the collection is empty
|
|
435
|
+
* @param {(props: ListBoxRenderProps) => React.ReactNode} [config.renderEmptyStateProp] - Function to render empty state
|
|
436
|
+
* @param {ListBoxRenderProps} config.renderValues - Current render values for render functions
|
|
437
|
+
* @param {ListState<unknown>['collection']} config.collection - The collection state to render
|
|
438
|
+
* @returns {React.ReactNode} The appropriate content to render inside the ListBox
|
|
439
|
+
* @internal
|
|
440
|
+
*/
|
|
441
|
+
function renderListBoxContent({ children, items, isEmpty, renderEmptyStateProp, renderValues, collection }) {
|
|
442
|
+
/* v8 ignore next 3 */
|
|
443
|
+
if (typeof children === "function" && !items) return children(renderValues);
|
|
444
|
+
if (isEmpty && renderEmptyStateProp) return renderEmptyState(renderEmptyStateProp, renderValues);
|
|
445
|
+
return renderCollectionItems(collection);
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Internal ListBox component that handles the core rendering logic.
|
|
449
|
+
* This component manages all the hooks, state, and prop composition needed
|
|
450
|
+
* for a fully functional ListBox. It's wrapped by the main ListBox component
|
|
451
|
+
* which handles collection building.
|
|
452
|
+
*
|
|
453
|
+
* @param {object} props - Component props
|
|
454
|
+
* @param {ListState<unknown>} props.state - The ListBox state instance
|
|
455
|
+
* @param {(props: ListBoxRenderProps) => React.ReactNode} [props.renderEmptyState] - Function to render when no items are present
|
|
456
|
+
* @param {React.ReactNode | ((item: unknown) => React.ReactNode) | ((props: ListBoxRenderProps) => React.ReactNode)} [props.children] - Static children, item render function, or ListBox render function
|
|
457
|
+
* @param {Iterable<unknown>} [props.items] - Items array for dynamic collections
|
|
458
|
+
* @param {React.RefObject<HTMLDivElement>} props.listBoxRef - Reference to the ListBox DOM element
|
|
459
|
+
* @param {'stack'} [props.layout] - Layout mode (stack or grid)
|
|
460
|
+
* @param {Orientation} [props.orientation] - Primary orientation of the items
|
|
461
|
+
* @param {boolean} [props.shouldSelectOnPressUp] - Whether selection occurs on pointer up
|
|
462
|
+
* @param {ListKeyboardDelegate<unknown>} [props.keyboardDelegate] - Custom keyboard navigation delegate
|
|
463
|
+
* @param {boolean} [props.allowsTabNavigation] - Whether tab key navigates between items
|
|
464
|
+
* @param {boolean} [props.shouldFocusWrap] - Whether focus wraps at boundaries
|
|
465
|
+
* @param {'none' | 'single' | 'multiple'} [props.selectionMode] - Selection mode (none, single, multiple)
|
|
466
|
+
* @param {SelectionBehavior} [props.selectionBehavior] - Selection behavior (toggle, replace)
|
|
467
|
+
* @returns {React.ReactElement} A fully functional ListBox element with focus scope
|
|
468
|
+
* @internal
|
|
469
|
+
*/
|
|
470
|
+
const ListBoxInner = function ListBoxInner({ state, renderEmptyState: renderEmptyStateProp, children, items, listBoxRef, ...otherProps }) {
|
|
471
|
+
const { layout = "stack", orientation = "vertical", shouldSelectOnPressUp, selectionBehavior } = otherProps;
|
|
472
|
+
const { collection, selectionManager } = state;
|
|
473
|
+
const { direction } = useLocale();
|
|
474
|
+
const keyboardDelegate = useKeyboardDelegate({
|
|
475
|
+
collection,
|
|
476
|
+
collator: useCollator({
|
|
477
|
+
usage: "search",
|
|
478
|
+
sensitivity: "base"
|
|
479
|
+
}),
|
|
480
|
+
listBoxRef,
|
|
481
|
+
selectionManager,
|
|
482
|
+
layout,
|
|
483
|
+
orientation,
|
|
484
|
+
direction,
|
|
485
|
+
keyboardDelegate: otherProps.keyboardDelegate
|
|
486
|
+
});
|
|
487
|
+
const { listBoxProps } = useListBox({
|
|
488
|
+
...otherProps,
|
|
489
|
+
shouldSelectOnPressUp,
|
|
490
|
+
keyboardDelegate
|
|
491
|
+
}, state, listBoxRef);
|
|
492
|
+
const { focusProps, isFocused, isFocusVisible } = useFocusRing();
|
|
493
|
+
const isEmpty = state.collection.size === 0;
|
|
494
|
+
const renderValues = {
|
|
495
|
+
isEmpty,
|
|
496
|
+
isFocused,
|
|
497
|
+
isFocusVisible,
|
|
498
|
+
isDropTarget: false,
|
|
499
|
+
layout,
|
|
500
|
+
state,
|
|
501
|
+
items
|
|
502
|
+
};
|
|
503
|
+
return /* @__PURE__ */ jsx(FocusScope, { children: /* @__PURE__ */ jsx("div", {
|
|
504
|
+
...useComposedProps({
|
|
505
|
+
otherProps,
|
|
506
|
+
renderValues,
|
|
507
|
+
listBoxProps,
|
|
508
|
+
focusProps,
|
|
509
|
+
dataAttributes: useListBoxDataAttributes({
|
|
510
|
+
isEmpty,
|
|
511
|
+
isFocused,
|
|
512
|
+
isFocusVisible,
|
|
513
|
+
layout,
|
|
514
|
+
orientation,
|
|
515
|
+
selectionManager,
|
|
516
|
+
allowsTabNavigation: otherProps.allowsTabNavigation,
|
|
517
|
+
shouldFocusWrap: otherProps.shouldFocusWrap,
|
|
518
|
+
originalSelectionBehavior: selectionBehavior
|
|
519
|
+
}),
|
|
520
|
+
selectionManager,
|
|
521
|
+
listBoxRef
|
|
522
|
+
}),
|
|
523
|
+
children: renderListBoxContent({
|
|
524
|
+
children,
|
|
525
|
+
items,
|
|
526
|
+
isEmpty,
|
|
527
|
+
renderEmptyStateProp,
|
|
528
|
+
renderValues,
|
|
529
|
+
collection
|
|
530
|
+
})
|
|
531
|
+
}) });
|
|
532
|
+
};
|
|
533
|
+
/**
|
|
534
|
+
* A complete ListBox component providing accessible selection lists with keyboard navigation.
|
|
535
|
+
* Supports both static children and dynamic collections, with single/multiple selection modes.
|
|
536
|
+
* Built on React Aria with full ARIA compliance and keyboard accessibility.
|
|
537
|
+
*
|
|
538
|
+
* @component
|
|
539
|
+
* @template T The type of items in the collection
|
|
540
|
+
* @param {ListBoxProps<T>} args - The properties passed to the ListBox component
|
|
541
|
+
* @param {React.ForwardedRef<HTMLDivElement>} ref - The ref to the listbox container
|
|
542
|
+
* @returns {React.ReactElement} A ListBox component
|
|
543
|
+
*
|
|
544
|
+
* @example
|
|
545
|
+
* ```tsx
|
|
546
|
+
* <ListBox aria-label="Fruits" selectionMode="single">
|
|
547
|
+
* <ListBoxItem id="apple" textValue="Apple">Apple</ListBoxItem>
|
|
548
|
+
* <ListBoxItem id="banana" textValue="Banana">Banana</ListBoxItem>
|
|
549
|
+
* </ListBox>
|
|
550
|
+
* ```
|
|
551
|
+
* @public
|
|
552
|
+
*/
|
|
553
|
+
function ListBoxComponent(...args) {
|
|
554
|
+
const [_args, ref] = args;
|
|
555
|
+
return /* @__PURE__ */ jsx(CollectionBuilder, {
|
|
556
|
+
content: /* @__PURE__ */ jsx(AriaCollection, { ..._args }),
|
|
557
|
+
children: function buildCollection(collection) {
|
|
558
|
+
return /* @__PURE__ */ jsx(StandaloneListBox, {
|
|
559
|
+
props: _args,
|
|
560
|
+
listBoxRef: ref,
|
|
561
|
+
collection
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Standalone ListBox component that manages its own state and collection.
|
|
568
|
+
* This component is used internally by the main ListBox component after
|
|
569
|
+
* collection building is complete. It handles prop processing, state creation,
|
|
570
|
+
* and context management.
|
|
571
|
+
*
|
|
572
|
+
* @param {object} props - Component props
|
|
573
|
+
* @param {ListBoxProps<unknown>} props.props - The original ListBox props
|
|
574
|
+
* @param {React.ForwardedRef<HTMLDivElement>} props.listBoxRef - Reference to forward to the ListBox element
|
|
575
|
+
* @param {unknown} props.collection - Built collection from CollectionBuilder
|
|
576
|
+
* @returns {React.ReactElement} A complete ListBox with state management and optional context wrapping
|
|
577
|
+
* @internal
|
|
578
|
+
*/
|
|
579
|
+
const StandaloneListBox = function StandaloneListBox({ props, listBoxRef, collection }) {
|
|
580
|
+
const originalRenderEmptyState = props.renderEmptyState;
|
|
581
|
+
const { props: processedProps } = useProps(props);
|
|
582
|
+
const processedRef = useSafeObjectRef(listBoxRef);
|
|
583
|
+
const { state, contextState } = useListBoxState({
|
|
584
|
+
...processedProps,
|
|
585
|
+
collection
|
|
586
|
+
});
|
|
587
|
+
const { renderEmptyState: _, ...cleanProcessedProps } = processedProps;
|
|
588
|
+
return renderWithOptionalContext(/* @__PURE__ */ jsx(ListBoxInner, {
|
|
589
|
+
state,
|
|
590
|
+
listBoxRef: processedRef,
|
|
591
|
+
renderEmptyState: originalRenderEmptyState,
|
|
592
|
+
...cleanProcessedProps
|
|
593
|
+
}), state, contextState);
|
|
594
|
+
};
|
|
595
|
+
/**
|
|
596
|
+
* A complete ListBox component providing accessible selection lists with keyboard navigation.
|
|
597
|
+
* Supports both static children and dynamic collections, with single/multiple selection modes.
|
|
598
|
+
* Built on React Aria with full ARIA compliance and keyboard accessibility.
|
|
599
|
+
*
|
|
600
|
+
* @component
|
|
601
|
+
* @example
|
|
602
|
+
* ```tsx
|
|
603
|
+
* <ListBox aria-label="Fruits" selectionMode="single">
|
|
604
|
+
* <ListBoxItem id="apple" textValue="Apple">Apple</ListBoxItem>
|
|
605
|
+
* <ListBoxItem id="banana" textValue="Banana">Banana</ListBoxItem>
|
|
606
|
+
* </ListBox>
|
|
607
|
+
* ```
|
|
608
|
+
* @public
|
|
609
|
+
*/
|
|
610
|
+
const ListBox = withSlots("BentoListBox", ListBoxComponent);
|
|
611
|
+
//#endregion
|
|
612
|
+
//#region src/listbox-item.tsx
|
|
613
|
+
/**
|
|
614
|
+
* Internal context for providing text-related slot attributes to child components.
|
|
615
|
+
* This context allows ListBoxItem to pass label and description attributes
|
|
616
|
+
* to nested components that need them for accessibility.
|
|
617
|
+
* @internal
|
|
618
|
+
*/
|
|
619
|
+
const TextContext = createContext({ slots: {} });
|
|
620
|
+
/**
|
|
621
|
+
* Enhanced ListBoxItem implementation with slots support.
|
|
622
|
+
* This wraps the core ListBoxItemImplComponent with Bento's slot system
|
|
623
|
+
* for advanced composition and styling capabilities.
|
|
624
|
+
* @internal
|
|
625
|
+
*/
|
|
626
|
+
const ListBoxItemImpl = withSlots("BentoListBoxItem", function ListBoxItemImplComponent(...restArgs) {
|
|
627
|
+
const [{ __node, ...props }, ref] = restArgs;
|
|
628
|
+
const state = useContext(ListStateContext);
|
|
629
|
+
const safeRef = useSafeObjectRef(ref);
|
|
630
|
+
const { optionProps, labelProps, descriptionProps, ...states } = useOption({
|
|
631
|
+
key: __node.key,
|
|
632
|
+
"aria-label": props["aria-label"],
|
|
633
|
+
isDisabled: props.isDisabled
|
|
634
|
+
}, state, safeRef);
|
|
635
|
+
const { hoverProps, isHovered } = useHover({
|
|
636
|
+
isDisabled: states.isDisabled,
|
|
637
|
+
onHoverStart: props.onHoverStart,
|
|
638
|
+
onHoverChange: props.onHoverChange,
|
|
639
|
+
onHoverEnd: props.onHoverEnd
|
|
640
|
+
});
|
|
641
|
+
const renderValues = {
|
|
642
|
+
...states,
|
|
643
|
+
isHovered,
|
|
644
|
+
selectionMode: state.selectionManager.selectionMode,
|
|
645
|
+
selectionBehavior: state.selectionManager.selectionBehavior
|
|
646
|
+
};
|
|
647
|
+
const content = typeof props.children === "function" ? props.children(renderValues) : props.children;
|
|
648
|
+
const { apply } = useProps(props, renderValues);
|
|
649
|
+
const dataAttributes = useDataAttributes({
|
|
650
|
+
selected: states.isSelected,
|
|
651
|
+
disabled: states.isDisabled,
|
|
652
|
+
hovered: isHovered,
|
|
653
|
+
focused: states.isFocused,
|
|
654
|
+
"focus-visible": states.isFocusVisible,
|
|
655
|
+
pressed: states.isPressed,
|
|
656
|
+
level: __node.level,
|
|
657
|
+
"selection-mode": state.selectionManager.selectionMode,
|
|
658
|
+
"selection-behavior": state.selectionManager.selectionBehavior
|
|
659
|
+
});
|
|
660
|
+
const textContext = useMemo(function createTextContext() {
|
|
661
|
+
return { slots: {
|
|
662
|
+
label: labelProps,
|
|
663
|
+
description: descriptionProps
|
|
664
|
+
} };
|
|
665
|
+
}, [labelProps, descriptionProps]);
|
|
666
|
+
const ElementType = __node.props.href ? "a" : "div";
|
|
667
|
+
const appliedUserProps = apply(__node.props, ["ref"]);
|
|
668
|
+
const finalAttributes = {
|
|
669
|
+
...mergeProps(optionProps, hoverProps),
|
|
670
|
+
...dataAttributes,
|
|
671
|
+
...appliedUserProps,
|
|
672
|
+
ref: safeRef,
|
|
673
|
+
"data-text-value": __node.textValue
|
|
674
|
+
};
|
|
675
|
+
return /* @__PURE__ */ jsx(TextContext.Provider, {
|
|
676
|
+
value: textContext,
|
|
677
|
+
children: React.createElement(ElementType, finalAttributes, content)
|
|
678
|
+
});
|
|
679
|
+
});
|
|
680
|
+
/**
|
|
681
|
+
* Adapter component that connects ListBoxItemImpl to React Aria's collection system.
|
|
682
|
+
* This function serves as a bridge between React Aria's createLeafComponent and
|
|
683
|
+
* the internal ListBoxItemImpl, ensuring proper prop forwarding and node injection.
|
|
684
|
+
*
|
|
685
|
+
* @template T - The type of the item value
|
|
686
|
+
* @param {ListBoxItemProps<T>} props - ListBoxItem component props
|
|
687
|
+
* @param {React.ForwardedRef<HTMLDivElement>} forwardedRef - Ref forwarded from the collection system
|
|
688
|
+
* @param {Node<T>} item - React Aria node containing item metadata and collection info
|
|
689
|
+
* @returns {React.ReactElement} The ListBoxItemImpl component with proper node and ref wiring
|
|
690
|
+
* @internal
|
|
691
|
+
*/
|
|
692
|
+
function ListBoxItemComponent(props, forwardedRef, item) {
|
|
693
|
+
return /* @__PURE__ */ jsx(ListBoxItemImpl, {
|
|
694
|
+
...props,
|
|
695
|
+
ref: forwardedRef,
|
|
696
|
+
__node: item
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* A single item within a ListBox component.
|
|
701
|
+
* Handles user interactions, accessibility, and state management for individual options.
|
|
702
|
+
*
|
|
703
|
+
* @component
|
|
704
|
+
* @template T The type of the item value
|
|
705
|
+
* @example
|
|
706
|
+
* ```tsx
|
|
707
|
+
* <ListBoxItem>Simple option</ListBoxItem>
|
|
708
|
+
* ```
|
|
709
|
+
* @public
|
|
710
|
+
*/
|
|
711
|
+
const ListBoxItem = createLeafComponent("item", ListBoxItemComponent);
|
|
712
|
+
//#endregion
|
|
713
|
+
export { AriaCollection as Collection, Header, HeaderContext, ListBox, ListBoxItem, ListBoxSection };
|
|
714
|
+
|
|
715
|
+
//# sourceMappingURL=index.mjs.map
|