@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.
package/src/header.tsx ADDED
@@ -0,0 +1,132 @@
1
+ import React, { forwardRef, createContext, useContext } from 'react';
2
+ import { createLeafComponent } from '@react-aria/collections';
3
+ import { useProps } from '@bento/use-props';
4
+ import { withSlots, type Slots } from '@bento/slots';
5
+
6
+ /**
7
+ * Props for the Header component.
8
+ * @interface HeaderProps
9
+ */
10
+ export interface HeaderProps extends Slots, React.ComponentProps<'header'> {
11
+ /**
12
+ * The children of the header.
13
+ */
14
+ readonly children?: React.ReactNode;
15
+ }
16
+
17
+ /**
18
+ * Context value structure for Header components.
19
+ * Extends HTML attributes to support all standard header element properties.
20
+ * @interface HeaderContextValue
21
+ */
22
+ interface HeaderContextValue extends React.HTMLAttributes<HTMLElement> {
23
+ /**
24
+ * Reference to the header element for forwarding.
25
+ */
26
+ readonly ref?: React.RefObject<HTMLDivElement>;
27
+ }
28
+
29
+ /**
30
+ * Combined props type for the internal BentoHeader implementation.
31
+ * Merges Header-specific props with standard HTML attributes to provide
32
+ * a comprehensive interface for the internal header component.
33
+ *
34
+ * @type BentoHeaderProps
35
+ * @internal
36
+ */
37
+ type BentoHeaderProps = HeaderProps & React.HTMLAttributes<HTMLElement>;
38
+
39
+ /**
40
+ * React context for providing header-related attributes and refs to Header components.
41
+ * Used internally by ListBoxSection to pass heading props to Header elements.
42
+ * @public
43
+ */
44
+ export const HeaderContext = createContext<HeaderContextValue>({});
45
+
46
+ /**
47
+ * Internal implementation of the BentoHeader component with slots support.
48
+ * This component handles prop processing and context integration.
49
+ * It merges props from useProps and HeaderContext while preserving styling props.
50
+ *
51
+ * @internal
52
+ */
53
+ const BentoHeaderImpl = withSlots(
54
+ 'BentoHeader',
55
+ forwardRef(function BentoHeader(props: BentoHeaderProps, ref: React.ForwardedRef<HTMLElement>) {
56
+ const { props: processedProps, apply } = useProps(props);
57
+ const contextProps = useContext(HeaderContext);
58
+
59
+ // Apply user props directly (preserves className, style, etc.)
60
+ const appliedUserProps = apply(processedProps);
61
+
62
+ const composed = {
63
+ ...contextProps,
64
+ ...appliedUserProps // User props take precedence over context
65
+ };
66
+
67
+ return (
68
+ <header {...composed} ref={contextProps.ref || ref}>
69
+ {processedProps.children}
70
+ </header>
71
+ );
72
+ })
73
+ );
74
+
75
+ /**
76
+ * Wrapper component that connects the BentoHeaderImpl to React Aria's collection system.
77
+ * This function serves as an adapter between the createLeafComponent system and
78
+ * the internal BentoHeaderImpl component, ensuring proper prop forwarding and ref handling.
79
+ *
80
+ * @param {HeaderProps} props - Header component props
81
+ * @param {React.ReactNode} [props.children] - React children to render inside the header
82
+ * @param {React.ForwardedRef<HTMLElement>} ref - Forwarded ref to the header element
83
+ * @returns {React.ReactElement} The BentoHeaderImpl component with forwarded props and ref
84
+ * @internal
85
+ */
86
+ function HeaderWrapper(props: HeaderProps, ref: React.ForwardedRef<HTMLElement>) {
87
+ return <BentoHeaderImpl {...props} ref={ref} />;
88
+ }
89
+
90
+ /**
91
+ * A Header represents a heading for a section within a ListBox.
92
+ * Uses React Aria's createLeafComponent for automatic collection handling.
93
+ *
94
+ * @component
95
+ * @param {HeaderProps} props - The props for the Header component
96
+ * @param {React.ForwardedRef<HTMLElement>} ref - Forwarded ref to the header element
97
+ * @returns {JSX.Element} A header element with proper accessibility attributes
98
+ *
99
+ * @example
100
+ * ```tsx
101
+ * <Header>My Section Title</Header>
102
+ * ```
103
+ * @public
104
+ */
105
+ /**
106
+ * Base Header component created through React Aria's collection system.
107
+ * This handles the connection to the parent ListBox's collection state and
108
+ * integrates with the collection rendering system.
109
+ * @internal
110
+ */
111
+ const HeaderBase = createLeafComponent('header', HeaderWrapper);
112
+
113
+ /**
114
+ * A Header component for section headings within a ListBox.
115
+ * Provides semantic header structure with proper accessibility attributes
116
+ * and integrates with React Aria's collection system for automatic handling.
117
+ *
118
+ * This is the main public interface for creating headers in ListBox sections.
119
+ * It automatically receives heading props from the parent ListBoxSection via HeaderContext.
120
+ *
121
+ * @component
122
+ * @example
123
+ * ```tsx
124
+ * <ListBoxSection>
125
+ * <Header>Fruits</Header>
126
+ * <ListBoxItem>Apple</ListBoxItem>
127
+ * <ListBoxItem>Banana</ListBoxItem>
128
+ * </ListBoxSection>
129
+ * ```
130
+ * @public
131
+ */
132
+ export const Header = HeaderBase as React.ForwardRefExoticComponent<HeaderProps & React.RefAttributes<HTMLElement>>;
package/src/index.tsx ADDED
@@ -0,0 +1,9 @@
1
+ export { Header, HeaderContext } from './header';
2
+ export { ListBoxItem } from './listbox-item';
3
+ export { ListBox, Collection } from './listbox';
4
+ export { ListBoxSection } from './listbox-section';
5
+
6
+ export type { HeaderProps } from './header';
7
+ export type { ListBoxSectionProps } from './listbox-section';
8
+ export type { ListBoxProps, ListBoxRenderProps } from './listbox';
9
+ export type { ListBoxItemProps, ListBoxItemRenderProps } from './listbox-item';
@@ -0,0 +1,255 @@
1
+ import React, { ForwardedRef, ReactNode, createContext, useContext, useMemo, forwardRef } from 'react';
2
+ import { mergeProps, useOption, useHover } from 'react-aria';
3
+ import { createLeafComponent } from '@react-aria/collections';
4
+ import { HoverEvents, Key, LinkDOMProps, Node } from '@react-types/shared';
5
+ import { useDataAttributes } from '@bento/use-data-attributes';
6
+ import { useProps } from '@bento/use-props';
7
+ import { ListStateContext } from './listbox';
8
+ import { useSafeObjectRef } from './utils';
9
+ import { withSlots } from '@bento/slots';
10
+
11
+ /**
12
+ * Context value structure for text-related slot attributes.
13
+ * Used to provide label and description attributes to child components.
14
+ * @interface TextContextValue
15
+ */
16
+ interface TextContextValue {
17
+ readonly slots: {
18
+ /** Attributes for label elements */
19
+ readonly label?: React.HTMLAttributes<HTMLElement>;
20
+ /** Attributes for description elements */
21
+ readonly description?: React.HTMLAttributes<HTMLElement>;
22
+ };
23
+ }
24
+
25
+ /**
26
+ * Internal context for providing text-related slot attributes to child components.
27
+ * This context allows ListBoxItem to pass label and description attributes
28
+ * to nested components that need them for accessibility.
29
+ * @internal
30
+ */
31
+ const TextContext = createContext<TextContextValue>({ slots: {} });
32
+
33
+ /**
34
+ * Render props provided to ListBoxItem render functions.
35
+ * @interface ListBoxItemRenderProps
36
+ */
37
+ export interface ListBoxItemRenderProps {
38
+ /**
39
+ * Whether the item is currently hovered.
40
+ * @selector [data-hovered]
41
+ */
42
+ readonly isHovered: boolean;
43
+ /**
44
+ * Whether the item is currently pressed.
45
+ * @selector [data-pressed]
46
+ */
47
+ readonly isPressed: boolean;
48
+ /**
49
+ * Whether the item is currently selected.
50
+ * @selector [data-selected]
51
+ */
52
+ readonly isSelected: boolean;
53
+ /**
54
+ * Whether the item is currently focused.
55
+ * @selector [data-focused]
56
+ */
57
+ readonly isFocused: boolean;
58
+ /**
59
+ * Whether the item is currently keyboard focused.
60
+ * @selector [data-focus-visible]
61
+ */
62
+ readonly isFocusVisible: boolean;
63
+ /**
64
+ * Whether the item is disabled.
65
+ * @selector [data-disabled]
66
+ */
67
+ readonly isDisabled: boolean;
68
+ /**
69
+ * The type of selection that is allowed in the collection.
70
+ * @selector [data-selection-mode="none | single | multiple"]
71
+ */
72
+ readonly selectionMode: 'none' | 'single' | 'multiple';
73
+ /**
74
+ * The selection behavior for the collection.
75
+ * @selector [data-selection-behavior="toggle | replace"]
76
+ */
77
+ readonly selectionBehavior: 'toggle' | 'replace';
78
+ }
79
+
80
+ /**
81
+ * Props for the ListBoxItem component.
82
+ * @interface ListBoxItemProps
83
+ * @template T The type of the item value
84
+ */
85
+ export interface ListBoxItemProps<T = object>
86
+ extends LinkDOMProps,
87
+ HoverEvents,
88
+ Omit<React.HTMLAttributes<HTMLElement>, keyof LinkDOMProps | keyof HoverEvents | 'id' | 'children'> {
89
+ /** The unique id of the item. If not provided, React Aria will auto-generate one. */
90
+ readonly id?: Key;
91
+ /** The object value that this item represents. When using dynamic collections, this is set automatically. */
92
+ readonly value?: T;
93
+ /** A string representation of the item's contents, used for features like typeahead. If not provided, React Aria will derive it from children automatically. */
94
+ readonly textValue?: string;
95
+ /**
96
+ * Handler that is called when a user performs an action on the item. The exact user event depends on the
97
+ * collection's `selectionBehavior` prop and the interaction modality.
98
+ */
99
+ readonly onAction?: () => void;
100
+ /** The contents of the item. Can be a render function that receives render props. */
101
+ readonly children?: ReactNode | ((values: ListBoxItemRenderProps) => ReactNode);
102
+ /**
103
+ * A slot name for the component. Used by Bento's slot system.
104
+ */
105
+ readonly slot?: string;
106
+ /** Whether the item is disabled. */
107
+ readonly isDisabled?: boolean;
108
+ }
109
+
110
+ /**
111
+ * Internal implementation component for ListBoxItem.
112
+ * Handles the core logic for rendering a single listbox item with proper accessibility.
113
+ * This component manages all the hooks, state, and interactions needed for a functional
114
+ * listbox item including selection, hover, focus, and keyboard interactions.
115
+ *
116
+ * @template T - The type of the item value
117
+ * @param {object} props - Combined ListBoxItem props and internal node data
118
+ * @param {Node<T>} props.__node - Internal React Aria node containing item metadata
119
+ * @param {React.ReactNode | ((values: ListBoxItemRenderProps) => React.ReactNode)} [props.children] - Content to render, can be static or a render function
120
+ * @param {boolean} [props.isDisabled] - Whether the item is disabled
121
+ * @param {string} [props.aria-label] - ARIA label for accessibility
122
+ * @param {(e: HoverEvent) => void} [props.onHoverStart] - Handler for hover start events
123
+ * @param {(isHovering: boolean) => void} [props.onHoverChange] - Handler for hover change events
124
+ * @param {(e: HoverEvent) => void} [props.onHoverEnd] - Handler for hover end events
125
+ * @param {React.ForwardedRef<HTMLDivElement>} ref - Forwarded ref to the item element
126
+ * @returns {React.ReactElement} A fully interactive listbox item with accessibility and state management
127
+ * @internal
128
+ */
129
+ const ListBoxItemImplComponent = function ListBoxItemImplComponent<T extends object>(
130
+ { __node, ...props }: ListBoxItemProps<T> & { readonly __node: Node<T> },
131
+ ref: ForwardedRef<HTMLDivElement>
132
+ ) {
133
+ const state = useContext(ListStateContext)!;
134
+ const safeRef = useSafeObjectRef(ref);
135
+
136
+ const { optionProps, labelProps, descriptionProps, ...states } = useOption(
137
+ {
138
+ key: __node.key,
139
+ 'aria-label': props['aria-label'],
140
+ isDisabled: props.isDisabled
141
+ },
142
+ state,
143
+ safeRef
144
+ );
145
+
146
+ const { hoverProps, isHovered } = useHover({
147
+ isDisabled: states.isDisabled,
148
+ onHoverStart: props.onHoverStart,
149
+ onHoverChange: props.onHoverChange,
150
+ onHoverEnd: props.onHoverEnd
151
+ });
152
+
153
+ const renderValues: ListBoxItemRenderProps = {
154
+ ...states,
155
+ isHovered,
156
+ selectionMode: state.selectionManager.selectionMode,
157
+ selectionBehavior: state.selectionManager.selectionBehavior
158
+ };
159
+
160
+ const content = typeof props.children === 'function' ? props.children(renderValues) : props.children;
161
+
162
+ const { apply } = useProps(props, renderValues);
163
+
164
+ const dataAttributes = useDataAttributes({
165
+ selected: states.isSelected,
166
+ disabled: states.isDisabled,
167
+ hovered: isHovered,
168
+ focused: states.isFocused,
169
+ 'focus-visible': states.isFocusVisible,
170
+ pressed: states.isPressed,
171
+ level: __node.level,
172
+ 'selection-mode': state.selectionManager.selectionMode,
173
+ 'selection-behavior': state.selectionManager.selectionBehavior
174
+ });
175
+
176
+ const textContext = useMemo(
177
+ function createTextContext() {
178
+ return {
179
+ slots: {
180
+ label: labelProps,
181
+ description: descriptionProps
182
+ }
183
+ };
184
+ },
185
+ [labelProps, descriptionProps]
186
+ );
187
+
188
+ const ElementType = __node.props.href ? 'a' : 'div';
189
+
190
+ // Use original node props (which contain className) not filtered finalProps
191
+ const appliedUserProps = apply(__node.props, ['ref']);
192
+
193
+ const finalAttributes = {
194
+ ...mergeProps(optionProps, hoverProps), // React Aria props
195
+ ...dataAttributes, // Bento data attributes
196
+ ...appliedUserProps,
197
+ ref: safeRef,
198
+ 'data-text-value': __node.textValue
199
+ };
200
+
201
+ return (
202
+ <TextContext.Provider value={textContext}>
203
+ {React.createElement(ElementType, finalAttributes, content)}
204
+ </TextContext.Provider>
205
+ );
206
+ };
207
+
208
+ /**
209
+ * Enhanced ListBoxItem implementation with slots support.
210
+ * This wraps the core ListBoxItemImplComponent with Bento's slot system
211
+ * for advanced composition and styling capabilities.
212
+ * @internal
213
+ */
214
+ export const ListBoxItemImpl = withSlots('BentoListBoxItem', forwardRef(ListBoxItemImplComponent));
215
+
216
+ /**
217
+ * Adapter component that connects ListBoxItemImpl to React Aria's collection system.
218
+ * This function serves as a bridge between React Aria's createLeafComponent and
219
+ * the internal ListBoxItemImpl, ensuring proper prop forwarding and node injection.
220
+ *
221
+ * @template T - The type of the item value
222
+ * @param {ListBoxItemProps<T>} props - ListBoxItem component props
223
+ * @param {React.ForwardedRef<HTMLDivElement>} forwardedRef - Ref forwarded from the collection system
224
+ * @param {Node<T>} item - React Aria node containing item metadata and collection info
225
+ * @returns {React.ReactElement} The ListBoxItemImpl component with proper node and ref wiring
226
+ * @internal
227
+ */
228
+ function ListBoxItemComponent<T extends object>(
229
+ props: ListBoxItemProps<T>,
230
+ forwardedRef: ForwardedRef<HTMLDivElement>,
231
+ item: Node<T>
232
+ ) {
233
+ return <ListBoxItemImpl {...props} ref={forwardedRef} __node={item} />;
234
+ }
235
+
236
+ /**
237
+ * Base ListBoxItem component created through React Aria's collection system.
238
+ * This handles the connection to the parent ListBox's collection state.
239
+ * @internal
240
+ */
241
+ const ListBoxItemBase = createLeafComponent('item', ListBoxItemComponent);
242
+
243
+ /**
244
+ * A single item within a ListBox component.
245
+ * Handles user interactions, accessibility, and state management for individual options.
246
+ *
247
+ * @component
248
+ * @template T The type of the item value
249
+ * @example
250
+ * ```tsx
251
+ * <ListBoxItem>Simple option</ListBoxItem>
252
+ * ```
253
+ * @public
254
+ */
255
+ export const ListBoxItem = ListBoxItemBase;
@@ -0,0 +1,171 @@
1
+ import React, { forwardRef, useRef, useContext } from 'react';
2
+ import { useListBoxSection, mergeProps } from 'react-aria';
3
+ import { createBranchComponent } from '@react-aria/collections';
4
+ import { CollectionRendererContext } from 'react-aria-components';
5
+ import type { Node } from '@react-types/shared';
6
+ import { useDataAttributes } from '@bento/use-data-attributes';
7
+ import { useProps } from '@bento/use-props';
8
+ import { withSlots } from '@bento/slots';
9
+ import { HeaderContext } from './header';
10
+ import { ListStateContext } from './listbox';
11
+
12
+ /**
13
+ * Props for the ListBoxSection component.
14
+ * @interface ListBoxSectionProps
15
+ */
16
+ export interface ListBoxSectionProps extends Omit<React.ComponentProps<'section'>, 'title'> {
17
+ /**
18
+ * A slot name for the component. Used by Bento's slot system.
19
+ */
20
+ readonly slot?: string;
21
+ /**
22
+ * The title of the section.
23
+ */
24
+ readonly title?: React.ReactNode;
25
+ /**
26
+ * The children of the section.
27
+ */
28
+ readonly children?: React.ReactNode;
29
+ }
30
+
31
+ /**
32
+ * Internal props interface for BentoListBoxSectionImpl component.
33
+ * Extends ListBoxSectionProps with internal React Aria node data and allows
34
+ * additional properties for flexibility in prop handling.
35
+ *
36
+ * @interface BentoListBoxSectionImplProps
37
+ * @template T - The type of the section node data
38
+ * @internal
39
+ */
40
+ interface BentoListBoxSectionImplProps<T = unknown> extends ListBoxSectionProps {
41
+ readonly __node?: Node<T>;
42
+ readonly [key: string]: unknown;
43
+ }
44
+
45
+ /**
46
+ * Props interface for the ListBoxSectionInner component.
47
+ * Contains the React Aria section node that represents this section
48
+ * in the collection hierarchy for dynamic rendering.
49
+ *
50
+ * @interface ListBoxSectionInnerProps
51
+ * @internal
52
+ */
53
+ interface ListBoxSectionInnerProps {
54
+ readonly section: Node<unknown>;
55
+ }
56
+
57
+ /**
58
+ * Internal implementation of the BentoListBoxSection component with slots support.
59
+ * This component handles the core logic for rendering a section within a ListBox,
60
+ * including title rendering, accessibility attributes, and child content management.
61
+ * It integrates with React Aria's useListBoxSection hook for proper ARIA compliance.
62
+ *
63
+ * @internal
64
+ */
65
+ const BentoListBoxSectionImpl = withSlots(
66
+ 'BentoListBoxSection',
67
+ forwardRef(function BentoListBoxSectionImpl<T>(
68
+ { __node, children, title: titleProp, ...rest }: BentoListBoxSectionImplProps<T>,
69
+ ref: React.ForwardedRef<HTMLElement>
70
+ ) {
71
+ const { props, apply } = useProps(rest);
72
+ const data = useDataAttributes({ level: __node?.level });
73
+ const headingRef = useRef<HTMLDivElement>(null);
74
+
75
+ const title = titleProp ?? props.title ?? __node?.rendered;
76
+ const { groupProps, headingProps } = useListBoxSection({
77
+ heading: title,
78
+ 'aria-label': props['aria-label']
79
+ });
80
+
81
+ const composed = mergeProps(apply({ ...data, ...props }, ['children', 'title', 'slot']), groupProps);
82
+
83
+ const sectionContent = children || props.children;
84
+
85
+ return (
86
+ <section {...composed} ref={ref}>
87
+ <HeaderContext.Provider value={{ ...headingProps, ref: headingRef }}>
88
+ {title && <div {...headingProps}>{title}</div>}
89
+ {sectionContent}
90
+ </HeaderContext.Provider>
91
+ </section>
92
+ );
93
+ })
94
+ );
95
+
96
+ /**
97
+ * Wrapper component that connects BentoListBoxSectionImpl to React Aria's collection system.
98
+ * This function serves as an adapter between createBranchComponent and the internal
99
+ * BentoListBoxSectionImpl, ensuring proper prop forwarding and node injection for sections.
100
+ *
101
+ * @template T - The type of the section node data
102
+ * @param {ListBoxSectionProps} props - ListBoxSection component props
103
+ * @param {string} [props.slot] - Slot name for Bento's slot system
104
+ * @param {React.ReactNode} [props.title] - Title for the section
105
+ * @param {React.ReactNode} [props.children] - Children to render in the section
106
+ * @param {string} [props.aria-label] - ARIA label for accessibility
107
+ * @param {React.ForwardedRef<HTMLElement>} ref - Ref forwarded from the collection system
108
+ * @param {Node<T>} section - React Aria node containing section metadata and collection info
109
+ * @returns {React.ReactElement} The BentoListBoxSectionImpl component with proper node and ref wiring
110
+ * @internal
111
+ */
112
+ /* v8 ignore start */
113
+ function ListBoxSectionWrapper<T extends object>(
114
+ props: ListBoxSectionProps,
115
+ ref: React.ForwardedRef<HTMLElement>,
116
+ section: Node<T>
117
+ ) {
118
+ return <BentoListBoxSectionImpl {...props} __node={section} ref={ref} />;
119
+ }
120
+ /* v8 ignore stop */
121
+
122
+ /**
123
+ * Base ListBoxSection component created through React Aria's collection system.
124
+ * This handles the connection to the parent ListBox's collection state and
125
+ * manages the branch structure for nested items.
126
+ * @internal
127
+ */
128
+ const ListBoxSectionBase = createBranchComponent('section', ListBoxSectionWrapper);
129
+
130
+ /**
131
+ * Internal component for rendering dynamic collection sections.
132
+ * This component is used specifically for sections that are part of a dynamic collection,
133
+ * connecting to the ListStateContext and CollectionRendererContext to properly render
134
+ * nested items through React Aria's collection system.
135
+ *
136
+ * @component
137
+ * @param {object} props - The component props containing the section node
138
+ * @param {Node<unknown>} props.section - The React Aria node representing this section in the collection
139
+ * @throws {BentoError} Throws an error if used outside of a ListBox context
140
+ * @returns {React.ReactElement} JSX element representing a dynamically rendered listbox section
141
+ * @internal
142
+ */
143
+ export const ListBoxSectionInner: React.FC<ListBoxSectionInnerProps> = function ListBoxSectionInner({ section }) {
144
+ const state = useContext(ListStateContext);
145
+ const { CollectionBranch } = useContext(CollectionRendererContext);
146
+
147
+ return (
148
+ <BentoListBoxSectionImpl {...section.props} __node={section}>
149
+ {CollectionBranch && state?.collection ? (
150
+ <CollectionBranch collection={state.collection} parent={section} />
151
+ ) : null}
152
+ </BentoListBoxSectionImpl>
153
+ );
154
+ };
155
+
156
+ /**
157
+ * A section component for organizing related items within a ListBox.
158
+ *
159
+ * @component
160
+ * @example
161
+ * ```tsx
162
+ * <ListBoxSection title="Fruits">
163
+ * <ListBoxItem>Apple</ListBoxItem>
164
+ * <ListBoxItem>Banana</ListBoxItem>
165
+ * </ListBoxSection>
166
+ * ```
167
+ * @public
168
+ */
169
+ export const ListBoxSection = ListBoxSectionBase as <_T extends object>(
170
+ props: ListBoxSectionProps & { children?: React.ReactNode }
171
+ ) => React.ReactElement;