@antadesign/anta 0.3.17 → 0.3.19
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/components/Input.d.ts +4 -1
- package/dist/components/Input.js +2 -0
- 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/Loader.d.ts +39 -0
- package/dist/components/Loader.js +33 -0
- package/dist/components/Menu.d.ts +5 -1
- package/dist/components/Menu.js +3 -2
- package/dist/components/Progress.d.ts +17 -8
- package/dist/components/Progress.js +12 -9
- package/dist/components/Select.d.ts +1 -0
- package/dist/components/Select.js +9 -13
- package/dist/components/SelectFaceted.d.ts +1 -0
- package/dist/components/SelectFaceted.js +29 -23
- package/dist/components/select-parts.css +1 -0
- package/dist/elements/a-button.css +1 -1
- package/dist/elements/a-icon.d.ts +1 -0
- package/dist/elements/a-icon.js +1 -0
- package/dist/elements/a-input.css +1 -1
- package/dist/elements/a-loader.css +1 -0
- package/dist/elements/a-loader.d.ts +12 -0
- package/dist/elements/a-loader.js +15 -0
- package/dist/elements/a-menu.js +12 -0
- package/dist/elements/a-progress.css +1 -1
- package/dist/elements/a-progress.js +32 -2
- 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/index.d.ts +1 -0
- package/dist/elements/index.js +3 -0
- package/dist/general_types.d.ts +27 -2
- package/dist/index.d.ts +6 -1
- package/dist/index.js +2 -0
- package/dist/jsx-runtime.d.ts +11 -1
- package/dist/reset.css +1 -1
- package/dist/theme-anta.css +1 -1
- package/docs/accessibility.md +34 -0
- package/docs/changelog.md +551 -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 +322 -0
- package/docs/components/checkbox.md +297 -0
- package/docs/components/dialog.md +314 -0
- package/docs/components/expander.md +529 -0
- package/docs/components/icon.md +155 -0
- package/docs/components/input-autocomplete.md +101 -0
- package/docs/components/input-date.md +406 -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 +447 -0
- package/docs/components/progress.md +182 -0
- package/docs/components/radio.md +369 -0
- package/docs/components/select-faceted.md +456 -0
- package/docs/components/select.md +931 -0
- package/docs/components/slider.md +142 -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 +22 -0
- package/docs/index.md +52 -0
- package/docs/install-config.md +240 -0
- package/docs/normalization.md +105 -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,456 @@
|
|
|
1
|
+
# SelectFaceted
|
|
2
|
+
|
|
3
|
+
A faceted filter: one trigger opens a menu of **facets** (dimensions), each a
|
|
4
|
+
submenu flyout with its own editor. The value is a `Record<facetKey, value>`, so
|
|
5
|
+
the same option value under two facets — assignee "Alice" and owner "Alice" —
|
|
6
|
+
stays distinct. Each facet declares a `kind`: `single`, `multiple` (with a
|
|
7
|
+
Select all row by default; `selectAll: false` drops it), `text` (free-form
|
|
8
|
+
substring, applied on Enter / blur), or `custom` (bring your own editor and
|
|
9
|
+
value, including an object).
|
|
10
|
+
|
|
11
|
+
It's a **composed component** built from [Menu](./menu.md), [Select](./select.md)'s
|
|
12
|
+
option model, [Input](./input.md), and [Tag](./tag.md) — no new element. Controlled via
|
|
13
|
+
`value`, uncontrolled via `defaultValue`; changes arrive through one
|
|
14
|
+
**`onValueChange`**.
|
|
15
|
+
|
|
16
|
+
Pass `placement` to control where the root facet menu opens relative to its
|
|
17
|
+
trigger. It uses the same values as `Menu` and still flips or clamps when space
|
|
18
|
+
is limited. `offset` sets the gap in pixels between the trigger and the menu:
|
|
19
|
+
|
|
20
|
+
```tsx
|
|
21
|
+
<SelectFaceted placement="top-end" offset={8} facets={FACETS} />
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
const FACETS = [
|
|
26
|
+
{ key: 'assignee', label: 'Assignee', kind: 'multiple', filter: true, options: people },
|
|
27
|
+
{ key: 'owner', label: 'Owner', kind: 'single', filter: true, options: people },
|
|
28
|
+
{ key: 'status', label: 'Status', kind: 'single', options: [{ value: 'open', label: 'Open' }, /* … */] },
|
|
29
|
+
{ key: 'title', label: 'Title contains', kind: 'text' },
|
|
30
|
+
{ key: 'duration', label: 'Min duration', kind: 'custom', summary: (v) => `≥ ${v.min}s`, render: /* … */ },
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
const isSet = (v) => !(v == null || v === '' || (Array.isArray(v) && v.length === 0))
|
|
34
|
+
|
|
35
|
+
function Demo() {
|
|
36
|
+
const [value, setValue] = useState({ assignee: ['Alice Nguyen'], status: 'open', title: 'crash' })
|
|
37
|
+
const setFacet = (key, v) =>
|
|
38
|
+
setValue((p) => (isSet(v) ? { ...p, [key]: v } : (({ [key]: _, ...rest }) => rest)(p)))
|
|
39
|
+
|
|
40
|
+
// Keep a text/custom chip mounted while it's focused, even when emptied — so
|
|
41
|
+
// backspacing to empty doesn't yank the field (only Clear or blur-when-empty does).
|
|
42
|
+
const [focusedKey, setFocusedKey] = useState(null)
|
|
43
|
+
const active = FACETS.filter((f) => isSet(value[f.key]) || f.key === focusedKey)
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div style={{
|
|
47
|
+
display: 'flex',
|
|
48
|
+
flexDirection: 'row',
|
|
49
|
+
flexWrap: 'wrap',
|
|
50
|
+
gap: 12,
|
|
51
|
+
alignItems: 'center',
|
|
52
|
+
width: '100%',
|
|
53
|
+
}}>
|
|
54
|
+
<SelectFaceted facets={FACETS} value={value} onValueChange={setValue} searchable />
|
|
55
|
+
|
|
56
|
+
{active.length > 0 && (
|
|
57
|
+
<div style={{ display: 'flex', flexDirection: 'row', flexWrap: 'wrap', gap: 8, alignItems: 'center' }}>
|
|
58
|
+
{/* Each active facet's result is its own control, the key piped into `leading`. */}
|
|
59
|
+
{active.map((facet) => {
|
|
60
|
+
// Options facets → an editable Select (single shows the trailing check;
|
|
61
|
+
// both are filterable + have a clearable footer).
|
|
62
|
+
if (facet.kind === 'single' || facet.kind === 'multiple')
|
|
63
|
+
return (
|
|
64
|
+
<Select
|
|
65
|
+
key={facet.key}
|
|
66
|
+
selection={facet.kind}
|
|
67
|
+
indicator={facet.kind === 'single' ? 'check' : undefined}
|
|
68
|
+
options={facet.options}
|
|
69
|
+
value={value[facet.key]}
|
|
70
|
+
onValueChange={(v) => setFacet(facet.key, v)}
|
|
71
|
+
leading={`${facet.label}:`}
|
|
72
|
+
filter={facet.filter}
|
|
73
|
+
clearable
|
|
74
|
+
// Chip sizes to its value and caps at a max-width (ellipsizing past it).
|
|
75
|
+
style={{ width: 'fit-content', maxWidth: '240px' }}
|
|
76
|
+
/>
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
// Free-form text / custom → an editable Input.
|
|
80
|
+
const val = facet.kind === 'text' ? value[facet.key] : value[facet.key]?.min
|
|
81
|
+
const write = (raw) =>
|
|
82
|
+
setFacet(facet.key, facet.kind === 'text' ? raw || undefined : raw ? { min: raw } : undefined)
|
|
83
|
+
return (
|
|
84
|
+
<Input
|
|
85
|
+
key={facet.key}
|
|
86
|
+
value={val ?? ''}
|
|
87
|
+
leading={`${facet.label}:`}
|
|
88
|
+
clearable
|
|
89
|
+
dimActions
|
|
90
|
+
onFocus={() => setFocusedKey(facet.key)}
|
|
91
|
+
onBlur={() => setFocusedKey(null)}
|
|
92
|
+
onInput={(e) => write(e.currentTarget.value)}
|
|
93
|
+
onClearInput={() => { setFocusedKey(null); setFacet(facet.key, undefined) }}
|
|
94
|
+
// Size the chip to its value, capped at a max-width.
|
|
95
|
+
style={{ width: 'fit-content', maxWidth: '240px' }}
|
|
96
|
+
/>
|
|
97
|
+
)
|
|
98
|
+
})}
|
|
99
|
+
{/* Once more than one result is showing, a single control resets them
|
|
100
|
+
all — pushed to the right of the last chip. */}
|
|
101
|
+
{active.length > 1 && (
|
|
102
|
+
<Button
|
|
103
|
+
priority="tertiary"
|
|
104
|
+
icon="filter-x"
|
|
105
|
+
label="Clear all"
|
|
106
|
+
style={{ marginLeft: 'auto' }}
|
|
107
|
+
onClick={() => { setFocusedKey(null); setValue({}) }}
|
|
108
|
+
/>
|
|
109
|
+
)}
|
|
110
|
+
</div>
|
|
111
|
+
)}
|
|
112
|
+
</div>
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The trigger opens the facet menu; each **active** facet's result renders below as its
|
|
118
|
+
own [Select](./select.md) — editable in place, with the facet key piped into the trigger's
|
|
119
|
+
`leading` slot as a prefix.
|
|
120
|
+
|
|
121
|
+
### Value & changes
|
|
122
|
+
|
|
123
|
+
The value is a **record keyed by facet key** — `{ [facetKey]: value }` — so each facet's
|
|
124
|
+
selection lives under its own `key`. The shape per facet follows its `kind`: an option
|
|
125
|
+
value for `single`, an array for `multiple`, a string for `text`, your own `V` for
|
|
126
|
+
`custom`. A **cleared** facet is absent from the record (not `undefined`), and the
|
|
127
|
+
trigger's badge counts the present keys. Control it with `value` (+ `onValueChange`) or
|
|
128
|
+
seed `defaultValue` for the uncontrolled case.
|
|
129
|
+
|
|
130
|
+
Because one control mixes facets that hold different value types, the record is typed
|
|
131
|
+
`Record<string, unknown>`; narrow per facet when you read it.
|
|
132
|
+
|
|
133
|
+
**`onValueChange(value, attrs)`** fires after any change:
|
|
134
|
+
|
|
135
|
+
- **`value`** — the whole new record, e.g. `{ assignee: ['Alice'], status: 'open', title: 'crash' }`.
|
|
136
|
+
- **`attrs`** — what changed: `{ facet, kind, value }` for a single facet edit (the
|
|
137
|
+
facet's `key`, its `kind`, and its new value, `undefined` when that facet was cleared),
|
|
138
|
+
or `{ all: true }` for the **Clear all** row. Narrow on `'all' in attrs` before reading
|
|
139
|
+
`facet`.
|
|
140
|
+
|
|
141
|
+
```tsx
|
|
142
|
+
<SelectFaceted
|
|
143
|
+
facets={FACETS}
|
|
144
|
+
onValueChange={(value, attrs) => {
|
|
145
|
+
if ('all' in attrs) return clearFilters() // the Clear all row
|
|
146
|
+
// `value` is the full record; `attrs.facet` / `attrs.value` is the one that changed
|
|
147
|
+
applyFilter(attrs.facet, attrs.value)
|
|
148
|
+
}}
|
|
149
|
+
/>
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Value types
|
|
153
|
+
|
|
154
|
+
A `single` / `multiple` facet's option values follow [Select](./select.md#value--changes):
|
|
155
|
+
`string`, `number`, or `boolean` (the `OptionValue` type), compared with `===` and
|
|
156
|
+
returned through the value record unchanged. For a value that isn't a primitive, give the
|
|
157
|
+
option `value` a **stable primitive key** (a date's ISO string, a record's id) and read
|
|
158
|
+
the full value back by that key. When the whole facet is object-shaped (a date range, a
|
|
159
|
+
numeric comparator), use a **`custom`** facet: it owns its editor and value type `V`, so
|
|
160
|
+
it holds any shape and reports it back untouched.
|
|
161
|
+
|
|
162
|
+
### Custom facet
|
|
163
|
+
|
|
164
|
+
A `custom` facet has **no option list**: you draw the editor and choose the value it
|
|
165
|
+
holds — a number, a date, a range, any object. Beyond the base `key` / `label` / `icon`,
|
|
166
|
+
provide two functions:
|
|
167
|
+
|
|
168
|
+
- **`render(ctx)`** — your editor, and the **only** place the value is produced.
|
|
169
|
+
`ctx.value` is the current value, `ctx.onChange(next)` sets it (`undefined` clears the
|
|
170
|
+
facet), `ctx.close()` dismisses the menu. Whatever you pass to `onChange` lands in
|
|
171
|
+
`value[key]` unchanged and comes back as `ctx.value` next render.
|
|
172
|
+
- **`summary(value)`** — the chip on the facet's row while a value is set. It's
|
|
173
|
+
**display-only** and decoupled from the value: the value round-trips through
|
|
174
|
+
`onChange` / `ctx.value`, so `summary` can return any node (or `null`) without changing
|
|
175
|
+
what you read from `value[key]`. The chip caps its width and ellipsizes a long
|
|
176
|
+
summary so it can't stretch the menu.
|
|
177
|
+
|
|
178
|
+
Nothing is compared or filtered inside the component; it stores the value and reports it
|
|
179
|
+
via `onValueChange`, and applying it to your rows is your code. Type the facet
|
|
180
|
+
`SelectFacetCustom<V>` so `ctx.value` and `summary` are typed.
|
|
181
|
+
|
|
182
|
+
A number, "min duration": the editor is one `Input`, which has no popup of its own, so it
|
|
183
|
+
sits directly in the flyout.
|
|
184
|
+
|
|
185
|
+
```tsx
|
|
186
|
+
import type { SelectFacetCustom } from '@antadesign/anta'
|
|
187
|
+
|
|
188
|
+
const duration: SelectFacetCustom<string> = {
|
|
189
|
+
key: 'duration',
|
|
190
|
+
label: 'Min duration',
|
|
191
|
+
kind: 'custom',
|
|
192
|
+
render: (ctx) => (
|
|
193
|
+
<Input
|
|
194
|
+
// Controlled numeric field: type="text" + inputMode, not type="number"
|
|
195
|
+
// (which sanitizes "1." / "1.0" to empty). Keep the raw string; parse it below.
|
|
196
|
+
type="text"
|
|
197
|
+
inputMode="decimal"
|
|
198
|
+
size="small"
|
|
199
|
+
placeholder="seconds"
|
|
200
|
+
value={ctx.value ?? ''}
|
|
201
|
+
onInput={(e) => ctx.onChange(e.currentTarget.value || undefined)}
|
|
202
|
+
/>
|
|
203
|
+
),
|
|
204
|
+
summary: (v) => `≥ ${v}s`,
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
<SelectFaceted facets={[duration]} value={value} onValueChange={setValue} />
|
|
208
|
+
|
|
209
|
+
// user types 1.5 → value.duration === "1.5" (the raw string, returned unchanged)
|
|
210
|
+
// parse it where you apply the filter, so partial input ("1.", "1.0") types cleanly:
|
|
211
|
+
rows.filter((r) => value.duration == null || r.seconds >= Number(value.duration))
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
A date, cast through a helper. Editors with **their own popup** (the inline `Calendar`,
|
|
215
|
+
`InputDate`, `Select`) work in the flyout too: each opens its menu stacked on top while
|
|
216
|
+
the flyout stays open. This one uses the inline `Calendar` grid; two helpers bridge its
|
|
217
|
+
ISO strings and the `Date` you store.
|
|
218
|
+
|
|
219
|
+
```tsx
|
|
220
|
+
import type { SelectFacetCustom } from '@antadesign/anta'
|
|
221
|
+
|
|
222
|
+
const toISO = (d: Date) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}` // Date → "2026-01-01"
|
|
223
|
+
const fromISO = (s: string | null) => (s ? new Date(`${s}T00:00:00`) : undefined) // ISO → Date | undefined
|
|
224
|
+
|
|
225
|
+
const since: SelectFacetCustom<Date> = {
|
|
226
|
+
key: 'since',
|
|
227
|
+
label: 'Created after',
|
|
228
|
+
kind: 'custom',
|
|
229
|
+
icon: 'calendar',
|
|
230
|
+
render: (ctx) => (
|
|
231
|
+
<Calendar
|
|
232
|
+
size="small"
|
|
233
|
+
value={ctx.value ? toISO(ctx.value) : ''}
|
|
234
|
+
// Controlled: apply the pick in onStateChange (onValueChange fires only after
|
|
235
|
+
// value changes, so it can't drive a controlled Calendar).
|
|
236
|
+
onStateChange={(_e, { next }) => ctx.onChange(fromISO(next))}
|
|
237
|
+
/>
|
|
238
|
+
),
|
|
239
|
+
summary: (d) => d.toLocaleDateString(),
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
<SelectFaceted facets={[since]} value={value} onValueChange={setValue} />
|
|
243
|
+
|
|
244
|
+
// user picks a day → value.since is a Date, returned unchanged
|
|
245
|
+
// you apply it:
|
|
246
|
+
rows.filter((r) => value.since == null || r.createdAt >= value.since)
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
A `custom` facet holds the value whole, so a `Date` works here: it isn't compared by
|
|
250
|
+
`===` like a `single` / `multiple` option value, so there's no identity pitfall.
|
|
251
|
+
|
|
252
|
+
#### A recency filter: presets plus a range
|
|
253
|
+
|
|
254
|
+
A compound editor in one flyout: a `RadioGroup` of presets (Today, Yesterday, Last
|
|
255
|
+
14 / 30 days) and a **Custom range** that reveals two `InputDate` fields. The facet
|
|
256
|
+
value is either `{ preset }` or `{ from, to }`, and a `resolveRange` helper turns
|
|
257
|
+
whichever one into the concrete `{ from, to }` you filter rows with (shown in the
|
|
258
|
+
readout under the trigger). Each `InputDate` opens its calendar stacked on top of the
|
|
259
|
+
flyout, which stays open throughout.
|
|
260
|
+
|
|
261
|
+
```tsx
|
|
262
|
+
type Recency = { preset: 'today' | 'yesterday' | 'last14' | 'last30' } | { from: string; to: string }
|
|
263
|
+
|
|
264
|
+
// Whichever shape the facet holds, resolve it to a concrete range to filter with.
|
|
265
|
+
const resolveRange = (r: Recency): { from: string; to: string } => {
|
|
266
|
+
if ('from' in r) return r
|
|
267
|
+
const today = isoDay(daysAgo(0))
|
|
268
|
+
switch (r.preset) {
|
|
269
|
+
case 'today': return { from: today, to: today }
|
|
270
|
+
case 'yesterday': { const y = isoDay(daysAgo(1)); return { from: y, to: y } }
|
|
271
|
+
case 'last14': return { from: isoDay(daysAgo(13)), to: today }
|
|
272
|
+
case 'last30': return { from: isoDay(daysAgo(29)), to: today }
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
import type { SelectFacetCustom } from '@antadesign/anta'
|
|
277
|
+
|
|
278
|
+
const recency: SelectFacetCustom<Recency> = {
|
|
279
|
+
key: 'recency',
|
|
280
|
+
label: 'Recency',
|
|
281
|
+
kind: 'custom',
|
|
282
|
+
icon: 'calendar',
|
|
283
|
+
summary: (v) => ('preset' in v ? PRESET_LABELS[v.preset] : `${v.from} → ${v.to}`),
|
|
284
|
+
render: ({ value, onChange }) => {
|
|
285
|
+
const mode = value == null ? '' : 'preset' in value ? value.preset : 'custom'
|
|
286
|
+
const range = value && 'from' in value ? value : { from: '', to: '' }
|
|
287
|
+
return (
|
|
288
|
+
<div style={{ display: 'grid', gap: 8 }}>
|
|
289
|
+
<RadioGroup
|
|
290
|
+
size="small"
|
|
291
|
+
options={PRESETS} // Today / Yesterday / Last 14 / Last 30 / Custom range
|
|
292
|
+
value={mode}
|
|
293
|
+
// Controlled group: apply the pick in onStateChange (onValueChange fires
|
|
294
|
+
// only after `value` changes, so it can't drive a controlled RadioGroup).
|
|
295
|
+
onStateChange={(_e, { next }) => onChange(next === 'custom' ? range : { preset: next })}
|
|
296
|
+
/>
|
|
297
|
+
{mode === 'custom' && (
|
|
298
|
+
<>
|
|
299
|
+
<InputDate size="small" label="From" value={range.from}
|
|
300
|
+
onValueChange={(from) => onChange({ from, to: range.to })} />
|
|
301
|
+
<InputDate size="small" label="To" value={range.to} min={range.from || undefined}
|
|
302
|
+
onValueChange={(to) => onChange({ from: range.from, to })} />
|
|
303
|
+
</>
|
|
304
|
+
)}
|
|
305
|
+
</div>
|
|
306
|
+
)
|
|
307
|
+
},
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
<SelectFaceted facets={[recency]} value={value} onValueChange={setValue} />
|
|
311
|
+
// value.recency is { preset: 'last14' } or { from, to } — apply resolveRange(value.recency) to your rows
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
#### Menu-item presets, calendar on the right
|
|
315
|
+
|
|
316
|
+
The same filter with the presets as `MenuItem` rows (a check marks the active one) and
|
|
317
|
+
a single **Custom day** that reveals one `InputDate`, its calendar opening to the
|
|
318
|
+
**right** via `placement="right-start"`. That helps when the trigger sits on the left of
|
|
319
|
+
a wide layout. `placement` (and `offset`) forward to the calendar's `Menu`; `right` /
|
|
320
|
+
`left` are new alongside the existing `top` / `bottom`.
|
|
321
|
+
|
|
322
|
+
```tsx
|
|
323
|
+
type Recency = { preset: 'today' | 'yesterday' | 'last14' | 'last30' } | { day: string }
|
|
324
|
+
|
|
325
|
+
const recency: SelectFacetCustom<Recency> = {
|
|
326
|
+
key: 'recency',
|
|
327
|
+
label: 'Recency',
|
|
328
|
+
kind: 'custom',
|
|
329
|
+
icon: 'calendar',
|
|
330
|
+
summary: (v) => ('preset' in v ? PRESET_LABELS[v.preset] : v.day || 'Custom day'),
|
|
331
|
+
render: ({ value, onChange }) => {
|
|
332
|
+
const mode = value == null ? '' : 'preset' in value ? value.preset : 'custom'
|
|
333
|
+
const day = value && 'day' in value ? value.day : ''
|
|
334
|
+
return (
|
|
335
|
+
<>
|
|
336
|
+
{DAY_PRESETS.map((p) => ( // Today / Yesterday / Last 14 / Last 30 / Custom day
|
|
337
|
+
<MenuItem
|
|
338
|
+
key={p.value}
|
|
339
|
+
label={p.label}
|
|
340
|
+
selectionIndicator="check"
|
|
341
|
+
selected={mode === p.value}
|
|
342
|
+
data-menu-open
|
|
343
|
+
onSelect={() => onChange(p.value === 'custom' ? { day } : { preset: p.value })}
|
|
344
|
+
/>
|
|
345
|
+
))}
|
|
346
|
+
{mode === 'custom' && (
|
|
347
|
+
<div data-menu-open style={{ padding: 8 }}>
|
|
348
|
+
{/* opens the calendar to the right of the field */}
|
|
349
|
+
<InputDate size="small" label="Day" placement="right-start" value={day}
|
|
350
|
+
onValueChange={(d) => onChange({ day: d })} />
|
|
351
|
+
</div>
|
|
352
|
+
)}
|
|
353
|
+
</>
|
|
354
|
+
)
|
|
355
|
+
},
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
<SelectFaceted facets={[recency]} value={value} onValueChange={setValue} />
|
|
359
|
+
// value.recency is { preset: 'last14' } or { day: '2026-07-20' } — resolve a preset to a
|
|
360
|
+
// concrete date (or take the day as-is) and filter your rows with it
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### Props
|
|
364
|
+
|
|
365
|
+
| Prop | Type | Default | Description |
|
|
366
|
+
|------|------|---------|-------------|
|
|
367
|
+
| `facets` | SelectFacet[] | — | The facets (dimensions) to filter across, each with its own editor kind. |
|
|
368
|
+
| `clearable?` | boolean | true | Show the per-facet "Clear" row and the "Clear all" row. |
|
|
369
|
+
| `clearAllLabel?` | string | Clear all | Label for the "Clear all" row. |
|
|
370
|
+
| `defaultValue?` | SelectFacetedValue | — | Initial value for the uncontrolled case (the wrapper then owns it). |
|
|
371
|
+
| `disabled?` | boolean | — | Disable the whole control. |
|
|
372
|
+
| `icon?` | IconShape | filter | Default trigger's leading icon. |
|
|
373
|
+
| `label?` | string | Filter | Default trigger's button label. |
|
|
374
|
+
| `offset?` | number | 4 | Gap in pixels between the trigger and the filter menu. |
|
|
375
|
+
| `onValueChange?` | (value, attrs) => void | — | Fires after any facet changes. `value` is the whole new record (a facet key →
|
|
376
|
+
that facet's value; a cleared facet is absent). `attrs` says what changed:
|
|
377
|
+
`{ facet, kind, value }` for a single facet edit, or `{ all: true }` for the
|
|
378
|
+
"Clear all" row — narrow on `'all' in attrs` before reading `facet`. |
|
|
379
|
+
| `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 root filter menu relative to its trigger. The
|
|
380
|
+
menu auto-flips vertically and clamps horizontally when needed. |
|
|
381
|
+
| `priority?` | 'primary' \| 'secondary' | secondary | Default trigger's button priority. |
|
|
382
|
+
| `renderTrigger?` | (state) => ReactNode | — | Render your own trigger in place of the default `Button`. Receives a
|
|
383
|
+
`SelectFacetedTriggerState`; **return exactly one focusable element** (the
|
|
384
|
+
menu anchors to it as its previous sibling and opens it on click). Give it
|
|
385
|
+
`aria-haspopup="menu"` plus `aria-expanded={state.open}`. The custom trigger
|
|
386
|
+
owns its own styling — `className` / `style` / other props apply to the
|
|
387
|
+
*default* trigger only, so put your own on the element you return. |
|
|
388
|
+
| `searchable?` | boolean | — | Add a single search field to the top of the root menu that flattens every
|
|
389
|
+
`single` / `multiple` facet's options into one list and searches across them
|
|
390
|
+
— typing "alice" surfaces it under both an Assignee and an Owner facet, each
|
|
391
|
+
selectable in place. `text` / `custom` facets are reachable when the search is
|
|
392
|
+
empty. Matching uses each facet's `filter` function if it has one, else the
|
|
393
|
+
built-in substring match. |
|
|
394
|
+
| `searchPlaceholder?` | string | Filter… | Placeholder for the global search field. |
|
|
395
|
+
| `size?` | 'small' \| 'medium' \| 'large' | medium | Default trigger's button size. |
|
|
396
|
+
| `toneSelected?` | 'neutral' \| 'brand' \| 'info' \| 'success' \| 'warning' \| 'critical' \| (string & {}) | — | Tone applied to a selected option row in the facet flyouts (label, selected
|
|
397
|
+
tint, and the check / checkbox indicator). A named tone or a custom CSS
|
|
398
|
+
colour, matching `Select`'s `toneSelected`. Defaults to a neutral selection. |
|
|
399
|
+
| `value?` | SelectFacetedValue | — | Controlled value — the facet-keyed record. When provided, the consumer
|
|
400
|
+
owns state: a pick only *requests* a change via `onValueChange` (reject by
|
|
401
|
+
not updating). Leave undefined for uncontrolled. |
|
|
402
|
+
|
|
403
|
+
### The `SelectFacet` type
|
|
404
|
+
|
|
405
|
+
`facets` takes a `SelectFacet[]`. Each facet is discriminated by **`kind`** and shares a
|
|
406
|
+
base of `key` / `label` / `icon`:
|
|
407
|
+
|
|
408
|
+
A `single` / `multiple` facet is [Select](./select.md)'s option model under a `key` — the same
|
|
409
|
+
`options` (`SelectItem[]`), `filter`, and `selectAll` you'd pass to a `Select`, with `kind`
|
|
410
|
+
where Select has `selection`. Know `Select` and you know these two facets; `text` and
|
|
411
|
+
`custom` are the kinds with no `Select` counterpart. The value each facet stores follows
|
|
412
|
+
the same rule: a `single`/`multiple` facet reports the chosen option's `value` (or an array
|
|
413
|
+
of them), exactly as [Select](./select.md#value--changes) does under its own key.
|
|
414
|
+
|
|
415
|
+
| Field | Type | Description |
|
|
416
|
+
|---|---|---|
|
|
417
|
+
| `key` | `string` | The facet's namespace — the key its value is stored under in the record, and what `attrs.facet` reports. Unique across `facets`. |
|
|
418
|
+
| `label` | `string` | The facet's row label in the menu. |
|
|
419
|
+
| `icon` | `IconShape` | Leading icon on the facet's row. |
|
|
420
|
+
|
|
421
|
+
**`single`** (`SelectFacetSingle`) — pick one. Value: the chosen option's `value`, or
|
|
422
|
+
absent when cleared (re-picking the selected option clears it).
|
|
423
|
+
|
|
424
|
+
| Field | Type | Description |
|
|
425
|
+
|---|---|---|
|
|
426
|
+
| `kind` | `'single'` | Discriminant. |
|
|
427
|
+
| `options` | `SelectItem[]` | The choices — bare strings or `SelectOption`s (groups / submenus flatten to their leaves). Option `value`s may be `string` / `number` / `boolean`. |
|
|
428
|
+
| `filter` | `FacetFilter` | Search field over this facet's options: `true` for the built-in substring match, or `(option, query) => boolean`. |
|
|
429
|
+
|
|
430
|
+
**`multiple`** (`SelectFacetMultiple`) — pick any number. Value: an array of the chosen
|
|
431
|
+
`value`s (empty array clears it).
|
|
432
|
+
|
|
433
|
+
| Field | Type | Description |
|
|
434
|
+
|---|---|---|
|
|
435
|
+
| `kind` | `'multiple'` | Discriminant. |
|
|
436
|
+
| `options` | `SelectItem[]` | As `single`. |
|
|
437
|
+
| `filter` | `FacetFilter` | As `single`. |
|
|
438
|
+
| `selectAll` | `boolean` | A "Select all" row toggling every visible option. Default `true`. |
|
|
439
|
+
| `selectAllLabel` | `string` | Label for that row. Default `Select all`. |
|
|
440
|
+
|
|
441
|
+
**`text`** (`SelectFacetText`) — free-form substring. Value: the typed string, applied on
|
|
442
|
+
Enter / blur (absent when empty).
|
|
443
|
+
|
|
444
|
+
| Field | Type | Description |
|
|
445
|
+
|---|---|---|
|
|
446
|
+
| `kind` | `'text'` | Discriminant. |
|
|
447
|
+
| `placeholder` | `string` | Placeholder for the text field. |
|
|
448
|
+
|
|
449
|
+
**`custom`** (`SelectFacetCustom<V>`) — bring your own editor and value type `V`, including
|
|
450
|
+
object-shaped values (a date range, a numeric comparator).
|
|
451
|
+
|
|
452
|
+
| Field | Type | Description |
|
|
453
|
+
|---|---|---|
|
|
454
|
+
| `kind` | `'custom'` | Discriminant. |
|
|
455
|
+
| `render` | `(ctx) => ReactNode` | Your editor. `ctx.value` is the current `V`, `ctx.onChange(next)` sets it (`undefined` clears the facet), `ctx.close()` dismisses the menu. |
|
|
456
|
+
| `summary` | `(value: V) => ReactNode` | The chip on the facet's row while a value is set. |
|