@antadesign/anta 0.3.18 → 0.3.20
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 +38 -242
- package/dist/anta_helpers.d.ts +2 -2
- package/dist/components/Banner.d.ts +3 -3
- package/dist/components/Button.d.ts +1 -1
- package/dist/components/Calendar.d.ts +11 -12
- package/dist/components/Card.d.ts +1 -1
- package/dist/components/Card.js +1 -1
- package/dist/components/Checkbox.d.ts +8 -9
- package/dist/components/Dialog.d.ts +1 -1
- package/dist/components/Expander.d.ts +5 -6
- package/dist/components/Input.d.ts +6 -3
- package/dist/components/Input.js +3 -1
- package/dist/components/InputAutocomplete.d.ts +8 -8
- package/dist/components/InputDate.css +1 -0
- package/dist/components/InputDate.d.ts +1 -0
- package/dist/components/InputDate.js +5 -5
- package/dist/components/InputTime.d.ts +3 -3
- package/dist/components/InputTime.js +1 -1
- package/dist/components/Menu.d.ts +7 -3
- package/dist/components/Menu.js +3 -2
- package/dist/components/MenuItem.d.ts +1 -1
- package/dist/components/MenuItem.js +1 -1
- package/dist/components/Progress.d.ts +1 -1
- package/dist/components/RadioGroup.d.ts +12 -13
- package/dist/components/Select.d.ts +32 -38
- package/dist/components/Select.js +9 -13
- package/dist/components/SelectFaceted.d.ts +28 -33
- package/dist/components/SelectFaceted.js +29 -23
- package/dist/components/Slider.d.ts +5 -5
- package/dist/components/Switch.d.ts +4 -4
- package/dist/components/Tabs.d.ts +7 -7
- package/dist/components/Title.d.ts +1 -1
- package/dist/components/Toaster.d.ts +1 -1
- package/dist/components/Tooltip.d.ts +1 -1
- package/dist/components/select-parts.css +1 -0
- package/dist/elements/a-card.css +1 -1
- package/dist/elements/a-dialog.css +1 -1
- package/dist/elements/a-expander.css +1 -1
- package/dist/elements/a-expander.d.ts +1 -0
- package/dist/elements/a-expander.js +12 -2
- package/dist/elements/a-input.css +1 -1
- package/dist/elements/a-menu.js +12 -0
- package/dist/elements/a-progress.css +1 -1
- package/dist/elements/a-slider.css +1 -1
- package/dist/elements/a-tabs.css +1 -1
- package/dist/elements/a-tabs.d.ts +3 -0
- package/dist/elements/a-tabs.js +40 -1
- package/dist/elements/a-tooltip.css +1 -1
- package/dist/elements/a-tooltip.d.ts +1 -1
- package/dist/elements/a-tooltip.js +1 -1
- package/dist/elements/index.d.ts +1 -1
- package/dist/general_types.d.ts +24 -22
- package/dist/index.d.ts +3 -3
- package/dist/jsx-runtime.d.ts +9 -0
- package/docs/accessibility.md +34 -0
- package/docs/changelog.md +561 -0
- package/docs/colors.md +42 -0
- package/docs/comparison.md +28 -0
- package/docs/components/banner.md +267 -0
- package/docs/components/button.md +603 -0
- package/docs/components/card.md +321 -0
- package/docs/components/checkbox.md +296 -0
- package/docs/components/dialog.md +314 -0
- package/docs/components/expander.md +526 -0
- package/docs/components/icon.md +155 -0
- package/docs/components/input-autocomplete.md +100 -0
- package/docs/components/input-date.md +401 -0
- package/docs/components/input-time.md +174 -0
- package/docs/components/input.md +610 -0
- package/docs/components/loader.md +139 -0
- package/docs/components/menu.md +511 -0
- package/docs/components/progress.md +182 -0
- package/docs/components/radio.md +368 -0
- package/docs/components/select-faceted.md +451 -0
- package/docs/components/select.md +926 -0
- package/docs/components/slider.md +147 -0
- package/docs/components/switch.md +198 -0
- package/docs/components/tabs.md +471 -0
- package/docs/components/tag.md +185 -0
- package/docs/components/text.md +148 -0
- package/docs/components/title.md +146 -0
- package/docs/components/toaster.md +313 -0
- package/docs/components/tooltip.md +177 -0
- package/docs/credits.md +18 -0
- package/docs/index.md +52 -0
- package/docs/install-config.md +240 -0
- package/docs/normalization.md +102 -0
- package/docs/overview.md +10 -0
- package/docs/packages/stickers.md +187 -0
- package/docs/packages/table.md +153 -0
- package/docs/theming.md +13 -0
- package/package.json +10 -6
|
@@ -0,0 +1,926 @@
|
|
|
1
|
+
# Select
|
|
2
|
+
|
|
3
|
+
`Select` lets people choose one or more options from a dropdown. Its read-only
|
|
4
|
+
field shows the selected value. Click the field to open the menu. The menu
|
|
5
|
+
closes when a user clicks outside it or presses Esc.
|
|
6
|
+
|
|
7
|
+
Select uses an [Input](./input.md) for its field and a [Menu](./menu.md) for its
|
|
8
|
+
dropdown. Pass `options` as an array of strings or objects. Use `value` with
|
|
9
|
+
**`onValueChange`** to control the selection, or use `defaultValue` for an
|
|
10
|
+
uncontrolled Select.
|
|
11
|
+
|
|
12
|
+
## Options
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
<Select
|
|
16
|
+
label="Field"
|
|
17
|
+
options={[
|
|
18
|
+
{ value: 'output_text', hint: 'Raw log message or event payload', icon: 'file' },
|
|
19
|
+
{ value: 'stream', hint: 'Log level: error · info · internal', icon: 'braces' },
|
|
20
|
+
{ value: 'container', hint: 'Host where the event occurred', icon: 'cube' },
|
|
21
|
+
{ value: 'archived', hint: 'Read-only snapshot', icon: 'folder-close', disabled: true },
|
|
22
|
+
{ value: 'flagged', hint: 'Needs review', icon: 'warning-triangle', tone: 'warning' },
|
|
23
|
+
{ value: 'custom', hint: 'Your own field path', icon: 'asterisk', tone: '#c026d3' },
|
|
24
|
+
]}
|
|
25
|
+
/>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`options` takes bare strings or objects. A string is shorthand for
|
|
29
|
+
`{ value, label: value }`. An object adds a `label`, a `hint` (the secondary line),
|
|
30
|
+
a leading `icon`, a per-option `tone` (a named tone or a custom CSS color), or
|
|
31
|
+
`disabled`. Open the field: the chosen row is highlighted, and the disabled row is
|
|
32
|
+
skipped by the keyboard.
|
|
33
|
+
|
|
34
|
+
## Selection modes
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
// Single-select marks (single value)
|
|
38
|
+
<Select indicator="check" options={OPTIONS} value={one} onValueChange={setOne} /> // trailing ✓ + tint
|
|
39
|
+
<Select indicator="radio" options={OPTIONS} value={one} onValueChange={setOne} /> // leading radio
|
|
40
|
+
<Select options={OPTIONS} value={one} onValueChange={setOne} /> // tint only (default)
|
|
41
|
+
|
|
42
|
+
// Tone the selected row: named tone or custom color (the indicator adopts it)
|
|
43
|
+
<Select indicator="check" toneSelected="brand" options={OPTIONS} value={one} onValueChange={setOne} />
|
|
44
|
+
<Select indicator="radio" toneSelected="#c026d3" options={OPTIONS} value={one} onValueChange={setOne} />
|
|
45
|
+
|
|
46
|
+
// Multiple: checkboxes, count summary, Select-all row (default); value is an array
|
|
47
|
+
<Select
|
|
48
|
+
selection="multiple"
|
|
49
|
+
options={OPTIONS}
|
|
50
|
+
value={many}
|
|
51
|
+
onValueChange={setMany}
|
|
52
|
+
/>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`selection` sets **behavior**, `indicator` sets the single-select **mark**.
|
|
56
|
+
|
|
57
|
+
`selection="single"` (the default) keeps `value` a single value and closes on pick.
|
|
58
|
+
`indicator` then picks the row mark: **`"none"`** (default; a tint-only highlight),
|
|
59
|
+
**`"check"`** (a trailing checkmark on the chosen row that keeps the tint, the
|
|
60
|
+
canonical Select look), or **`"radio"`** (a leading radio on every row).
|
|
61
|
+
|
|
62
|
+
`selection="multiple"` makes `value` an **array**, puts a **checkbox** on
|
|
63
|
+
every row, keeps the menu open as you toggle, and summarises the selection in the
|
|
64
|
+
field: **`All`** when every option is on, the single label for one, else **"N
|
|
65
|
+
selected"** (pass **`verbose`** to spell the picks out — `3 selected: A, B, C`; or
|
|
66
|
+
take over the text entirely with [`renderSummary`](#selection-summary); an empty
|
|
67
|
+
selection shows the `placeholder`). A
|
|
68
|
+
**Select all** top row shows by default; it toggles everything, and
|
|
69
|
+
its box goes mixed when only some are on. Pass **`selectAll={false}`** to drop it.
|
|
70
|
+
|
|
71
|
+
In a multi-select, **`Alt`+Click** a row to clear every other selection and
|
|
72
|
+
select that row. On macOS, use Option instead of Alt.
|
|
73
|
+
The standard tooltip describes this shortcut when the pointer is over a row.
|
|
74
|
+
Set `SelectOption.tooltip` to replace it, or to `''` to hide it. The shortcut
|
|
75
|
+
is available only when `selectAll` is enabled, so `selectAll={false}` removes it.
|
|
76
|
+
|
|
77
|
+
Each checkable row is the control itself (`role="menuitemcheckbox"` /
|
|
78
|
+
`menuitemradio`, `aria-checked`); the checkbox / radio / check is a passive indicator
|
|
79
|
+
the row drives.
|
|
80
|
+
|
|
81
|
+
**`toneSelected`** tones the selected row(s): the whole row takes the tone, including
|
|
82
|
+
label, icon, indicator, and tint. Pass a named tone or any CSS color (a custom color
|
|
83
|
+
keeps its hue, with lightness pinned to the brand text). It reads strongest with
|
|
84
|
+
`indicator` `none` / `check`, which show the row tint; `radio` / `checkbox` have no
|
|
85
|
+
row tint, so it tones the label and indicator only.
|
|
86
|
+
|
|
87
|
+
## Filter
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
// Built-in matcher: case-insensitive substring of value / label / hint
|
|
91
|
+
<Select filter indicator="check" options={OPTIONS} value={v} onValueChange={setV} />
|
|
92
|
+
|
|
93
|
+
// Custom matcher: a per-option predicate
|
|
94
|
+
<Select
|
|
95
|
+
filter={(option, query) => option.value.startsWith(query)}
|
|
96
|
+
options={OPTIONS}
|
|
97
|
+
value={v}
|
|
98
|
+
onValueChange={setV}
|
|
99
|
+
/>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Add **`filter`** for a search field at the top of the menu that narrows the options
|
|
103
|
+
as you type. `true` uses the built-in matcher: a case-insensitive substring of the
|
|
104
|
+
option's **value, label, or hint**. It's **whitespace-flexible** (a typed space
|
|
105
|
+
matches any run of whitespace) and **bolds the matched substring** in the results.
|
|
106
|
+
Pass a **function** `(option, query) => boolean` for custom matching (no highlight).
|
|
107
|
+
|
|
108
|
+
With `filter`, Select behaves as a **combobox**. Opening it focuses the search
|
|
109
|
+
field. ↑ and ↓ move through matching options while focus
|
|
110
|
+
stays in the field. Enter selects the active option, and Esc
|
|
111
|
+
closes the menu. The search field stays visible while the results scroll. A
|
|
112
|
+
"No matches" row appears when nothing matches, and the query resets when the
|
|
113
|
+
menu closes. In `multiple` mode, `selectAll` affects only the visible matches.
|
|
114
|
+
|
|
115
|
+
## Value and changes
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
const [field, setField] = useState('stream')
|
|
119
|
+
|
|
120
|
+
<Select
|
|
121
|
+
label="Field"
|
|
122
|
+
options={['output_text', 'stream', 'container', 'vtime', 'custom']}
|
|
123
|
+
value={field}
|
|
124
|
+
onValueChange={(v) => setField(v)}
|
|
125
|
+
/>
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The value is the **option's `value`** — `value="stream"` in single mode, an array
|
|
129
|
+
like `value={['stream', 'container']}` in `multiple`. It's the option's identity:
|
|
130
|
+
what `value` / `defaultValue` name and what `onValueChange` reports, so keep each
|
|
131
|
+
option's `value` **unique across the whole tree**. Selection is value-keyed and
|
|
132
|
+
global across groups / submenus, so a value repeated in two sections is one logical
|
|
133
|
+
pick (both rows toggle together; the trigger resolves to the last). Dev builds
|
|
134
|
+
`console.warn` on a duplicate.
|
|
135
|
+
|
|
136
|
+
Values aren't limited to strings. `Select` **infers its value type from `options`**:
|
|
137
|
+
pass `options={[{ value: 365 }, { value: 90 }]}` and `value`, `onValueChange`'s value,
|
|
138
|
+
and `attrs.value` are all `number` (the same for `boolean`). Values compare with `===`,
|
|
139
|
+
so `365` and `"365"` stay distinct and no string conversion is needed. The exported
|
|
140
|
+
`OptionValue` type is `string | number | boolean`; for anything richer (a date, an
|
|
141
|
+
object), give `value` a **stable primitive key** (a date's ISO string, a record's id)
|
|
142
|
+
and read the full value back off `attrs.option`.
|
|
143
|
+
|
|
144
|
+
```tsx
|
|
145
|
+
// Use the ID as the value and attach the full record to the option.
|
|
146
|
+
<Select
|
|
147
|
+
options={runs.map((r) => ({ value: r.id, label: r.name, run: r }))}
|
|
148
|
+
onValueChange={(id, attrs) => { if (!('all' in attrs)) openRun(attrs.option.run) }}
|
|
149
|
+
/>
|
|
150
|
+
|
|
151
|
+
// Use an ISO string as the value, then convert it back to a Date in the callback.
|
|
152
|
+
<Select
|
|
153
|
+
options={days.map((d) => ({ value: d.toISOString(), label: fmt(d) }))}
|
|
154
|
+
onValueChange={(iso) => setDay(new Date(iso))}
|
|
155
|
+
/>
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
For an **uncontrolled** Select, omit `value` and pass `defaultValue`. For a
|
|
159
|
+
**controlled** Select, pass `value` and update it in `onValueChange`. The field
|
|
160
|
+
then shows the value supplied by the application.
|
|
161
|
+
|
|
162
|
+
**`onValueChange(value, attrs)`** is the single selection callback. The new value
|
|
163
|
+
comes first (a single value, or an array in `multiple` mode), with a snapshot
|
|
164
|
+
second: `{ value, option }` for the changed row, plus `selected` in `multiple` mode
|
|
165
|
+
(or `{ all: true }` for the Select-all row). There's **no `onStateChange`**: Select
|
|
166
|
+
has no discrete element state to veto (Checkbox and RadioGroup do), so it follows
|
|
167
|
+
Input's model of one value callback, and a controlled consumer rejects a pick by not
|
|
168
|
+
updating `value`. See the [event-model note on Input](./input.md#events) for
|
|
169
|
+
the rationale.
|
|
170
|
+
|
|
171
|
+
### Deriving the selection tree
|
|
172
|
+
|
|
173
|
+
The callback hands you the new value (and the one row that changed); to read the
|
|
174
|
+
whole selection back against your hierarchy, use **`optionsWithSelection(options,
|
|
175
|
+
values)`**. It returns your `options` tree with every leaf marked `selected` and
|
|
176
|
+
every group / submenu carrying a rolled-up `selectionState` — `'all'`, `'none'`, or
|
|
177
|
+
`'some'` of its descendants. It's a pure function (no `Select` instance, reads
|
|
178
|
+
nothing off the event), so it behaves the same controlled or uncontrolled: hand it
|
|
179
|
+
your current `value` and render a grouped summary, a section indicator, or a diff.
|
|
180
|
+
|
|
181
|
+
The panel on the right is that projection, re-derived on every pick. Toggle scopes and
|
|
182
|
+
watch the leaf checks and the section `Tag`s (`some` / `all`) update:
|
|
183
|
+
|
|
184
|
+
```tsx
|
|
185
|
+
import { Select, optionsWithSelection } from '@antadesign/anta'
|
|
186
|
+
|
|
187
|
+
const [values, setValues] = useState<string[]>(['repo.read', 'billing.view'])
|
|
188
|
+
|
|
189
|
+
// `values` drives the Select; the tree is derived from it and rendered read-only.
|
|
190
|
+
const tree = optionsWithSelection(scopes, values)
|
|
191
|
+
// tree: [ { label: 'Repositories', selectionState: 'some', options: [
|
|
192
|
+
// { value: 'repo.read', label: 'Read', selected: true },
|
|
193
|
+
// { value: 'repo.write', label: 'Write', selected: false }, … ] },
|
|
194
|
+
// { label: 'Members', selectionState: 'some', options: [ …,
|
|
195
|
+
// { label: 'Billing', selectionState: 'all', submenu: [
|
|
196
|
+
// { value: 'billing.view', label: 'View invoices', selected: true },
|
|
197
|
+
// { value: 'billing.manage', label: 'Manage plan', selected: true } ] } ] },
|
|
198
|
+
// { value: 'audit.read', label: 'Audit log', selected: false } ]
|
|
199
|
+
|
|
200
|
+
<Select selection="multiple" options={scopes} value={values} onValueChange={setValues} />
|
|
201
|
+
{/* render `tree` however you like — a summary, section indicators, a diff */}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
**Don't feed the tree back into `options` to control selection.** The projection
|
|
205
|
+
flows one way: `value` controls the `Select`, and the tree is a derived, read-only
|
|
206
|
+
view of it. `Select` resolves selection purely from `value` and never reads a
|
|
207
|
+
`selected` flag off `options`, so a `selected: true` in the tree changes nothing — it
|
|
208
|
+
rides along as inert data. Control the `Select` with the value array; keep the
|
|
209
|
+
annotated tree for rendering.
|
|
210
|
+
|
|
211
|
+
```tsx
|
|
212
|
+
// ✗ Wrong — `selected` is ignored, so the Select isn't controlled by this
|
|
213
|
+
<Select options={optionsWithSelection(scopes, values)} />
|
|
214
|
+
|
|
215
|
+
// ✓ Right — selection is value-keyed; drive it with `value`
|
|
216
|
+
<Select options={scopes} value={values} onValueChange={setValues} />
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Bare-string options normalize to `{ value, label }`; a value under more than one
|
|
220
|
+
section marks the leaf everywhere it occurs (selection is value-keyed).
|
|
221
|
+
|
|
222
|
+
## Size and status
|
|
223
|
+
|
|
224
|
+
```tsx
|
|
225
|
+
<Select label="Small · info" size="small" status="info" hint="Lowercase & dashes only" options={OPTIONS} defaultValue="stream" />
|
|
226
|
+
<Select label="Medium · success" size="medium" status="success" hint="Field is valid" options={OPTIONS} defaultValue="stream" />
|
|
227
|
+
<Select label="Large · critical" size="large" status="critical" hint="Pick a field" options={OPTIONS} placeholder="Required…" />
|
|
228
|
+
{/* statusIcon overrides the per-status glyph, or `false` drops it, as on Input */}
|
|
229
|
+
<Select status="info" statusIcon="sparkles" hint="Custom glyph" options={OPTIONS} defaultValue="stream" />
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
The trigger is an [Input](./input.md), so it inherits the field props.
|
|
233
|
+
**`size`** (`small` · `medium` · `large`) sets the height and type scale.
|
|
234
|
+
**`status`** paints a validation tone on the border, `hint`, and chevron, and
|
|
235
|
+
prefixes a glyph (`info` / `success` / `warning` / `critical`); **`statusIcon`**
|
|
236
|
+
overrides that glyph (any shape, or `false` to drop it), as on Input. The message
|
|
237
|
+
rides in `hint`.
|
|
238
|
+
|
|
239
|
+
`status` is a validation *state* over the same palette as `tone`: it also carries
|
|
240
|
+
the glyph and, for `critical`, validity. See the
|
|
241
|
+
[Input status note](./input.md#status). The field also takes `label`,
|
|
242
|
+
`placeholder`, `round`, and `disabled`, and fills its container; set a width via
|
|
243
|
+
`style` / `className` (both forwarded to the field).
|
|
244
|
+
|
|
245
|
+
## Customization
|
|
246
|
+
|
|
247
|
+
Select has no `<a-select>` element. The React and Preact wrapper coordinates an
|
|
248
|
+
Input and a Menu. You can use the wrapper, render custom option content with
|
|
249
|
+
`renderOption`, replace its selection mark with `renderIndicator`, or build the
|
|
250
|
+
same interaction from the raw elements.
|
|
251
|
+
|
|
252
|
+
Pass `placement` to control where the options menu opens relative to its field. It
|
|
253
|
+
uses the same values as `Menu` and still flips or clamps when space is limited.
|
|
254
|
+
`offset` sets the gap in pixels between the field and the menu:
|
|
255
|
+
|
|
256
|
+
```tsx
|
|
257
|
+
<Select placement="top-end" offset={8} options={OPTIONS} label="Field" />
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Build it from elements
|
|
261
|
+
|
|
262
|
+
The wrapper renders a read-only `<Input>` and a `<Menu>`. It updates the field
|
|
263
|
+
and each option's `selected` state from the value, and reports a pick through
|
|
264
|
+
`onValueChange`. Menu handles positioning, dismissal, and keyboard navigation.
|
|
265
|
+
|
|
266
|
+
The following example shows the equivalent markup without React or Preact. Place
|
|
267
|
+
the Menu directly after the Input so it can use that Input as its trigger.
|
|
268
|
+
|
|
269
|
+
```html
|
|
270
|
+
<!-- `.select` scopes this example's CSS. Use an application selector instead. -->
|
|
271
|
+
<div class="select" style="width: 240px">
|
|
272
|
+
<!-- Read-only field trigger; `dim-actions` dims the chevron at rest. The
|
|
273
|
+
trailing slot supplies the inline inset (--input-trailing-inset); the
|
|
274
|
+
transition on the chevron matches <Select>. -->
|
|
275
|
+
<a-input readonly dim-actions value="stream" aria-haspopup="menu">
|
|
276
|
+
<a-icon slot="trailing" shape="chevron-down" style="transition: transform 150ms ease"></a-icon>
|
|
277
|
+
</a-input>
|
|
278
|
+
<a-menu role="menu">
|
|
279
|
+
<a-menu-item role="menuitemradio" tabindex="0" aria-checked="false" value="output_text"><a-menu-item-label>output_text</a-menu-item-label></a-menu-item>
|
|
280
|
+
<a-menu-item role="menuitemradio" tabindex="0" aria-checked="true" value="stream" selected><a-menu-item-label>stream</a-menu-item-label></a-menu-item>
|
|
281
|
+
<a-menu-item role="menuitemradio" tabindex="0" aria-checked="false" value="container"><a-menu-item-label>container</a-menu-item-label></a-menu-item>
|
|
282
|
+
</a-menu>
|
|
283
|
+
</div>
|
|
284
|
+
<script type="module">
|
|
285
|
+
import '@antadesign/anta/elements'
|
|
286
|
+
const root = document.querySelector('.select')
|
|
287
|
+
const field = root.querySelector('a-input')
|
|
288
|
+
const menu = root.querySelector('a-menu')
|
|
289
|
+
const chevron = field.querySelector('a-icon')
|
|
290
|
+
// Flip the chevron with the menu's open state (as <Select> does).
|
|
291
|
+
menu.addEventListener('statechange', (e) => {
|
|
292
|
+
chevron.style.transform = e.detail.next === 'open' ? 'rotate(180deg)' : ''
|
|
293
|
+
})
|
|
294
|
+
// On pick: mark the row selected + reflect its label into the field.
|
|
295
|
+
menu.addEventListener('click', (e) => {
|
|
296
|
+
const item = e.target.closest('a-menu-item')
|
|
297
|
+
if (!item) return
|
|
298
|
+
menu.querySelectorAll('a-menu-item').forEach((el) => {
|
|
299
|
+
const selected = el === item
|
|
300
|
+
el.toggleAttribute('selected', selected)
|
|
301
|
+
el.setAttribute('aria-checked', String(selected))
|
|
302
|
+
})
|
|
303
|
+
field.value = item.querySelector('a-menu-item-label').textContent
|
|
304
|
+
})
|
|
305
|
+
</script>
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
The React and Preact `Select` wrapper performs those updates for you.
|
|
309
|
+
|
|
310
|
+
### Custom option rendering
|
|
311
|
+
|
|
312
|
+
```tsx
|
|
313
|
+
const TONE = { in_progress: 'info', completed: 'neutral', incomplete: 'critical' }
|
|
314
|
+
const LABEL = { in_progress: 'In progress', completed: 'Completed', incomplete: 'Incomplete' }
|
|
315
|
+
|
|
316
|
+
// The field fills its container, so the wrapper's width bounds the trigger.
|
|
317
|
+
// (<Select style={{ width: 260 }} …> works too; style forwards to the field.)
|
|
318
|
+
<div style={{ width: 260 }}>
|
|
319
|
+
<Select
|
|
320
|
+
label="Test run"
|
|
321
|
+
filter // name / date stay searchable
|
|
322
|
+
options={runs} // { value, label, name, ranAt, status }[]
|
|
323
|
+
value={run}
|
|
324
|
+
onValueChange={setRun}
|
|
325
|
+
renderOption={(option) => {
|
|
326
|
+
const run = option as Run // index-signature fields are `unknown`
|
|
327
|
+
return (
|
|
328
|
+
<div style={{ display: 'flex', flexDirection: 'column', flex: 1, minWidth: 0, gap: 2 }}>
|
|
329
|
+
{/* The ellipsized name is the anchor; the nested Tooltip (its children are the
|
|
330
|
+
bubble) reveals the full name when the name is clipped. */}
|
|
331
|
+
<span style={{ display: 'block', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
|
332
|
+
{run.name}
|
|
333
|
+
<Tooltip truncatedOnly>{run.name}</Tooltip>
|
|
334
|
+
</span>
|
|
335
|
+
<div style={{ display: 'flex', alignItems: 'center' }}>
|
|
336
|
+
<span style={{ color: 'var(--text-3)', fontSize: 12, fontVariantNumeric: 'tabular-nums' }}>{run.ranAt}</span>
|
|
337
|
+
<Tag size="small" tone={TONE[run.status]} style={{ marginInlineStart: 'auto' }}>{LABEL[run.status]}</Tag>
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
)
|
|
341
|
+
}}
|
|
342
|
+
/>
|
|
343
|
+
</div>
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
**`renderOption(option, state)`** replaces the built-in `label`, `hint`, and
|
|
347
|
+
`icon` layout for a row. Select still renders the row container, click behavior,
|
|
348
|
+
ARIA attributes, and selection indicator. Set each option's `label` for the
|
|
349
|
+
closed field text.
|
|
350
|
+
|
|
351
|
+
Attach your own fields to each option (`SelectOption` carries an index signature) and
|
|
352
|
+
read them back off `option`; those extra fields are typed `unknown`, so cast once at
|
|
353
|
+
the top. `state` gives you `value` / `selected` / `disabled` for the row. Give your
|
|
354
|
+
root `flex: 1; min-width: 0` so it fills the row (an inner `margin-inline-start: auto`
|
|
355
|
+
right-aligns a trailing element, like the status `Tag` here).
|
|
356
|
+
|
|
357
|
+
`filter` still matches the option's `value`, `label`, and `hint`. Select does
|
|
358
|
+
not highlight matches in custom content because it cannot identify text within
|
|
359
|
+
the returned node. `indicator` and `selection` still provide the selection mark.
|
|
360
|
+
Use `renderIndicator` to replace that mark too; see
|
|
361
|
+
[Custom indicators](#custom-indicators).
|
|
362
|
+
|
|
363
|
+
### Limiting the width
|
|
364
|
+
|
|
365
|
+
```tsx
|
|
366
|
+
// Same ellipsis-ready row for both; it truncates once the menu is capped.
|
|
367
|
+
const renderPath = (o) => (
|
|
368
|
+
<span style={{ display: 'block', flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
|
369
|
+
{o.label}
|
|
370
|
+
<Tooltip truncatedOnly>{o.label}</Tooltip> {/* reveals the full path when clipped */}
|
|
371
|
+
</span>
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
// First: no cap, so the widest row stretches the menu past the trigger.
|
|
375
|
+
<div style={{ width: 220 }}>
|
|
376
|
+
<Select label="No cap · grows to fit" options={paths} value={a} onValueChange={setA} renderOption={renderPath} />
|
|
377
|
+
</div>
|
|
378
|
+
|
|
379
|
+
// Second: cap the popover so the same rows truncate to the trigger width.
|
|
380
|
+
<style>{`.path-capped a-menu::part(menu) { max-width: 220px }`}</style>
|
|
381
|
+
<div className="path-capped" style={{ width: 220 }}>
|
|
382
|
+
<Select label="Capped · ellipsizes" options={paths} value={b} onValueChange={setB} renderOption={renderPath} />
|
|
383
|
+
</div>
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
A menu **grows to fit its widest row**. The trigger width sets the floor; the
|
|
387
|
+
viewport sets the ceiling. Built-in `label`s stay bounded (they wrap), but **custom
|
|
388
|
+
`renderOption` content does whatever your CSS says**, so wide content stretches the
|
|
389
|
+
whole menu. Open both dropdowns: same rows, yet the first grows past its 220px field
|
|
390
|
+
while the second (capped) stays put and ellipsizes.
|
|
391
|
+
|
|
392
|
+
Enforce the limit on the **menu**: cap it with **`::part(menu) { max-width }`**. Only
|
|
393
|
+
then does ellipsis-ready content truncate, so give the row a shrinkable box
|
|
394
|
+
(`min-width: 0`) and single-line ellipsis (`overflow: hidden; text-overflow:
|
|
395
|
+
ellipsis; white-space: nowrap`). Nest a **`Tooltip truncatedOnly`** in that element
|
|
396
|
+
to reveal the full text on hover, only when it's clipped. `min-width: 0` alone won't
|
|
397
|
+
bound it: a shrink-to-fit popover grows to its content's max-content width, so the
|
|
398
|
+
cap is what does the work.
|
|
399
|
+
|
|
400
|
+
### Custom indicators
|
|
401
|
+
|
|
402
|
+
```tsx
|
|
403
|
+
<Select
|
|
404
|
+
label="Log level"
|
|
405
|
+
indicator="check" // keeps the semantics (role + aria-checked)
|
|
406
|
+
options={levels} // { value, label, dot }[]
|
|
407
|
+
value={level}
|
|
408
|
+
onValueChange={setLevel}
|
|
409
|
+
renderIndicator={(state) => {
|
|
410
|
+
const dot = levels.find((l) => l.value === state.value).dot
|
|
411
|
+
return (
|
|
412
|
+
<span style={{
|
|
413
|
+
width: 10, height: 10, borderRadius: '50%', display: 'inline-block',
|
|
414
|
+
boxShadow: `inset 0 0 0 2px ${dot}`,
|
|
415
|
+
background: state.selected ? dot : 'transparent',
|
|
416
|
+
}} />
|
|
417
|
+
)
|
|
418
|
+
}}
|
|
419
|
+
/>
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
**`renderIndicator(state)`** replaces the row's selection **mark** with your own
|
|
423
|
+
node, drawn at the **leading** edge. The row stays the control: keep an `indicator`
|
|
424
|
+
(`'check'` / `'radio'`) or `selection="multiple"` for the `role` + `aria-checked`
|
|
425
|
+
semantics, and only the drawn glyph changes. `state` gives you `value` / `selected` /
|
|
426
|
+
`disabled`, so the mark can reflect selection (here, an outline ring that fills when
|
|
427
|
+
chosen). It composes with `renderOption`, so you can override the content, the mark,
|
|
428
|
+
both, or neither.
|
|
429
|
+
|
|
430
|
+
### Selection summary
|
|
431
|
+
|
|
432
|
+
```tsx
|
|
433
|
+
const teams = ['Engineering', 'Design', 'Operations', 'Sales', 'Support']
|
|
434
|
+
const [a, setA] = useState(['eng', 'design', 'ops'])
|
|
435
|
+
const [b, setB] = useState(['eng', 'design'])
|
|
436
|
+
|
|
437
|
+
// verbose: the count summary lists the picks
|
|
438
|
+
<Select selection="multiple" placeholder="Pick teams" options={teams} value={a} onValueChange={setA} verbose />
|
|
439
|
+
|
|
440
|
+
// renderSummary: build the text yourself
|
|
441
|
+
<Select
|
|
442
|
+
selection="multiple"
|
|
443
|
+
placeholder="Pick teams"
|
|
444
|
+
options={teams}
|
|
445
|
+
value={b}
|
|
446
|
+
onValueChange={setB}
|
|
447
|
+
// one label; a count past that; every option → "All teams"
|
|
448
|
+
renderSummary={(selected) =>
|
|
449
|
+
selected.length === teams.length
|
|
450
|
+
? 'All teams'
|
|
451
|
+
: selected.length > 1
|
|
452
|
+
? `${selected.length} teams`
|
|
453
|
+
: undefined // fall back to the single label
|
|
454
|
+
}
|
|
455
|
+
/>
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
**`renderSummary(selected)`** builds the multi-select field text in place of the
|
|
459
|
+
built-in "`All` / one label / `N selected`". It runs while something is selected — an
|
|
460
|
+
empty selection still shows the `placeholder` — and takes the resolved `selected`
|
|
461
|
+
options (`selected.length` is the count). Return `undefined` for any case you'd rather
|
|
462
|
+
leave to the default, as above for the single-label case.
|
|
463
|
+
|
|
464
|
+
Return a **string**: it flows into the same read-only field the default uses, so a
|
|
465
|
+
long summary **ellipsizes at the field's width** the way a long value does
|
|
466
|
+
(`Engineering, Design, … `). For rich content — chips, several nodes — reach for
|
|
467
|
+
`renderTrigger` below, which replaces the whole field.
|
|
468
|
+
|
|
469
|
+
For the common case of just listing the picks, skip `renderSummary` and pass
|
|
470
|
+
**`verbose`**: the count summary becomes `3 selected: A, B, C` (labels comma-joined),
|
|
471
|
+
ellipsizing at the field width like any long value. `renderSummary` overrides it.
|
|
472
|
+
|
|
473
|
+
### Custom trigger
|
|
474
|
+
|
|
475
|
+
```tsx
|
|
476
|
+
const [value, setValue] = useState(['stream', 'message'])
|
|
477
|
+
|
|
478
|
+
<Select
|
|
479
|
+
selection="multiple"
|
|
480
|
+
options={fields}
|
|
481
|
+
value={value}
|
|
482
|
+
onValueChange={setValue}
|
|
483
|
+
renderTrigger={({ open, selected }) => (
|
|
484
|
+
// Return one focusable element. It supplies the required ARIA attributes.
|
|
485
|
+
<Button icon="filter" label="Filter" priority="secondary"
|
|
486
|
+
aria-haspopup="menu" aria-expanded={open ? 'true' : 'false'}>
|
|
487
|
+
{selected.length > 0 && (
|
|
488
|
+
<Tag size="small" priority="primary" tone="brand">
|
|
489
|
+
{selected.length === fields.length ? 'All' : selected.length}
|
|
490
|
+
</Tag>
|
|
491
|
+
)}
|
|
492
|
+
</Button>
|
|
493
|
+
)}
|
|
494
|
+
/>
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
```tsx
|
|
498
|
+
const columns = ['Name', 'Status', 'Owner', 'Created', 'Duration', 'Environment', 'Branch', 'Commit']
|
|
499
|
+
const [hidden, setHidden] = useState([])
|
|
500
|
+
|
|
501
|
+
<Select
|
|
502
|
+
selection="multiple"
|
|
503
|
+
options={columns}
|
|
504
|
+
value={hidden}
|
|
505
|
+
onValueChange={setHidden}
|
|
506
|
+
// selecting a column hides it: eye by default, eye-closed once hidden
|
|
507
|
+
renderIndicator={({ selected }) => (
|
|
508
|
+
<Icon shape={selected ? 'eye-closed' : 'eye'} />
|
|
509
|
+
)}
|
|
510
|
+
renderTrigger={({ open, selected }) => (
|
|
511
|
+
<Button icon="columns-3-cog" priority="tertiary" aria-label="Configure columns"
|
|
512
|
+
aria-haspopup="menu" aria-expanded={open ? 'true' : 'false'}>
|
|
513
|
+
{selected.length > 0 && (
|
|
514
|
+
<Tag size="small" priority="secondary" tone="neutral">{selected.length}</Tag>
|
|
515
|
+
)}
|
|
516
|
+
</Button>
|
|
517
|
+
)}
|
|
518
|
+
/>
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
**`renderTrigger(state)`** replaces the default field. Return exactly one
|
|
522
|
+
focusable element, such as the Anta `Button` in this example. Select positions
|
|
523
|
+
the menu relative to that element and opens it when it is clicked. A fragment,
|
|
524
|
+
multiple sibling elements, or a non-focusable wrapper prevents Select from finding
|
|
525
|
+
the trigger. It logs a console warning when that happens. Add
|
|
526
|
+
`aria-haspopup="menu"` and `aria-expanded={state.open}` to the element.
|
|
527
|
+
|
|
528
|
+
`state` includes `open`, `value`, `selected`, `disabled`, and `icon`. `selected`
|
|
529
|
+
is the resolved option list, so `selected.length` is the multi-select count. The
|
|
530
|
+
default field props, including `label`, `size`, and `status`, do not apply to a
|
|
531
|
+
custom trigger. `state.icon` contains the Select `icon` prop so the replacement
|
|
532
|
+
trigger can display the same icon.
|
|
533
|
+
|
|
534
|
+
The second example uses a custom trigger and `renderIndicator` for a **column
|
|
535
|
+
visibility** control. The options are column names. In this example, a selected
|
|
536
|
+
option means that its column is hidden: the indicator changes from `eye` to
|
|
537
|
+
`eye-closed`. The Tag uses `selected.length`, so it appears only after a column
|
|
538
|
+
is hidden.
|
|
539
|
+
|
|
540
|
+
### Empty state
|
|
541
|
+
|
|
542
|
+
```tsx
|
|
543
|
+
const [options, setOptions] = useState(['bug', 'feature', 'chore', 'docs'])
|
|
544
|
+
const [value, setValue] = useState()
|
|
545
|
+
const [loading, setLoading] = useState(false) // application fetch state
|
|
546
|
+
|
|
547
|
+
<Select
|
|
548
|
+
label="Tag"
|
|
549
|
+
filter
|
|
550
|
+
placeholder="Filter or create…"
|
|
551
|
+
options={loading ? [] : options}
|
|
552
|
+
value={value}
|
|
553
|
+
onValueChange={setValue}
|
|
554
|
+
renderEmpty={({ query }) =>
|
|
555
|
+
loading ? (
|
|
556
|
+
<MenuSeparator>Loading…</MenuSeparator>
|
|
557
|
+
) : (
|
|
558
|
+
<>
|
|
559
|
+
<MenuSeparator>No options are matching the filter</MenuSeparator>
|
|
560
|
+
<MenuItem
|
|
561
|
+
icon="plus"
|
|
562
|
+
label={`Create "${query}"`}
|
|
563
|
+
onSelect={() => { setOptions((o) => [...o, query]); setValue(query) }}
|
|
564
|
+
/>
|
|
565
|
+
</>
|
|
566
|
+
)
|
|
567
|
+
}
|
|
568
|
+
/>
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
**`renderEmpty({ query })`** renders content when the option list is empty after
|
|
572
|
+
filtering. It can return a "No results" message, a loading indicator, or a row
|
|
573
|
+
that creates an option from `query`. A non-empty `query` means that filtering
|
|
574
|
+
removed every option; an empty `query` means that no options were supplied.
|
|
575
|
+
|
|
576
|
+
Select has no built-in empty message. If `renderEmpty` is omitted, an empty list
|
|
577
|
+
has no content. Loading also belongs to application state, so return a
|
|
578
|
+
`'Loading…'` branch when that state is true.
|
|
579
|
+
|
|
580
|
+
The returned content replaces the option rows. A `MenuSeparator` with text is a
|
|
581
|
+
muted caption and an `aria-live="polite"` region. Return a `MenuItem`, such as
|
|
582
|
+
the `Create "…"` row, when the content must be selectable by keyboard.
|
|
583
|
+
|
|
584
|
+
### Groups and submenus
|
|
585
|
+
|
|
586
|
+
```tsx
|
|
587
|
+
// The same tree drives both modes — selection is global, so `selection` is the
|
|
588
|
+
// only difference between the two selects below.
|
|
589
|
+
const fields = [
|
|
590
|
+
'output_text', // a plain option
|
|
591
|
+
{ label: 'Log', options: ['stream', 'message'] }, // an inline group
|
|
592
|
+
{
|
|
593
|
+
label: 'Metadata', // a submenu (flyout)
|
|
594
|
+
icon: 'braces',
|
|
595
|
+
submenu: [
|
|
596
|
+
'metadata.host',
|
|
597
|
+
{ label: 'Tags', options: ['tag.env', 'tag.team'] }, // group inside submenu
|
|
598
|
+
{ label: 'Custom', submenu: ['custom.a', 'custom.b'] }, // submenu inside submenu
|
|
599
|
+
],
|
|
600
|
+
},
|
|
601
|
+
{ label: 'Time', submenu: ['vtime'], disabled: true }, // disabled → cascades
|
|
602
|
+
]
|
|
603
|
+
|
|
604
|
+
<Select label="Field · single" filter options={fields} value={one} onValueChange={setOne} />
|
|
605
|
+
|
|
606
|
+
<Select label="Fields · multiple" selection="multiple" filter options={fields} value={many} onValueChange={setMany} />
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
An `options` entry is a plain option, a **group** (`{ label, options }`, an inline
|
|
610
|
+
titled section), or a **submenu** (`{ label, submenu }`, a flyout). They nest and mix
|
|
611
|
+
freely. Discriminated by shape — an `options` array is a group, a `submenu` array is a
|
|
612
|
+
submenu, everything else is an option (so those two keys are reserved on an option).
|
|
613
|
+
|
|
614
|
+
**Selection stays global.** The Select's `selection` (single / multiple) applies to
|
|
615
|
+
every leaf wherever it sits; group headings and submenu parents organize but aren't
|
|
616
|
+
selectable. One `value`, one `onValueChange`, one count — unchanged. The two selects
|
|
617
|
+
above share the exact same `fields` tree and differ only by `selection`. `disabled` on
|
|
618
|
+
a group or submenu cascades to all its descendants. Because selection is global, a
|
|
619
|
+
leaf's `value` must be **unique across the whole tree** — the same value under two
|
|
620
|
+
sections is one pick (dev builds warn).
|
|
621
|
+
|
|
622
|
+
**A query flattens the tree.** With `filter`, typing collapses submenus into inline
|
|
623
|
+
groups (their label becomes the heading) and drops empty ones, so results read as one
|
|
624
|
+
scannable list instead of hiding matches behind flyouts.
|
|
625
|
+
|
|
626
|
+
### Props
|
|
627
|
+
|
|
628
|
+
| Prop | Type | Default | Description |
|
|
629
|
+
|------|------|---------|-------------|
|
|
630
|
+
| `options` | SelectItem[] | — | The options to choose from — bare strings, `SelectOption` objects, `SelectGroup`s
|
|
631
|
+
(inline titled sections), or `SelectSubmenu`s (flyout branches). Groups and
|
|
632
|
+
submenus nest and mix with plain options. Selection stays global (one `value`,
|
|
633
|
+
leaf options only); a filter query flattens the tree into grouped results.
|
|
634
|
+
|
|
635
|
+
`Select` infers its value type `V` from these options: `{ value: 365 }` makes
|
|
636
|
+
`onValueChange` report `number`. A mix of value types widens `V` to the union.
|
|
637
|
+
|
|
638
|
+
Each leaf `value` is the option's identity and must be **unique across the whole
|
|
639
|
+
tree** (selection is value-keyed, so a value repeated in two sections is one logical
|
|
640
|
+
pick: both rows toggle together, the trigger resolves to the last). Values that
|
|
641
|
+
stringify alike (`365` and `"365"`) also collide as row keys; dev builds
|
|
642
|
+
`console.warn` on either. |
|
|
643
|
+
| `clearable?` | boolean | — | Add a "Clear" row pinned in the menu **footer** that empties the selection
|
|
644
|
+
(single → none, multiple → `[]`). Shown only while something is selected, so
|
|
645
|
+
it never scrolls away in a long or filtered list. |
|
|
646
|
+
| `clearLabel?` | string | Clear | Label for the `clearable` footer row. |
|
|
647
|
+
| `defaultValue?` | V \| V[] | — | Initial selected option value for uncontrolled use. |
|
|
648
|
+
| `disabled?` | boolean | — | Disable the whole select. |
|
|
649
|
+
| `filter?` | boolean \| (option, query) => boolean | — | Add a search field at the top of the menu that filters the options as you
|
|
650
|
+
type. `true` uses the built-in matcher — a case-insensitive substring of the
|
|
651
|
+
option's **value / label / hint**. Pass a **function** `(option, query) =>
|
|
652
|
+
boolean` for custom matching (called per option; return `true` to keep it). |
|
|
653
|
+
| `hint?` | string | — | Helper text under the field (Input's `hint`). |
|
|
654
|
+
| `icon?` | IconShape | — | Leading icon shown at the left of the field (the default trigger's `Input`
|
|
655
|
+
`leading` slot). With a custom `renderTrigger`, it's passed through as
|
|
656
|
+
`state.icon` instead — the consumer places it. |
|
|
657
|
+
| `indicator?` | 'none' \| 'check' \| 'radio' | none | The per-row mark for **single**-select: `'none'` (a tint-only highlight),
|
|
658
|
+
`'check'` (a trailing checkmark on the selected row, keeping the tint — the
|
|
659
|
+
canonical Select look), or `'radio'` (a leading radio on every row).
|
|
660
|
+
Multi-select always uses checkboxes. |
|
|
661
|
+
| `label?` | string | — | Field label, above the trigger (Input's `label`). |
|
|
662
|
+
| `leading?` | ReactNode | — | Content for the default trigger's `leading` slot, such as a key prefix
|
|
663
|
+
before the value. It replaces the icon derived from `icon`. Include an
|
|
664
|
+
`<Icon>` in this content when both are needed. Ignored by `renderTrigger`. |
|
|
665
|
+
| `offset?` | number | 4 | Gap in pixels between the trigger and the options menu. |
|
|
666
|
+
| `onValueChange?` | (value, attrs) => void | — | Fires after the selection changes, with the new value and a
|
|
667
|
+
`{ value, option }` snapshot. Select has no discrete element state, so
|
|
668
|
+
there is no cancelable `onStateChange` (see the Input event-model note). |
|
|
669
|
+
| `placeholder?` | string | — | Text shown when nothing is selected. |
|
|
670
|
+
| `placement?` | 'left' \| 'right' \| 'bottom' \| 'top' \| 'bottom-start' \| 'bottom-end' \| 'top-start' \| 'top-end' \| 'right-start' \| 'right-end' \| 'left-start' \| 'left-end' | bottom-start | Preferred placement of the options menu relative to the trigger. The menu
|
|
671
|
+
auto-flips vertically and clamps horizontally when needed. |
|
|
672
|
+
| `renderEmpty?` | (state) => ReactNode | — | Render content in the menu body when the (filtered) option list is empty —
|
|
673
|
+
a "no results" message, a loading indicator (gated on your own external
|
|
674
|
+
loading state), or a "create from the query" row. Receives an `EmptyState`
|
|
675
|
+
(`query`, trimmed). There is no built-in empty message: when omitted, an empty
|
|
676
|
+
list renders nothing. Whatever you return goes where the option rows would —
|
|
677
|
+
a plain node is inert; return a `MenuItem` (e.g. a "Create" row) to make it
|
|
678
|
+
focusable and selectable. |
|
|
679
|
+
| `renderIndicator?` | (state) => ReactNode | — | Replace each row's selection **mark** with your own node, drawn at the
|
|
680
|
+
leading edge. The row stays the control (`role` + `aria-checked` from
|
|
681
|
+
`indicator` / `selection`); only the drawn mark changes, so pair it with an
|
|
682
|
+
`indicator` (`'check'` / `'radio'`) or `selection="multiple"` for the
|
|
683
|
+
semantics. Composes with `renderOption`. |
|
|
684
|
+
| `renderOption?` | (option, state) => ReactNode | — | Replaces the built-in `label`, `hint`, and `icon` layout for each option row.
|
|
685
|
+
Select still supplies the row container, click handling, ARIA attributes, and
|
|
686
|
+
selection indicator. Read extra option fields through `SelectOption`'s index
|
|
687
|
+
signature. `state` contains `value`, `selected`, and `disabled`. Filtering
|
|
688
|
+
still matches the option's `value`, `label`, and `hint`, but Select cannot
|
|
689
|
+
highlight matches within the returned content. |
|
|
690
|
+
| `renderSummary?` | (selected) => string \| undefined | — | `multiple` only: build the trigger's selection summary text yourself,
|
|
691
|
+
replacing the built-in "`All` / one label / `N selected`" logic. Receives
|
|
692
|
+
the resolved selected options (`selected.length` is the count) and runs only
|
|
693
|
+
while something is selected — an empty selection still shows the
|
|
694
|
+
`placeholder`. Return a **string**: it flows into the default trigger's
|
|
695
|
+
read-only field, so a long summary ellipsizes at the field's width just
|
|
696
|
+
like a long value (`Engineering, Design, … `). Return `undefined` to fall
|
|
697
|
+
back to the default for that case (e.g. customize only the count, keeping
|
|
698
|
+
the single-label case built-in). For rich content (chips, multiple nodes)
|
|
699
|
+
use `renderTrigger`, which replaces the whole field. |
|
|
700
|
+
| `renderTrigger?` | (state) => ReactNode | — | Replaces the default field with a trigger returned from this function.
|
|
701
|
+
Receives `open`, `value`, `selected`, `disabled`, and `icon`. Return exactly
|
|
702
|
+
one focusable element, such as an Anta `Button`. The menu is positioned
|
|
703
|
+
relative to that element and opens when it is clicked. Do not return a
|
|
704
|
+
fragment, multiple siblings, or a non-focusable wrapper. Add
|
|
705
|
+
`aria-haspopup="menu"` and `aria-expanded={state.open}` to the element.
|
|
706
|
+
Field props (`label`, `hint`, `size`, `status`, `placeholder`, and `round`) and
|
|
707
|
+
`className` / `style` apply only to the default field. Add styling and
|
|
708
|
+
attributes to the returned element instead. |
|
|
709
|
+
| `round?` | boolean \| number \| string | — | Round the field corners — `true` for fully round, or a number / CSS length. |
|
|
710
|
+
| `selectAll?` | boolean | true | `multiple` only: shows a "Select all" row that toggles every enabled option,
|
|
711
|
+
or only the visible options when a filter query is active. Its checkbox is
|
|
712
|
+
mixed when some options are selected. It is on by default. Set it to `false`
|
|
713
|
+
to remove the row and the Alt/Option-click shortcut that selects only one row. |
|
|
714
|
+
| `selectAllLabel?` | string | Select all | Label for the `selectAll` row. |
|
|
715
|
+
| `selection?` | 'single' \| 'multiple' | single | Selection mode. `'single'` (the default) keeps `value` a single value and
|
|
716
|
+
closes the menu on pick. Switch to `'multiple'` for checkboxes + an
|
|
717
|
+
array value. |
|
|
718
|
+
| `size?` | 'small' \| 'medium' \| 'large' | medium | Field size. |
|
|
719
|
+
| `status?` | 'neutral' \| 'brand' \| 'info' \| 'success' \| 'warning' \| 'critical' | neutral | Validation/feedback tone for the field (Input's `status`). |
|
|
720
|
+
| `statusIcon?` | (string & {}) \| false \| IconShape | — | Glyph shown before the `hint` when `status` is set (Input's `statusIcon`).
|
|
721
|
+
Each status has a default; pass a shape to override, or `false` to drop it. |
|
|
722
|
+
| `toneSelected?` | 'neutral' \| 'brand' \| 'info' \| 'success' \| 'warning' \| 'critical' \| (string & {}) | — | Tone applied to the **selected** row(s) — the whole row takes this tone
|
|
723
|
+
(label, icon, indicator, and the background tint), like passing `tone` to just
|
|
724
|
+
the chosen option. A named tone or a custom CSS color. Most visible with the
|
|
725
|
+
tint-based marks (`indicator` `'none'` / `'check'`); with `'radio'` /
|
|
726
|
+
`'checkbox'` it tones the label + indicator (those modes have no row tint). |
|
|
727
|
+
| `value?` | V \| V[] | — | Controlled value: the selected option's `value`. Update it through
|
|
728
|
+
`onValueChange`. Leave it undefined for uncontrolled use. |
|
|
729
|
+
| `verbose?` | boolean | — | `multiple` only: spell the picks out in the count summary — `3 selected:
|
|
730
|
+
A, B, C` (labels comma-joined) in place of the bare `3 selected`. Applies
|
|
731
|
+
to the multi-count case only: `All` stays `All`, a single pick stays its
|
|
732
|
+
own label, and an empty selection stays the `placeholder`. The list flows
|
|
733
|
+
into the read-only field, so it ellipsizes at the field's width when long
|
|
734
|
+
(`3 selected: Engineering, Des… `). `renderSummary` overrides this. |
|
|
735
|
+
|
|
736
|
+
### The `SelectItem` type
|
|
737
|
+
|
|
738
|
+
`options` takes a `SelectItem[]`. Each entry is one of four shapes, discriminated by
|
|
739
|
+
its keys — an `options` array is a group, a `submenu` array is a submenu, and a bare
|
|
740
|
+
string or a plain object is a single option:
|
|
741
|
+
|
|
742
|
+
| Shape | Renders as |
|
|
743
|
+
|---|---|
|
|
744
|
+
| `string` | One option; shorthand for `{ value: s, label: s }`. |
|
|
745
|
+
| `SelectOption` | One option with a label, hint, icon, tone, tooltip, or your own fields. |
|
|
746
|
+
| `SelectGroup` | A titled group of items, **inline** under a heading. |
|
|
747
|
+
| `SelectSubmenu` | A titled branch of items behind a **flyout**. |
|
|
748
|
+
|
|
749
|
+
`value` is unique across the whole tree — selection is value-keyed and global, so a
|
|
750
|
+
group or submenu only organizes (its heading isn't selectable), and a `disabled`
|
|
751
|
+
group or submenu cascades to every descendant.
|
|
752
|
+
|
|
753
|
+
The smallest form is three options, each carrying only a `value` (a bare string is
|
|
754
|
+
the same thing):
|
|
755
|
+
|
|
756
|
+
```tsx
|
|
757
|
+
<Select
|
|
758
|
+
label="Priority"
|
|
759
|
+
options={[
|
|
760
|
+
{ value: 'low' },
|
|
761
|
+
{ value: 'medium' },
|
|
762
|
+
{ value: 'high' },
|
|
763
|
+
]}
|
|
764
|
+
/>
|
|
765
|
+
// equivalent: options={['low', 'medium', 'high']}
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
**`SelectOption`** — one selectable row.
|
|
769
|
+
|
|
770
|
+
| Field | Type | Description |
|
|
771
|
+
|---|---|---|
|
|
772
|
+
| `value` | `OptionValue` | The option's identity: `string`, `number`, or `boolean` (`Select` infers the type from your `options`, compared with `===`). What `value` / `defaultValue` name and `onValueChange` reports. Required, unique across the tree. See [Value and changes](#value-and-changes) for non-string values and rich data. |
|
|
773
|
+
| `label` | `string` | Visible label. Defaults to `value`. |
|
|
774
|
+
| `hint` | `string` | Secondary line under the label. |
|
|
775
|
+
| `icon` | `IconShape` | Leading icon, after the selection indicator. |
|
|
776
|
+
| `disabled` | `boolean` | Disable just this option. |
|
|
777
|
+
| `tone` | `string` | The row's color across label, icon, hint, selected tint, and indicator. A named tone (`neutral` / `brand` / `info` / `success` / `warning` / `critical`) or any CSS color. |
|
|
778
|
+
| `tooltip` | `React.ReactNode` | Row tooltip. In a `multiple` select with `selectAll`, a row with no `tooltip` shows the default "select only this" (Alt/⌥-click) hint; set it to override, or `''` to suppress. |
|
|
779
|
+
| `[key: string]` | `unknown` | Your own data — attach anything and read it back in `renderOption`. |
|
|
780
|
+
|
|
781
|
+
**`SelectGroup`** — a heading with items rendered inline beneath it.
|
|
782
|
+
|
|
783
|
+
| Field | Type | Description |
|
|
784
|
+
|---|---|---|
|
|
785
|
+
| `label` | `string` | The section heading (non-interactive). |
|
|
786
|
+
| `options` | `SelectItem[]` | The grouped items — options, or nested groups / submenus. |
|
|
787
|
+
| `disabled` | `boolean` | Disable the whole group (cascades to all descendants). |
|
|
788
|
+
|
|
789
|
+
**`SelectSubmenu`** — a parent row whose items live behind a flyout.
|
|
790
|
+
|
|
791
|
+
| Field | Type | Description |
|
|
792
|
+
|---|---|---|
|
|
793
|
+
| `label` | `string` | The parent row's label, and the group heading when a filter query flattens the tree. |
|
|
794
|
+
| `icon` | `IconShape` | Leading icon on the parent row. |
|
|
795
|
+
| `submenu` | `SelectItem[]` | The branch's items — options, or nested groups / submenus. |
|
|
796
|
+
| `disabled` | `boolean` | Disable the whole branch (cascades to all descendants). |
|
|
797
|
+
|
|
798
|
+
All four shapes mix in one `options` array — a bare string, a full `SelectOption`, an
|
|
799
|
+
inline `SelectGroup`, and a flyout `SelectSubmenu` (whose items are themselves
|
|
800
|
+
`SelectItem`s, so groups and submenus nest freely):
|
|
801
|
+
|
|
802
|
+
```tsx
|
|
803
|
+
<Select
|
|
804
|
+
label="Field"
|
|
805
|
+
options={[
|
|
806
|
+
'output_text', // string shorthand
|
|
807
|
+
{ value: 'stream', label: 'Stream', hint: 'Log level', icon: 'braces' }, // SelectOption
|
|
808
|
+
{ // SelectGroup — inline heading
|
|
809
|
+
label: 'Metadata',
|
|
810
|
+
options: [
|
|
811
|
+
{ value: 'container', icon: 'cube' },
|
|
812
|
+
{ value: 'file_path', icon: 'file' },
|
|
813
|
+
],
|
|
814
|
+
},
|
|
815
|
+
{ // SelectSubmenu — flyout branch
|
|
816
|
+
label: 'Advanced',
|
|
817
|
+
icon: 'folder-close',
|
|
818
|
+
submenu: [
|
|
819
|
+
'raw_bytes',
|
|
820
|
+
{ value: 'custom', hint: 'Your own field path', icon: 'asterisk', disabled: true },
|
|
821
|
+
],
|
|
822
|
+
},
|
|
823
|
+
]}
|
|
824
|
+
/>
|
|
825
|
+
```
|
|
826
|
+
|
|
827
|
+
Use the web component directly when you are not using React or Preact and a native control does not fit.
|
|
828
|
+
|
|
829
|
+
`Select` has no host element. Compose a read-only `<a-input>` trigger with an
|
|
830
|
+
`<a-menu>` of options. The menu owns opening and dismissal; the short controller
|
|
831
|
+
reflects a picked option into the field and selected row.
|
|
832
|
+
|
|
833
|
+
```html
|
|
834
|
+
<div data-anta-composition="select" style="display: grid; gap: 4px; width: 280px">
|
|
835
|
+
<a-input readonly value="stream">
|
|
836
|
+
<span slot="label">Output</span>
|
|
837
|
+
<span slot="trailing"><a-icon shape="chevron-down" aria-hidden="true"></a-icon></span>
|
|
838
|
+
</a-input>
|
|
839
|
+
<a-menu role="menu">
|
|
840
|
+
<a-menu-item role="menuitem" tabindex="0" value="output_text"><a-menu-item-label>output_text</a-menu-item-label></a-menu-item>
|
|
841
|
+
<a-menu-item role="menuitem" tabindex="0" value="stream" selected><a-menu-item-label>stream</a-menu-item-label></a-menu-item>
|
|
842
|
+
</a-menu>
|
|
843
|
+
</div>
|
|
844
|
+
<script type="module">
|
|
845
|
+
import '@antadesign/anta/elements'
|
|
846
|
+
|
|
847
|
+
const root = document.querySelector('[data-anta-composition="select"]')
|
|
848
|
+
const field = root.querySelector('a-input')
|
|
849
|
+
const menu = root.querySelector('a-menu')
|
|
850
|
+
|
|
851
|
+
menu.addEventListener('click', (event) => {
|
|
852
|
+
const item = event.target instanceof Element ? event.target.closest('a-menu-item') : null
|
|
853
|
+
if (!item || item.closest('a-menu') !== menu) return
|
|
854
|
+
menu.querySelectorAll(':scope > a-menu-item').forEach((option) => {
|
|
855
|
+
const selected = option === item
|
|
856
|
+
option.toggleAttribute('selected', selected)
|
|
857
|
+
})
|
|
858
|
+
field.value = item.querySelector('a-menu-item-label').textContent.trim()
|
|
859
|
+
})
|
|
860
|
+
</script>
|
|
861
|
+
```
|
|
862
|
+
|
|
863
|
+
### Native HTML select
|
|
864
|
+
|
|
865
|
+
When a native single-value select is enough, add `data-anta` to `<select>`. The
|
|
866
|
+
modern customizable-select model keeps native selection, keyboard behavior, and
|
|
867
|
+
form submission while exposing its trigger `button`, `selectedcontent`, options,
|
|
868
|
+
and optgroups for styling. Anta styles every supported part; browsers without that
|
|
869
|
+
model retain their regular native select. Use `Select` for filtering, multiple
|
|
870
|
+
selection, nested menus, or custom option coordination.
|
|
871
|
+
|
|
872
|
+
Use `data-anta-size="small"` or `"large"` for the field size. Native selects
|
|
873
|
+
continue to use their standard `size` attribute, and `round` gives the trigger matching rounded
|
|
874
|
+
corners.
|
|
875
|
+
|
|
876
|
+
```html
|
|
877
|
+
<select data-anta data-anta-size="small" round name="output">
|
|
878
|
+
<!-- The native trigger; selectedcontent mirrors the chosen option. -->
|
|
879
|
+
<button>
|
|
880
|
+
<selectedcontent></selectedcontent>
|
|
881
|
+
<a-icon shape="chevron-down" aria-hidden="true"></a-icon>
|
|
882
|
+
</button>
|
|
883
|
+
|
|
884
|
+
<option value="" selected disabled>Choose an output</option>
|
|
885
|
+
<option value="stream">Stream</option>
|
|
886
|
+
<option value="output_text">Output text</option>
|
|
887
|
+
<optgroup label="Metadata">
|
|
888
|
+
<option value="container">Container</option>
|
|
889
|
+
<option value="file_path">File path</option>
|
|
890
|
+
</optgroup>
|
|
891
|
+
</select>
|
|
892
|
+
```
|
|
893
|
+
|
|
894
|
+
Select is an [Input](./input.md) plus a [Menu](./menu.md), so it
|
|
895
|
+
inherits both surfaces' hooks. Reach for props first, then plain CSS or `::part` for
|
|
896
|
+
the rest. Don't override an element's internal `--*` output tokens.
|
|
897
|
+
|
|
898
|
+
**Selection color** routes through the props: a per-option `tone`, or `toneSelected`
|
|
899
|
+
for the chosen row(s). Both take a named tone or any CSS color (a custom color keeps
|
|
900
|
+
its hue, with lightness pinned to the brand text).
|
|
901
|
+
|
|
902
|
+
**The field** is styled through Input's props (`size`, `status`, `round`) and its
|
|
903
|
+
`::part`s; set its width with `style` / `className` (forwarded to the field). **The
|
|
904
|
+
popover** takes Menu's `::part(menu)`.
|
|
905
|
+
|
|
906
|
+
```tsx
|
|
907
|
+
// Tone the selection; give the trigger a width.
|
|
908
|
+
<Select toneSelected="brand" style={{ width: '220px' }} options={OPTIONS} />
|
|
909
|
+
```
|
|
910
|
+
|
|
911
|
+
**Borderless trigger.** The field's border is an inset `box-shadow`, so you can drop
|
|
912
|
+
it *at rest* and let the standard hover / focus shadow return on its own; no need to
|
|
913
|
+
re-declare it. Pair that with hiding the chevron for a field that reads as plain text
|
|
914
|
+
until you reach for it. Hide the chevron by targeting the trailing `a-icon` (its slot
|
|
915
|
+
wrapper is inline-`display:contents`, so hit the icon itself).
|
|
916
|
+
|
|
917
|
+
```css
|
|
918
|
+
/* Drop the resting border-shadow; the standard hover / focus shadow still shows. */
|
|
919
|
+
.ghost-select a-input:not(:hover):not(:focus-within)::part(field) {
|
|
920
|
+
box-shadow: none;
|
|
921
|
+
}
|
|
922
|
+
/* Center the chosen label in the field. */
|
|
923
|
+
.ghost-select a-input::part(input) { text-align: center; }
|
|
924
|
+
/* The chevron's slot wrapper is inline display:contents; hide the icon itself. */
|
|
925
|
+
.ghost-select a-input [slot="trailing"] a-icon { display: none; }
|
|
926
|
+
```
|