@bento/listbox 0.1.2 β 0.1.3
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 +532 -0
- package/package.json +5 -4
package/README.md
ADDED
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
# ListBox
|
|
2
|
+
|
|
3
|
+
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.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```shell
|
|
8
|
+
npm install --save @bento/listbox
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Component Structure
|
|
12
|
+
|
|
13
|
+
The `@bento/listbox` package exports five main components:
|
|
14
|
+
|
|
15
|
+
- **ListBox**: The main container component that manages selection state, keyboard navigation, and accessibility
|
|
16
|
+
- **ListBoxItem**: Individual selectable items within the listbox
|
|
17
|
+
- **ListBoxSection**: Optional grouping component for organizing options into sections
|
|
18
|
+
- **Header**: Accessible heading for a `ListBoxSection`, forwards props and refs for full styling control
|
|
19
|
+
- **Collection**: Utility for rendering nested dynamic data inside a `ListBoxSection` (or other collection-aware context).
|
|
20
|
+
|
|
21
|
+
## Props
|
|
22
|
+
|
|
23
|
+
The following properties are available on the `ListBox` component:
|
|
24
|
+
|
|
25
|
+
## Static Items
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
/* v8 ignore next */
|
|
29
|
+
import React from 'react';
|
|
30
|
+
import { ListBox, ListBoxItem } from '@bento/listbox';
|
|
31
|
+
import style from './listbox.module.css';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Example component demonstrating basic ListBox usage.
|
|
35
|
+
*
|
|
36
|
+
* @param {any} args - The component props.
|
|
37
|
+
* @returns {JSX.Element} The rendered ListBox with static items.
|
|
38
|
+
* @public
|
|
39
|
+
*/
|
|
40
|
+
export function BasicListBoxExample(args: any) {
|
|
41
|
+
return (
|
|
42
|
+
<ListBox {...args} className={style.listbox}>
|
|
43
|
+
<ListBoxItem>Chicken Teriyaki</ListBoxItem>
|
|
44
|
+
<ListBoxItem>Salmon Bento</ListBoxItem>
|
|
45
|
+
<ListBoxItem>Beef Bowl</ListBoxItem>
|
|
46
|
+
</ListBox>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Sections
|
|
52
|
+
|
|
53
|
+
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`.
|
|
54
|
+
|
|
55
|
+
The `<Header>` component accepts standard DOM props and a `slot` prop for Bentoβs slot system, enabling fine-grained overrides in composite components.
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
/* v8 ignore next */
|
|
59
|
+
import React from 'react';
|
|
60
|
+
import { ListBox, ListBoxItem, ListBoxSection, Header } from '@bento/listbox';
|
|
61
|
+
import style from './listbox.module.css';
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Example component demonstrating ListBox with static sections.
|
|
65
|
+
*
|
|
66
|
+
* @param {any} args - The component props.
|
|
67
|
+
* @returns {JSX.Element} The rendered ListBox with sectioned items.
|
|
68
|
+
* @public
|
|
69
|
+
*/
|
|
70
|
+
export function SectionsExample(args: any) {
|
|
71
|
+
return (
|
|
72
|
+
<ListBox {...args} className={style.listbox}>
|
|
73
|
+
<ListBoxSection>
|
|
74
|
+
<Header>Main Dishes</Header>
|
|
75
|
+
<ListBoxItem>Chicken Teriyaki</ListBoxItem>
|
|
76
|
+
<ListBoxItem>Salmon Bento</ListBoxItem>
|
|
77
|
+
</ListBoxSection>
|
|
78
|
+
<ListBoxSection>
|
|
79
|
+
<Header>Side Dishes</Header>
|
|
80
|
+
<ListBoxItem>Pickled Vegetables</ListBoxItem>
|
|
81
|
+
<ListBoxItem>Edamame</ListBoxItem>
|
|
82
|
+
</ListBoxSection>
|
|
83
|
+
</ListBox>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Dynamic Collections
|
|
89
|
+
|
|
90
|
+
For dynamic data, use the `items` prop with a render function. The ListBox component follows different patterns depending on how it's used:
|
|
91
|
+
|
|
92
|
+
### When `items` prop is provided
|
|
93
|
+
When you provide an `items` prop, the `children` function receives **individual items** for React Aria compatibility:
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
/* v8 ignore next */
|
|
97
|
+
import React from 'react';
|
|
98
|
+
import { ListBox, ListBoxItem } from '@bento/listbox';
|
|
99
|
+
import style from './listbox.module.css';
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Example component demonstrating ListBox with dynamic collections.
|
|
103
|
+
*
|
|
104
|
+
* @param {any} args - The component props including items array.
|
|
105
|
+
* @returns {JSX.Element} The rendered ListBox with dynamic items.
|
|
106
|
+
* @public
|
|
107
|
+
*/
|
|
108
|
+
export function DynamicCollectionExample({ items, ...args }: any) {
|
|
109
|
+
return (
|
|
110
|
+
<ListBox {...args} className={style.listbox} items={items}>
|
|
111
|
+
{(item: any) => (
|
|
112
|
+
<ListBoxItem key={item.id} textValue={item.name}>
|
|
113
|
+
{item.name}
|
|
114
|
+
</ListBoxItem>
|
|
115
|
+
)}
|
|
116
|
+
</ListBox>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
In this pattern, `children` is called for each item in the `items` array, receiving the individual item data to render `ListBoxItem` components.
|
|
122
|
+
|
|
123
|
+
### When no `items` prop is provided
|
|
124
|
+
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:
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
<ListBox aria-label="Custom ListBox">
|
|
128
|
+
{({ isEmpty, isFocused, state, items }) => (
|
|
129
|
+
isEmpty ? (
|
|
130
|
+
<div>No items available</div>
|
|
131
|
+
) : (
|
|
132
|
+
// Render items normally using static children or other logic
|
|
133
|
+
<ListBoxItem>Static Item</ListBoxItem>
|
|
134
|
+
)
|
|
135
|
+
)}
|
|
136
|
+
</ListBox>
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
This pattern provides access to the ListBox's state, focus status, and other render props following Bento's consistent render prop API.
|
|
140
|
+
|
|
141
|
+
### Nested Collections with Sections
|
|
142
|
+
You can also render nested data inside a section using the exported `Collection` component:
|
|
143
|
+
|
|
144
|
+
```tsx
|
|
145
|
+
/* v8 ignore next */
|
|
146
|
+
import React from 'react';
|
|
147
|
+
import { ListBox, ListBoxItem } from '@bento/listbox';
|
|
148
|
+
import style from './listbox.module.css';
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Example component demonstrating ListBox with dynamic collections.
|
|
152
|
+
*
|
|
153
|
+
* @param {any} args - The component props including items array.
|
|
154
|
+
* @returns {JSX.Element} The rendered ListBox with dynamic items.
|
|
155
|
+
* @public
|
|
156
|
+
*/
|
|
157
|
+
export function DynamicCollectionExample({ items, ...args }: any) {
|
|
158
|
+
return (
|
|
159
|
+
<ListBox {...args} className={style.listbox} items={items}>
|
|
160
|
+
{(item: any) => (
|
|
161
|
+
<ListBoxItem key={item.id} textValue={item.name}>
|
|
162
|
+
{item.name}
|
|
163
|
+
</ListBoxItem>
|
|
164
|
+
)}
|
|
165
|
+
</ListBox>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Customization
|
|
171
|
+
|
|
172
|
+
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.
|
|
173
|
+
|
|
174
|
+
### Styling with Data Attributes
|
|
175
|
+
|
|
176
|
+
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.
|
|
177
|
+
|
|
178
|
+
#### ListBox Container Attributes
|
|
179
|
+
|
|
180
|
+
The main `ListBox` component exposes these data attributes:
|
|
181
|
+
|
|
182
|
+
- `[data-empty]` - Applied when the listbox contains no items
|
|
183
|
+
- `[data-focused]` - Applied when the listbox is focused
|
|
184
|
+
- `[data-focus-visible]` - Applied when the listbox has keyboard focus
|
|
185
|
+
- `[data-layout="stack"]` - The layout type (currently only "stack" supported)
|
|
186
|
+
- `[data-orientation="vertical|horizontal"]` - The primary orientation
|
|
187
|
+
- `[data-selection-mode="none|single|multiple"]` - The selection mode
|
|
188
|
+
- `[data-selection-behavior="toggle|replace"]` - How selection behaves
|
|
189
|
+
- `[data-allows-tab-navigation]` - Whether tab navigation is enabled
|
|
190
|
+
- `[data-focus-wrap]` - Whether focus wraps around the collection
|
|
191
|
+
|
|
192
|
+
#### ListBoxItem Attributes
|
|
193
|
+
|
|
194
|
+
Individual `ListBoxItem` components expose these data attributes:
|
|
195
|
+
|
|
196
|
+
- `[data-selected]` - Applied when the item is selected
|
|
197
|
+
- `[data-disabled]` - Applied when the item is disabled
|
|
198
|
+
- `[data-hovered]` - Applied when the item is being hovered
|
|
199
|
+
- `[data-focused]` - Applied when the item is focused
|
|
200
|
+
- `[data-focus-visible]` - Applied when the item has keyboard focus
|
|
201
|
+
- `[data-pressed]` - Applied when the item is being pressed
|
|
202
|
+
- `[data-level]` - The nesting level (useful for indentation)
|
|
203
|
+
- `[data-selection-mode]` - Inherited selection mode
|
|
204
|
+
- `[data-selection-behavior]` - Inherited selection behavior
|
|
205
|
+
- `[data-text-value]` - The computed text value for the item
|
|
206
|
+
|
|
207
|
+
#### ListBoxSection Attributes
|
|
208
|
+
|
|
209
|
+
The `ListBoxSection` component exposes:
|
|
210
|
+
|
|
211
|
+
- `[data-level]` - The nesting level of the section
|
|
212
|
+
|
|
213
|
+
#### CSS Styling Examples
|
|
214
|
+
|
|
215
|
+
```css
|
|
216
|
+
/* Basic item styling */
|
|
217
|
+
[role="option"] {
|
|
218
|
+
padding: 8px 12px;
|
|
219
|
+
border-radius: 6px;
|
|
220
|
+
transition: all 0.15s ease-in-out;
|
|
221
|
+
cursor: pointer;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/* Selected items */
|
|
225
|
+
[role="option"][data-selected] {
|
|
226
|
+
background: Highlight;
|
|
227
|
+
color: HighlightText;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/* Hovered items */
|
|
231
|
+
[role="option"][data-hovered] {
|
|
232
|
+
background: color-mix(in srgb, Highlight 10%, transparent);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/* Disabled items */
|
|
236
|
+
[role="option"][data-disabled] {
|
|
237
|
+
opacity: 0.6;
|
|
238
|
+
cursor: not-allowed;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/* Focused items (keyboard navigation) */
|
|
242
|
+
[role="option"][data-focus-visible] {
|
|
243
|
+
outline: 2px solid Highlight;
|
|
244
|
+
outline-offset: -2px;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/* Combined states */
|
|
248
|
+
[role="option"][data-selected][data-hovered] {
|
|
249
|
+
background: color-mix(in srgb, Highlight 90%, white);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/* Empty state styling */
|
|
253
|
+
.listbox[data-empty] {
|
|
254
|
+
min-height: 100px;
|
|
255
|
+
display: flex;
|
|
256
|
+
align-items: center;
|
|
257
|
+
justify-content: center;
|
|
258
|
+
color: #64748b;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/* Section headers */
|
|
262
|
+
.section-header {
|
|
263
|
+
font-weight: 600;
|
|
264
|
+
font-size: 0.875rem;
|
|
265
|
+
color: #64748b;
|
|
266
|
+
padding: 6px 12px;
|
|
267
|
+
margin-bottom: 4px;
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Slots System
|
|
272
|
+
|
|
273
|
+
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.
|
|
274
|
+
|
|
275
|
+
#### Basic Slot Usage
|
|
276
|
+
|
|
277
|
+
```tsx
|
|
278
|
+
/* v8 ignore next */
|
|
279
|
+
import React from 'react';
|
|
280
|
+
import { ListBox, ListBoxItem, ListBoxSection, Header, Collection } from '@bento/listbox';
|
|
281
|
+
import { useProps } from '@bento/use-props';
|
|
282
|
+
import style from './listbox.module.css';
|
|
283
|
+
|
|
284
|
+
//
|
|
285
|
+
// Slot namespace layout:
|
|
286
|
+
//
|
|
287
|
+
// ```
|
|
288
|
+
// bento-list (ListBox)
|
|
289
|
+
// βββ main-dishes (ListBoxSection)
|
|
290
|
+
// β βββ header (Header)
|
|
291
|
+
// β βββ chicken-teriyaki (ListBoxItem)
|
|
292
|
+
// β βββ salmon-bento (ListBoxItem)
|
|
293
|
+
// βββ side-dishes (ListBoxSection)
|
|
294
|
+
// βββ header (Header)
|
|
295
|
+
// βββ pickled-vegetables (ListBoxItem)
|
|
296
|
+
// βββ edamame (ListBoxItem)
|
|
297
|
+
// ```
|
|
298
|
+
//
|
|
299
|
+
// This example demonstrates several slot override patterns:
|
|
300
|
+
// 1. `side-dishes.header` β Custom header component with enhanced styling
|
|
301
|
+
// 2. `side-dishes.pickled-vegetables` β Override specific item in specific section
|
|
302
|
+
// 3. `main-dishes` β Override entire section styling
|
|
303
|
+
//
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Example component demonstrating ListBox with dynamic sections and slot overrides.
|
|
307
|
+
*
|
|
308
|
+
* @param {any} args - The component props including categories and slots.
|
|
309
|
+
* @returns {JSX.Element} The rendered ListBox with slotted dynamic sections.
|
|
310
|
+
* @public
|
|
311
|
+
*/
|
|
312
|
+
export function SlotsDynamicSectionsExample({ categories, ...args }: any) {
|
|
313
|
+
const {
|
|
314
|
+
items: argItems,
|
|
315
|
+
slots: argSlots = {},
|
|
316
|
+
...rest
|
|
317
|
+
} = args as {
|
|
318
|
+
items?: Iterable<unknown>;
|
|
319
|
+
slots?: Record<string, any>;
|
|
320
|
+
} & Record<string, unknown>;
|
|
321
|
+
|
|
322
|
+
const { apply } = useProps(rest);
|
|
323
|
+
|
|
324
|
+
//
|
|
325
|
+
// Default slot overrides for demo - these show different override patterns
|
|
326
|
+
//
|
|
327
|
+
const demoSlots: Record<string, any> = {
|
|
328
|
+
//
|
|
329
|
+
// Override a header in a specific section with custom styling
|
|
330
|
+
//
|
|
331
|
+
'side-dishes.header': ({ props, original }: { props: Record<string, any>; original: React.ReactNode }) => (
|
|
332
|
+
<Header {...props}>π₯’ {original}</Header>
|
|
333
|
+
),
|
|
334
|
+
|
|
335
|
+
//
|
|
336
|
+
// Override another specific item with custom content
|
|
337
|
+
//
|
|
338
|
+
'side-dishes.pickled-vegetables': ({ original }: { original: React.ReactNode }) => (
|
|
339
|
+
<div style={{ backgroundColor: '#4ade80', color: 'white', padding: '2px 6px', borderRadius: '4px' }}>
|
|
340
|
+
π₯ {original} (Traditional)
|
|
341
|
+
</div>
|
|
342
|
+
),
|
|
343
|
+
|
|
344
|
+
//
|
|
345
|
+
// Override an entire section with custom wrapper
|
|
346
|
+
//
|
|
347
|
+
'main-dishes': ({ original }: { original: React.ReactNode }) => (
|
|
348
|
+
<div style={{ border: '2px dashed #f59e0b', padding: '8px', borderRadius: '6px' }}>{original}</div>
|
|
349
|
+
)
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
//
|
|
353
|
+
// Merge provided slots with demo slots (provided slots take precedence)
|
|
354
|
+
//
|
|
355
|
+
const mergedSlots = { ...demoSlots, ...argSlots };
|
|
356
|
+
|
|
357
|
+
return (
|
|
358
|
+
<ListBox
|
|
359
|
+
{...apply({ className: style.listbox })}
|
|
360
|
+
// Only set items if caller didn't already supply one
|
|
361
|
+
items={argItems ?? categories}
|
|
362
|
+
slot="bento-list"
|
|
363
|
+
slots={mergedSlots}
|
|
364
|
+
>
|
|
365
|
+
{(category: any) => (
|
|
366
|
+
<ListBoxSection key={category.id} slot={category.id}>
|
|
367
|
+
<Header slot="header">{category.name}</Header>
|
|
368
|
+
<Collection items={category.items}>
|
|
369
|
+
{(item: { id: string; name: string }) => (
|
|
370
|
+
<ListBoxItem key={item.id} textValue={item.name} slot={item.id}>
|
|
371
|
+
{item.name}
|
|
372
|
+
</ListBoxItem>
|
|
373
|
+
)}
|
|
374
|
+
</Collection>
|
|
375
|
+
</ListBoxSection>
|
|
376
|
+
)}
|
|
377
|
+
</ListBox>
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
#### Advanced Slot Patterns
|
|
383
|
+
|
|
384
|
+
You can target specific items or sections using hierarchical slot names:
|
|
385
|
+
|
|
386
|
+
```tsx
|
|
387
|
+
// Override a specific section header
|
|
388
|
+
const slots = {
|
|
389
|
+
'my-listbox.fruits.header': ({ original, props }) => (
|
|
390
|
+
<Header {...props}>π {original}</Header>
|
|
391
|
+
),
|
|
392
|
+
|
|
393
|
+
// Override a specific item in a specific section
|
|
394
|
+
'my-listbox.fruits.apple': ({ original }) => (
|
|
395
|
+
<div className="special-item">β {original}</div>
|
|
396
|
+
),
|
|
397
|
+
|
|
398
|
+
// Override an entire section
|
|
399
|
+
'my-listbox.vegetables': ({ original }) => (
|
|
400
|
+
<div className="veggie-section">{original}</div>
|
|
401
|
+
)
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
<ListBox slot="my-listbox" slots={slots}>
|
|
405
|
+
<ListBoxSection slot="fruits">
|
|
406
|
+
<Header slot="header">Fruits</Header>
|
|
407
|
+
<ListBoxItem slot="apple">Apple</ListBoxItem>
|
|
408
|
+
<ListBoxItem slot="orange">Orange</ListBoxItem>
|
|
409
|
+
</ListBoxSection>
|
|
410
|
+
<ListBoxSection slot="vegetables">
|
|
411
|
+
<Header slot="header">Vegetables</Header>
|
|
412
|
+
<ListBoxItem slot="carrot">Carrot</ListBoxItem>
|
|
413
|
+
</ListBoxSection>
|
|
414
|
+
</ListBox>
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
### Render Props
|
|
418
|
+
|
|
419
|
+
The `ListBoxItem` component supports render prop patterns for dynamic content based on interaction state:
|
|
420
|
+
|
|
421
|
+
```tsx
|
|
422
|
+
// ListBoxItem render prop with interaction state
|
|
423
|
+
<ListBoxItem>
|
|
424
|
+
{({ isSelected, isHovered, isDisabled }) => (
|
|
425
|
+
<div className={`item ${isSelected ? 'selected' : ''}`}>
|
|
426
|
+
{isHovered && 'π '}
|
|
427
|
+
My Item
|
|
428
|
+
{isSelected && ' β'}
|
|
429
|
+
</div>
|
|
430
|
+
)}
|
|
431
|
+
</ListBoxItem>
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
For ListBox-level render props, use the `renderEmptyState` prop to customize empty state display:
|
|
435
|
+
|
|
436
|
+
```tsx
|
|
437
|
+
<ListBox
|
|
438
|
+
items={items}
|
|
439
|
+
renderEmptyState={({ isEmpty, isFocused, state, items }) => (
|
|
440
|
+
<div className="empty-state">
|
|
441
|
+
{isFocused ? 'No items found (focused)' : 'No items available'}
|
|
442
|
+
</div>
|
|
443
|
+
)}
|
|
444
|
+
>
|
|
445
|
+
{(item: any) => (
|
|
446
|
+
<ListBoxItem key={item.id} textValue={item.name}>
|
|
447
|
+
{item.name}
|
|
448
|
+
</ListBoxItem>
|
|
449
|
+
)}
|
|
450
|
+
</ListBox>
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
### Empty State Customization
|
|
454
|
+
|
|
455
|
+
Customize the appearance when no items are present:
|
|
456
|
+
|
|
457
|
+
```tsx
|
|
458
|
+
<ListBox
|
|
459
|
+
renderEmptyState={({ isEmpty, isFocused }) => (
|
|
460
|
+
<div className="empty-state">
|
|
461
|
+
<span>π</span>
|
|
462
|
+
<p>No items to display</p>
|
|
463
|
+
{isFocused && <p>Start typing to search...</p>}
|
|
464
|
+
</div>
|
|
465
|
+
)}
|
|
466
|
+
>
|
|
467
|
+
{/* Items when present */}
|
|
468
|
+
</ListBox>
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
### Accessibility Customization
|
|
472
|
+
|
|
473
|
+
All components support standard ARIA attributes for enhanced accessibility:
|
|
474
|
+
|
|
475
|
+
```tsx
|
|
476
|
+
<ListBox
|
|
477
|
+
aria-label="Food menu"
|
|
478
|
+
aria-describedby="menu-description"
|
|
479
|
+
>
|
|
480
|
+
<ListBoxSection aria-label="Main courses">
|
|
481
|
+
<Header>Main Dishes</Header>
|
|
482
|
+
<ListBoxItem aria-label="Chicken teriyaki with rice">
|
|
483
|
+
Chicken Teriyaki
|
|
484
|
+
</ListBoxItem>
|
|
485
|
+
</ListBoxSection>
|
|
486
|
+
</ListBox>
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
### CSS-in-JS and Styled Components
|
|
490
|
+
|
|
491
|
+
The data attributes work seamlessly with CSS-in-JS libraries:
|
|
492
|
+
|
|
493
|
+
```tsx
|
|
494
|
+
// Styled Components
|
|
495
|
+
const StyledListBox = styled.div`
|
|
496
|
+
&[data-focused] {
|
|
497
|
+
box-shadow: 0 0 0 2px blue;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
[role="option"][data-selected] {
|
|
501
|
+
background: ${props => props.theme.primary};
|
|
502
|
+
}
|
|
503
|
+
`;
|
|
504
|
+
|
|
505
|
+
// Emotion
|
|
506
|
+
const listboxStyles = css`
|
|
507
|
+
&[data-empty] {
|
|
508
|
+
opacity: 0.5;
|
|
509
|
+
}
|
|
510
|
+
`;
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
### Animation and Transitions
|
|
514
|
+
|
|
515
|
+
Data attributes enable smooth state transitions:
|
|
516
|
+
|
|
517
|
+
```css
|
|
518
|
+
[role="option"] {
|
|
519
|
+
transition: all 0.2s ease-in-out;
|
|
520
|
+
transform: translateY(0);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
[role="option"][data-hovered] {
|
|
524
|
+
transform: translateY(-2px);
|
|
525
|
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
[role="option"][data-pressed] {
|
|
529
|
+
transform: translateY(0);
|
|
530
|
+
transition-duration: 0.1s;
|
|
531
|
+
}
|
|
532
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bento/listbox",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Listbox component",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"dev": "tsup-node --watch",
|
|
11
11
|
"lint": "biome lint && tsc --noEmit",
|
|
12
12
|
"posttest": "npm run lint",
|
|
13
|
+
"prepublishOnly": "node ../../scripts/compile-readme.ts",
|
|
13
14
|
"pretest": "npm run build",
|
|
14
15
|
"test": "vitest --run",
|
|
15
16
|
"test:watch": "vitest"
|
|
@@ -42,9 +43,9 @@
|
|
|
42
43
|
"package.json"
|
|
43
44
|
],
|
|
44
45
|
"dependencies": {
|
|
45
|
-
"@bento/slots": "^0.1.
|
|
46
|
-
"@bento/use-data-attributes": "^0.1.
|
|
47
|
-
"@bento/use-props": "^0.1.
|
|
46
|
+
"@bento/slots": "^0.1.3",
|
|
47
|
+
"@bento/use-data-attributes": "^0.1.1",
|
|
48
|
+
"@bento/use-props": "^0.1.1",
|
|
48
49
|
"@react-aria/collections": "^3.0.0",
|
|
49
50
|
"@react-types/listbox": "^3.7.1",
|
|
50
51
|
"react-aria": "^3.44.0",
|