@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/LICENSE ADDED
@@ -0,0 +1,9 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 GoDaddy Operating Company, LLC.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.mdx ADDED
@@ -0,0 +1,342 @@
1
+ import { Meta, ArgTypes, Story, Controls, Source } from '@storybook/addon-docs/blocks';
2
+ import * as Stories from './listbox.stories.tsx';
3
+
4
+ import BasicExample from './examples/basic-listbox.tsx?raw';
5
+ import SectionsExample from './examples/sections.tsx?raw';
6
+ import DynamicExample from './examples/dynamic-collection.tsx?raw';
7
+ import SlotsExample from './examples/slots-dynamic-sections.tsx?raw';
8
+
9
+ <Meta of={Stories} name="Overview" />
10
+
11
+ # ListBox
12
+
13
+ The `@bento/listbox` package provides a flexible, accessible listbox primitive that supports both controlled and uncontrolled selection modes. Built on React Aria for interaction fidelity and designed for composition within higher-level components like Select, Combobox, and Menu.
14
+
15
+ ## Installation
16
+
17
+ ```shell
18
+ npm install --save @bento/listbox
19
+ ```
20
+
21
+ ## Component Structure
22
+
23
+ The `@bento/listbox` package exports five main components:
24
+
25
+ - **ListBox**: The main container component that manages selection state, keyboard navigation, and accessibility
26
+ - **ListBoxItem**: Individual selectable items within the listbox
27
+ - **ListBoxSection**: Optional grouping component for organizing options into sections
28
+ - **Header**: Accessible heading for a `ListBoxSection`, forwards props and refs for full styling control
29
+ - **Collection**: Utility for rendering nested dynamic data inside a `ListBoxSection` (or other collection-aware context).
30
+
31
+ ## Props
32
+
33
+ The following properties are available on the `ListBox` component:
34
+
35
+ <ArgTypes of={Stories.Props} />
36
+
37
+ ## Static Items
38
+
39
+ <Source language='tsx' code={BasicExample} />
40
+
41
+ ## Sections
42
+
43
+ Use `ListBoxSection` to group related options. Use `Header` inside a `ListBoxSection` to render an accessible heading for the group. It automatically links the heading to the section via `aria-labelledby`.
44
+
45
+ The `<Header>` component accepts standard DOM props and a `slot` prop for Bento’s slot system, enabling fine-grained overrides in composite components.
46
+
47
+ <Source language='tsx' code={SectionsExample} />
48
+
49
+ ## Dynamic Collections
50
+
51
+ For dynamic data, use the `items` prop with a render function. The ListBox component follows different patterns depending on how it's used:
52
+
53
+ ### When `items` prop is provided
54
+ When you provide an `items` prop, the `children` function receives **individual items** for React Aria compatibility:
55
+
56
+ <Source language='tsx' code={DynamicExample} />
57
+
58
+ In this pattern, `children` is called for each item in the `items` array, receiving the individual item data to render `ListBoxItem` components.
59
+
60
+ ### When no `items` prop is provided
61
+ When you don't provide an `items` prop but use `children` as a function, it follows **Bento's render prop pattern** and receives an object with render props:
62
+
63
+ ```tsx
64
+ <ListBox aria-label="Custom ListBox">
65
+ {({ isEmpty, isFocused, state, items }) => (
66
+ isEmpty ? (
67
+ <div>No items available</div>
68
+ ) : (
69
+ // Render items normally using static children or other logic
70
+ <ListBoxItem>Static Item</ListBoxItem>
71
+ )
72
+ )}
73
+ </ListBox>
74
+ ```
75
+
76
+ This pattern provides access to the ListBox's state, focus status, and other render props following Bento's consistent render prop API.
77
+
78
+ ### Nested Collections with Sections
79
+ You can also render nested data inside a section using the exported `Collection` component:
80
+
81
+ <Source language='tsx' code={DynamicExample} />
82
+
83
+ ## Customization
84
+
85
+ The ListBox components provide extensive customization options through data attributes, slots, render props, and standard CSS styling. This section covers all available approaches to customize the appearance and behavior.
86
+
87
+ ### Styling with Data Attributes
88
+
89
+ All ListBox components expose their internal state through `data-` attributes, enabling CSS-based styling without JavaScript. This follows Bento's design philosophy of returning styling control to CSS.
90
+
91
+ #### ListBox Container Attributes
92
+
93
+ The main `ListBox` component exposes these data attributes:
94
+
95
+ - `[data-empty]` - Applied when the listbox contains no items
96
+ - `[data-focused]` - Applied when the listbox is focused
97
+ - `[data-focus-visible]` - Applied when the listbox has keyboard focus
98
+ - `[data-layout="stack"]` - The layout type (currently only "stack" supported)
99
+ - `[data-orientation="vertical|horizontal"]` - The primary orientation
100
+ - `[data-selection-mode="none|single|multiple"]` - The selection mode
101
+ - `[data-selection-behavior="toggle|replace"]` - How selection behaves
102
+ - `[data-allows-tab-navigation]` - Whether tab navigation is enabled
103
+ - `[data-focus-wrap]` - Whether focus wraps around the collection
104
+
105
+ #### ListBoxItem Attributes
106
+
107
+ Individual `ListBoxItem` components expose these data attributes:
108
+
109
+ - `[data-selected]` - Applied when the item is selected
110
+ - `[data-disabled]` - Applied when the item is disabled
111
+ - `[data-hovered]` - Applied when the item is being hovered
112
+ - `[data-focused]` - Applied when the item is focused
113
+ - `[data-focus-visible]` - Applied when the item has keyboard focus
114
+ - `[data-pressed]` - Applied when the item is being pressed
115
+ - `[data-level]` - The nesting level (useful for indentation)
116
+ - `[data-selection-mode]` - Inherited selection mode
117
+ - `[data-selection-behavior]` - Inherited selection behavior
118
+ - `[data-text-value]` - The computed text value for the item
119
+
120
+ #### ListBoxSection Attributes
121
+
122
+ The `ListBoxSection` component exposes:
123
+
124
+ - `[data-level]` - The nesting level of the section
125
+
126
+ #### CSS Styling Examples
127
+
128
+ ```css
129
+ /* Basic item styling */
130
+ [role="option"] {
131
+ padding: 8px 12px;
132
+ border-radius: 6px;
133
+ transition: all 0.15s ease-in-out;
134
+ cursor: pointer;
135
+ }
136
+
137
+ /* Selected items */
138
+ [role="option"][data-selected] {
139
+ background: Highlight;
140
+ color: HighlightText;
141
+ }
142
+
143
+ /* Hovered items */
144
+ [role="option"][data-hovered] {
145
+ background: color-mix(in srgb, Highlight 10%, transparent);
146
+ }
147
+
148
+ /* Disabled items */
149
+ [role="option"][data-disabled] {
150
+ opacity: 0.6;
151
+ cursor: not-allowed;
152
+ }
153
+
154
+ /* Focused items (keyboard navigation) */
155
+ [role="option"][data-focus-visible] {
156
+ outline: 2px solid Highlight;
157
+ outline-offset: -2px;
158
+ }
159
+
160
+ /* Combined states */
161
+ [role="option"][data-selected][data-hovered] {
162
+ background: color-mix(in srgb, Highlight 90%, white);
163
+ }
164
+
165
+ /* Empty state styling */
166
+ .listbox[data-empty] {
167
+ min-height: 100px;
168
+ display: flex;
169
+ align-items: center;
170
+ justify-content: center;
171
+ color: #64748b;
172
+ }
173
+
174
+ /* Section headers */
175
+ .section-header {
176
+ font-weight: 600;
177
+ font-size: 0.875rem;
178
+ color: #64748b;
179
+ padding: 6px 12px;
180
+ margin-bottom: 4px;
181
+ }
182
+ ```
183
+
184
+ ### Slots System
185
+
186
+ The components use Bento's `@bento/slots` package for fine-grained component overrides. Slots allow you to replace or wrap specific parts of the component tree with custom implementations.
187
+
188
+ #### Basic Slot Usage
189
+
190
+ <Source language='tsx' code={SlotsExample} />
191
+
192
+ #### Advanced Slot Patterns
193
+
194
+ You can target specific items or sections using hierarchical slot names:
195
+
196
+ ```tsx
197
+ // Override a specific section header
198
+ const slots = {
199
+ 'my-listbox.fruits.header': ({ original, props }) => (
200
+ <Header {...props}>🍎 {original}</Header>
201
+ ),
202
+
203
+ // Override a specific item in a specific section
204
+ 'my-listbox.fruits.apple': ({ original }) => (
205
+ <div className="special-item">⭐ {original}</div>
206
+ ),
207
+
208
+ // Override an entire section
209
+ 'my-listbox.vegetables': ({ original }) => (
210
+ <div className="veggie-section">{original}</div>
211
+ )
212
+ };
213
+
214
+ <ListBox slot="my-listbox" slots={slots}>
215
+ <ListBoxSection slot="fruits">
216
+ <Header slot="header">Fruits</Header>
217
+ <ListBoxItem slot="apple">Apple</ListBoxItem>
218
+ <ListBoxItem slot="orange">Orange</ListBoxItem>
219
+ </ListBoxSection>
220
+ <ListBoxSection slot="vegetables">
221
+ <Header slot="header">Vegetables</Header>
222
+ <ListBoxItem slot="carrot">Carrot</ListBoxItem>
223
+ </ListBoxSection>
224
+ </ListBox>
225
+ ```
226
+
227
+ ### Render Props
228
+
229
+ The `ListBoxItem` component supports render prop patterns for dynamic content based on interaction state:
230
+
231
+ ```tsx
232
+ // ListBoxItem render prop with interaction state
233
+ <ListBoxItem>
234
+ {({ isSelected, isHovered, isDisabled }) => (
235
+ <div className={`item ${isSelected ? 'selected' : ''}`}>
236
+ {isHovered && '👆 '}
237
+ My Item
238
+ {isSelected && ' ✓'}
239
+ </div>
240
+ )}
241
+ </ListBoxItem>
242
+ ```
243
+
244
+ For ListBox-level render props, use the `renderEmptyState` prop to customize empty state display:
245
+
246
+ ```tsx
247
+ <ListBox
248
+ items={items}
249
+ renderEmptyState={({ isEmpty, isFocused, state, items }) => (
250
+ <div className="empty-state">
251
+ {isFocused ? 'No items found (focused)' : 'No items available'}
252
+ </div>
253
+ )}
254
+ >
255
+ {(item: any) => (
256
+ <ListBoxItem key={item.id} textValue={item.name}>
257
+ {item.name}
258
+ </ListBoxItem>
259
+ )}
260
+ </ListBox>
261
+ ```
262
+
263
+ ### Empty State Customization
264
+
265
+ Customize the appearance when no items are present:
266
+
267
+ ```tsx
268
+ <ListBox
269
+ renderEmptyState={({ isEmpty, isFocused }) => (
270
+ <div className="empty-state">
271
+ <span>📭</span>
272
+ <p>No items to display</p>
273
+ {isFocused && <p>Start typing to search...</p>}
274
+ </div>
275
+ )}
276
+ >
277
+ {/* Items when present */}
278
+ </ListBox>
279
+ ```
280
+
281
+ ### Accessibility Customization
282
+
283
+ All components support standard ARIA attributes for enhanced accessibility:
284
+
285
+ ```tsx
286
+ <ListBox
287
+ aria-label="Food menu"
288
+ aria-describedby="menu-description"
289
+ >
290
+ <ListBoxSection aria-label="Main courses">
291
+ <Header>Main Dishes</Header>
292
+ <ListBoxItem aria-label="Chicken teriyaki with rice">
293
+ Chicken Teriyaki
294
+ </ListBoxItem>
295
+ </ListBoxSection>
296
+ </ListBox>
297
+ ```
298
+
299
+ ### CSS-in-JS and Styled Components
300
+
301
+ The data attributes work seamlessly with CSS-in-JS libraries:
302
+
303
+ ```tsx
304
+ // Styled Components
305
+ const StyledListBox = styled.div`
306
+ &[data-focused] {
307
+ box-shadow: 0 0 0 2px blue;
308
+ }
309
+
310
+ [role="option"][data-selected] {
311
+ background: ${props => props.theme.primary};
312
+ }
313
+ `;
314
+
315
+ // Emotion
316
+ const listboxStyles = css`
317
+ &[data-empty] {
318
+ opacity: 0.5;
319
+ }
320
+ `;
321
+ ```
322
+
323
+ ### Animation and Transitions
324
+
325
+ Data attributes enable smooth state transitions:
326
+
327
+ ```css
328
+ [role="option"] {
329
+ transition: all 0.2s ease-in-out;
330
+ transform: translateY(0);
331
+ }
332
+
333
+ [role="option"][data-hovered] {
334
+ transform: translateY(-2px);
335
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
336
+ }
337
+
338
+ [role="option"][data-pressed] {
339
+ transform: translateY(0);
340
+ transition-duration: 0.1s;
341
+ }
342
+ ```