@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,451 @@
|
|
|
1
|
+
# SelectFaceted
|
|
2
|
+
|
|
3
|
+
A faceted filter lets people filter a result set by several categories. Anta
|
|
4
|
+
calls each category a **facet**. The trigger opens a menu of facets, and each
|
|
5
|
+
facet opens a control for choosing its value.
|
|
6
|
+
|
|
7
|
+
The value is a `Record<facetKey, value>`. This keeps values separate when two
|
|
8
|
+
facets use the same option. For example, `assignee` and `owner` below are
|
|
9
|
+
application-defined facet keys, not Anta terms. Each facet has one of four
|
|
10
|
+
`kind` values: `single`, `multiple`, `text`, or `custom`.
|
|
11
|
+
|
|
12
|
+
`SelectFaceted` uses [Menu](./menu.md), [Select](./select.md)'s option model,
|
|
13
|
+
[Input](./input.md), and [Tag](./tag.md). It does not create a custom element.
|
|
14
|
+
Use `value` with `onValueChange` to control it, or use `defaultValue` for an
|
|
15
|
+
uncontrolled filter.
|
|
16
|
+
|
|
17
|
+
Pass `placement` to control where the root facet menu opens relative to its
|
|
18
|
+
trigger. It uses the same values as `Menu` and still flips or clamps when space
|
|
19
|
+
is limited. `offset` sets the gap in pixels between the trigger and the menu:
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
<SelectFaceted placement="top-end" offset={8} facets={FACETS} />
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
const FACETS = [
|
|
27
|
+
{ key: 'assignee', label: 'Assignee', kind: 'multiple', filter: true, options: people },
|
|
28
|
+
{ key: 'owner', label: 'Owner', kind: 'single', filter: true, options: people },
|
|
29
|
+
{ key: 'status', label: 'Status', kind: 'single', options: [{ value: 'open', label: 'Open' }, /* … */] },
|
|
30
|
+
{ key: 'title', label: 'Title contains', kind: 'text' },
|
|
31
|
+
{ key: 'duration', label: 'Min duration', kind: 'custom', summary: (v) => `≥ ${v.min}s`, render: /* … */ },
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
const isSet = (v) => !(v == null || v === '' || (Array.isArray(v) && v.length === 0))
|
|
35
|
+
|
|
36
|
+
function Demo() {
|
|
37
|
+
const [value, setValue] = useState({ assignee: ['Alice Nguyen'], status: 'open', title: 'crash' })
|
|
38
|
+
const setFacet = (key, v) =>
|
|
39
|
+
setValue((p) => (isSet(v) ? { ...p, [key]: v } : (({ [key]: _, ...rest }) => rest)(p)))
|
|
40
|
+
|
|
41
|
+
// Keep a text/custom chip mounted while it's focused, even when emptied — so
|
|
42
|
+
// backspacing to empty doesn't yank the field (only Clear or blur-when-empty does).
|
|
43
|
+
const [focusedKey, setFocusedKey] = useState(null)
|
|
44
|
+
const active = FACETS.filter((f) => isSet(value[f.key]) || f.key === focusedKey)
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div style={{
|
|
48
|
+
display: 'flex',
|
|
49
|
+
flexDirection: 'row',
|
|
50
|
+
flexWrap: 'wrap',
|
|
51
|
+
gap: 12,
|
|
52
|
+
alignItems: 'center',
|
|
53
|
+
width: '100%',
|
|
54
|
+
}}>
|
|
55
|
+
<SelectFaceted facets={FACETS} value={value} onValueChange={setValue} searchable />
|
|
56
|
+
|
|
57
|
+
{active.length > 0 && (
|
|
58
|
+
<div style={{ display: 'flex', flexDirection: 'row', flexWrap: 'wrap', gap: 8, alignItems: 'center' }}>
|
|
59
|
+
{/* Show one editable control for each active facet. Its label becomes
|
|
60
|
+
the control's `leading` prefix. */}
|
|
61
|
+
{active.map((facet) => {
|
|
62
|
+
// Options facets → an editable Select (single shows the trailing check;
|
|
63
|
+
// both are filterable + have a clearable footer).
|
|
64
|
+
if (facet.kind === 'single' || facet.kind === 'multiple')
|
|
65
|
+
return (
|
|
66
|
+
<Select
|
|
67
|
+
key={facet.key}
|
|
68
|
+
selection={facet.kind}
|
|
69
|
+
indicator={facet.kind === 'single' ? 'check' : undefined}
|
|
70
|
+
options={facet.options}
|
|
71
|
+
value={value[facet.key]}
|
|
72
|
+
onValueChange={(v) => setFacet(facet.key, v)}
|
|
73
|
+
leading={`${facet.label}:`}
|
|
74
|
+
filter={facet.filter}
|
|
75
|
+
clearable
|
|
76
|
+
// Chip sizes to its value and caps at a max-width (ellipsizing past it).
|
|
77
|
+
style={{ width: 'fit-content', maxWidth: '240px' }}
|
|
78
|
+
/>
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
// Free-form text / custom → an editable Input.
|
|
82
|
+
const val = facet.kind === 'text' ? value[facet.key] : value[facet.key]?.min
|
|
83
|
+
const write = (raw) =>
|
|
84
|
+
setFacet(facet.key, facet.kind === 'text' ? raw || undefined : raw ? { min: raw } : undefined)
|
|
85
|
+
return (
|
|
86
|
+
<Input
|
|
87
|
+
key={facet.key}
|
|
88
|
+
value={val ?? ''}
|
|
89
|
+
leading={`${facet.label}:`}
|
|
90
|
+
clearable
|
|
91
|
+
dimActions
|
|
92
|
+
onFocus={() => setFocusedKey(facet.key)}
|
|
93
|
+
onBlur={() => setFocusedKey(null)}
|
|
94
|
+
onInput={(e) => write(e.currentTarget.value)}
|
|
95
|
+
onClearInput={() => { setFocusedKey(null); setFacet(facet.key, undefined) }}
|
|
96
|
+
// Size the chip to its value, capped at a max-width.
|
|
97
|
+
style={{ width: 'fit-content', maxWidth: '240px' }}
|
|
98
|
+
/>
|
|
99
|
+
)
|
|
100
|
+
})}
|
|
101
|
+
{/* Once more than one result is showing, a single control resets them
|
|
102
|
+
all — pushed to the right of the last chip. */}
|
|
103
|
+
{active.length > 1 && (
|
|
104
|
+
<Button
|
|
105
|
+
priority="tertiary"
|
|
106
|
+
icon="filter-x"
|
|
107
|
+
label="Clear all"
|
|
108
|
+
style={{ marginLeft: 'auto' }}
|
|
109
|
+
onClick={() => { setFocusedKey(null); setValue({}) }}
|
|
110
|
+
/>
|
|
111
|
+
)}
|
|
112
|
+
</div>
|
|
113
|
+
)}
|
|
114
|
+
</div>
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The trigger opens the facet menu. The example renders one editable
|
|
120
|
+
[Select](./select.md) below it for each active facet. Each Select receives the
|
|
121
|
+
facet label through `leading`, which displays it as a prefix.
|
|
122
|
+
|
|
123
|
+
### Value and changes
|
|
124
|
+
|
|
125
|
+
The value is a record keyed by facet key: `{ [facetKey]: value }`. Its shape
|
|
126
|
+
depends on `kind`: an option value for `single`, an array for `multiple`, a
|
|
127
|
+
string for `text`, and a value of type `V` for `custom`. A cleared facet is
|
|
128
|
+
absent from the record, rather than set to `undefined`. The trigger badge counts
|
|
129
|
+
the keys that remain. Use `value` with `onValueChange` to control the filter, or
|
|
130
|
+
use `defaultValue` for the uncontrolled case.
|
|
131
|
+
|
|
132
|
+
Because one control mixes facets that hold different value types, the record is typed
|
|
133
|
+
`Record<string, unknown>`; narrow per facet when you read it.
|
|
134
|
+
|
|
135
|
+
**`onValueChange(value, attrs)`** fires after any change:
|
|
136
|
+
|
|
137
|
+
- **`value`** — the whole new record, e.g. `{ assignee: ['Alice'], status: 'open', title: 'crash' }`.
|
|
138
|
+
- **`attrs`** — what changed: `{ facet, kind, value }` for a single facet edit (the
|
|
139
|
+
facet's `key`, its `kind`, and its new value, `undefined` when that facet was cleared),
|
|
140
|
+
or `{ all: true }` for the **Clear all** row. Narrow on `'all' in attrs` before reading
|
|
141
|
+
`facet`.
|
|
142
|
+
|
|
143
|
+
```tsx
|
|
144
|
+
<SelectFaceted
|
|
145
|
+
facets={FACETS}
|
|
146
|
+
onValueChange={(value, attrs) => {
|
|
147
|
+
if ('all' in attrs) return clearFilters() // the Clear all row
|
|
148
|
+
// `value` is the full record; `attrs.facet` / `attrs.value` is the one that changed
|
|
149
|
+
applyFilter(attrs.facet, attrs.value)
|
|
150
|
+
}}
|
|
151
|
+
/>
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Value types
|
|
155
|
+
|
|
156
|
+
`single` and `multiple` facets use [Select](./select.md#value-and-changes) option
|
|
157
|
+
values: `string`, `number`, or `boolean` (`OptionValue`). Anta compares them
|
|
158
|
+
with `===` and returns them unchanged. For a non-primitive value, use a stable
|
|
159
|
+
primitive key such as a date's ISO string or a record ID. Look up the full value
|
|
160
|
+
with that key. Use a `custom` facet when the facet value is an object, such as a
|
|
161
|
+
date range or numeric comparison. Its editor can return any value of type `V`.
|
|
162
|
+
|
|
163
|
+
### Custom facet
|
|
164
|
+
|
|
165
|
+
A `custom` facet has no option list. Provide its editor and choose the value it
|
|
166
|
+
returns: a number, date, range, or any other object. In addition to `key`,
|
|
167
|
+
`label`, and `icon`, provide two functions:
|
|
168
|
+
|
|
169
|
+
- **`render(ctx)`** renders the editor. `ctx.value` is the current value.
|
|
170
|
+
`ctx.onChange(next)` sets it, and `undefined` clears the facet. `ctx.close()`
|
|
171
|
+
closes the menu. Anta stores the value passed to `onChange` at `value[key]`
|
|
172
|
+
without changing it.
|
|
173
|
+
- **`summary(value)`** renders the chip on the facet row when a value is set.
|
|
174
|
+
It does not change the stored value. It can return any node or `null`. Long
|
|
175
|
+
summaries are truncated so that they do not widen the menu.
|
|
176
|
+
|
|
177
|
+
`SelectFaceted` does not filter application data. It stores the facet values and
|
|
178
|
+
reports them through `onValueChange`. Filter the application's rows from that
|
|
179
|
+
value. Type the facet as `SelectFacetCustom<V>` so `ctx.value` and `summary`
|
|
180
|
+
use the same value type.
|
|
181
|
+
|
|
182
|
+
For a minimum-duration filter, the editor is one `Input`. Because that Input
|
|
183
|
+
does not open a popup, it appears directly in the facet menu.
|
|
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
|
+
For dates, the example converts between the Calendar's ISO strings and a stored
|
|
215
|
+
`Date`. Editors that open a menu, including `InputDate` and `Select`, also work
|
|
216
|
+
inside a custom facet. Their menu opens above the facet menu, which remains open.
|
|
217
|
+
|
|
218
|
+
```tsx
|
|
219
|
+
import type { SelectFacetCustom } from '@antadesign/anta'
|
|
220
|
+
|
|
221
|
+
const toISO = (d: Date) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}` // Date → "2026-01-01"
|
|
222
|
+
const fromISO = (s: string | null) => (s ? new Date(`${s}T00:00:00`) : undefined) // ISO → Date | undefined
|
|
223
|
+
|
|
224
|
+
const since: SelectFacetCustom<Date> = {
|
|
225
|
+
key: 'since',
|
|
226
|
+
label: 'Created after',
|
|
227
|
+
kind: 'custom',
|
|
228
|
+
icon: 'calendar',
|
|
229
|
+
render: (ctx) => (
|
|
230
|
+
<Calendar
|
|
231
|
+
size="small"
|
|
232
|
+
value={ctx.value ? toISO(ctx.value) : ''}
|
|
233
|
+
// Controlled: apply the pick in onStateChange (onValueChange fires only after
|
|
234
|
+
// value changes, so it can't drive a controlled Calendar).
|
|
235
|
+
onStateChange={(_e, { next }) => ctx.onChange(fromISO(next))}
|
|
236
|
+
/>
|
|
237
|
+
),
|
|
238
|
+
summary: (d) => d.toLocaleDateString(),
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
<SelectFaceted facets={[since]} value={value} onValueChange={setValue} />
|
|
242
|
+
|
|
243
|
+
// user picks a day → value.since is a Date, returned unchanged
|
|
244
|
+
// you apply it:
|
|
245
|
+
rows.filter((r) => value.since == null || r.createdAt >= value.since)
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
A custom facet stores a `Date` directly. Unlike the value of a `single` or
|
|
249
|
+
`multiple` facet, it is not compared with `===` to find an option.
|
|
250
|
+
|
|
251
|
+
#### A recency filter: presets plus a range
|
|
252
|
+
|
|
253
|
+
This editor combines a `RadioGroup` of presets with a **Custom range** option
|
|
254
|
+
that reveals two `InputDate` fields. Its value is either `{ preset }` or
|
|
255
|
+
`{ from, to }`. The `resolveRange` helper converts either shape into the date
|
|
256
|
+
range used to filter the result set. Each InputDate calendar opens above the
|
|
257
|
+
facet menu without closing it.
|
|
258
|
+
|
|
259
|
+
```tsx
|
|
260
|
+
type Recency = { preset: 'today' | 'yesterday' | 'last14' | 'last30' } | { from: string; to: string }
|
|
261
|
+
|
|
262
|
+
// Whichever shape the facet holds, resolve it to a concrete range to filter with.
|
|
263
|
+
const resolveRange = (r: Recency): { from: string; to: string } => {
|
|
264
|
+
if ('from' in r) return r
|
|
265
|
+
const today = isoDay(daysAgo(0))
|
|
266
|
+
switch (r.preset) {
|
|
267
|
+
case 'today': return { from: today, to: today }
|
|
268
|
+
case 'yesterday': { const y = isoDay(daysAgo(1)); return { from: y, to: y } }
|
|
269
|
+
case 'last14': return { from: isoDay(daysAgo(13)), to: today }
|
|
270
|
+
case 'last30': return { from: isoDay(daysAgo(29)), to: today }
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
import type { SelectFacetCustom } from '@antadesign/anta'
|
|
275
|
+
|
|
276
|
+
const recency: SelectFacetCustom<Recency> = {
|
|
277
|
+
key: 'recency',
|
|
278
|
+
label: 'Recency',
|
|
279
|
+
kind: 'custom',
|
|
280
|
+
icon: 'calendar',
|
|
281
|
+
summary: (v) => ('preset' in v ? PRESET_LABELS[v.preset] : `${v.from} → ${v.to}`),
|
|
282
|
+
render: ({ value, onChange }) => {
|
|
283
|
+
const mode = value == null ? '' : 'preset' in value ? value.preset : 'custom'
|
|
284
|
+
const range = value && 'from' in value ? value : { from: '', to: '' }
|
|
285
|
+
return (
|
|
286
|
+
<div style={{ display: 'grid', gap: 8 }}>
|
|
287
|
+
<RadioGroup
|
|
288
|
+
size="small"
|
|
289
|
+
options={PRESETS} // Today / Yesterday / Last 14 / Last 30 / Custom range
|
|
290
|
+
value={mode}
|
|
291
|
+
// Controlled group: apply the pick in onStateChange (onValueChange fires
|
|
292
|
+
// only after `value` changes, so it can't drive a controlled RadioGroup).
|
|
293
|
+
onStateChange={(_e, { next }) => onChange(next === 'custom' ? range : { preset: next })}
|
|
294
|
+
/>
|
|
295
|
+
{mode === 'custom' && (
|
|
296
|
+
<>
|
|
297
|
+
<InputDate size="small" label="From" value={range.from}
|
|
298
|
+
onValueChange={(from) => onChange({ from, to: range.to })} />
|
|
299
|
+
<InputDate size="small" label="To" value={range.to} min={range.from || undefined}
|
|
300
|
+
onValueChange={(to) => onChange({ from: range.from, to })} />
|
|
301
|
+
</>
|
|
302
|
+
)}
|
|
303
|
+
</div>
|
|
304
|
+
)
|
|
305
|
+
},
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
<SelectFaceted facets={[recency]} value={value} onValueChange={setValue} />
|
|
309
|
+
// value.recency is { preset: 'last14' } or { from, to } — apply resolveRange(value.recency) to your rows
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
#### Menu-item presets, calendar on the right
|
|
313
|
+
|
|
314
|
+
This version renders the presets as `MenuItem` rows and marks the selected row
|
|
315
|
+
with a check. **Custom day** reveals one `InputDate`. Its calendar opens to the
|
|
316
|
+
right with `placement="right-start"`, which is useful when the trigger is on the
|
|
317
|
+
left side of a wide layout. `placement` and `offset` are passed to the
|
|
318
|
+
calendar's Menu.
|
|
319
|
+
|
|
320
|
+
```tsx
|
|
321
|
+
type Recency = { preset: 'today' | 'yesterday' | 'last14' | 'last30' } | { day: string }
|
|
322
|
+
|
|
323
|
+
const recency: SelectFacetCustom<Recency> = {
|
|
324
|
+
key: 'recency',
|
|
325
|
+
label: 'Recency',
|
|
326
|
+
kind: 'custom',
|
|
327
|
+
icon: 'calendar',
|
|
328
|
+
summary: (v) => ('preset' in v ? PRESET_LABELS[v.preset] : v.day || 'Custom day'),
|
|
329
|
+
render: ({ value, onChange }) => {
|
|
330
|
+
const mode = value == null ? '' : 'preset' in value ? value.preset : 'custom'
|
|
331
|
+
const day = value && 'day' in value ? value.day : ''
|
|
332
|
+
return (
|
|
333
|
+
<>
|
|
334
|
+
{DAY_PRESETS.map((p) => ( // Today / Yesterday / Last 14 / Last 30 / Custom day
|
|
335
|
+
<MenuItem
|
|
336
|
+
key={p.value}
|
|
337
|
+
label={p.label}
|
|
338
|
+
selectionIndicator="check"
|
|
339
|
+
selected={mode === p.value}
|
|
340
|
+
data-menu-open
|
|
341
|
+
onSelect={() => onChange(p.value === 'custom' ? { day } : { preset: p.value })}
|
|
342
|
+
/>
|
|
343
|
+
))}
|
|
344
|
+
{mode === 'custom' && (
|
|
345
|
+
<div data-menu-open style={{ padding: 8 }}>
|
|
346
|
+
{/* opens the calendar to the right of the field */}
|
|
347
|
+
<InputDate size="small" label="Day" placement="right-start" value={day}
|
|
348
|
+
onValueChange={(d) => onChange({ day: d })} />
|
|
349
|
+
</div>
|
|
350
|
+
)}
|
|
351
|
+
</>
|
|
352
|
+
)
|
|
353
|
+
},
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
<SelectFaceted facets={[recency]} value={value} onValueChange={setValue} />
|
|
357
|
+
// value.recency is { preset: 'last14' } or { day: '2026-07-20' } — resolve a preset to a
|
|
358
|
+
// concrete date (or take the day as-is) and filter your rows with it
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
### Props
|
|
362
|
+
|
|
363
|
+
| Prop | Type | Default | Description |
|
|
364
|
+
|------|------|---------|-------------|
|
|
365
|
+
| `facets` | SelectFacet[] | — | Categories to filter by. Each facet has a `kind` that determines its editor. |
|
|
366
|
+
| `clearable?` | boolean | true | Show the per-facet "Clear" row and the "Clear all" row. |
|
|
367
|
+
| `clearAllLabel?` | string | Clear all | Label for the "Clear all" row. |
|
|
368
|
+
| `defaultValue?` | SelectFacetedValue | — | Initial value for uncontrolled use. |
|
|
369
|
+
| `disabled?` | boolean | — | Disable the whole control. |
|
|
370
|
+
| `icon?` | IconShape | filter | Default trigger's leading icon. |
|
|
371
|
+
| `label?` | string | Filter | Default trigger's button label. |
|
|
372
|
+
| `offset?` | number | 4 | Gap in pixels between the trigger and the filter menu. |
|
|
373
|
+
| `onValueChange?` | (value, attrs) => void | — | Fires after any facet changes. `value` is the whole new record (a facet key →
|
|
374
|
+
that facet's value; a cleared facet is absent). `attrs` says what changed:
|
|
375
|
+
`{ facet, kind, value }` for a single facet edit, or `{ all: true }` for the
|
|
376
|
+
"Clear all" row — narrow on `'all' in attrs` before reading `facet`. |
|
|
377
|
+
| `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
|
|
378
|
+
menu auto-flips vertically and clamps horizontally when needed. |
|
|
379
|
+
| `priority?` | 'primary' \| 'secondary' | secondary | Default trigger's button priority. |
|
|
380
|
+
| `renderTrigger?` | (state) => ReactNode | — | Replaces the default `Button` with a trigger returned from this function.
|
|
381
|
+
Receives a `SelectFacetedTriggerState`. Return exactly one focusable element:
|
|
382
|
+
the menu is positioned relative to that element and opens when it is clicked.
|
|
383
|
+
Add `aria-haspopup="menu"` and `aria-expanded={state.open}` to the returned
|
|
384
|
+
element. `className`, `style`, and other trigger props apply only to the
|
|
385
|
+
default Button, so add styling and attributes to the returned element. |
|
|
386
|
+
| `searchable?` | boolean | — | Adds a search field at the top of the root menu. It searches the options of
|
|
387
|
+
every `single` and `multiple` facet in one list. For example, "alice" can
|
|
388
|
+
appear under application-defined Assignee and Owner facets. `text` and
|
|
389
|
+
`custom` facets remain available when the search is empty. Each facet uses
|
|
390
|
+
its `filter` function when supplied, or the built-in substring match. |
|
|
391
|
+
| `searchPlaceholder?` | string | Filter… | Placeholder for the global search field. |
|
|
392
|
+
| `size?` | 'small' \| 'medium' \| 'large' | medium | Default trigger's button size. |
|
|
393
|
+
| `toneSelected?` | 'neutral' \| 'brand' \| 'info' \| 'success' \| 'warning' \| 'critical' \| (string & {}) | — | Tone applied to a selected option row in the facet flyouts (label, selected
|
|
394
|
+
tint, and the check / checkbox indicator). A named tone or a custom CSS
|
|
395
|
+
color, matching `Select`'s `toneSelected`. Defaults to a neutral selection. |
|
|
396
|
+
| `value?` | SelectFacetedValue | — | Controlled value record, keyed by facet. When provided, update it through
|
|
397
|
+
`onValueChange`. Leave it undefined for uncontrolled use. |
|
|
398
|
+
|
|
399
|
+
### The `SelectFacet` type
|
|
400
|
+
|
|
401
|
+
`facets` takes a `SelectFacet[]`. Every facet has a `key`, `label`, and optional
|
|
402
|
+
`icon`. The `kind` field selects one of the shapes below.
|
|
403
|
+
|
|
404
|
+
`single` and `multiple` facets use the same `options`, `filter`, and `selectAll`
|
|
405
|
+
properties as [Select](./select.md). They use `kind` in place of Select's
|
|
406
|
+
`selection`. `text` and `custom` do not use Select options. A `single` facet
|
|
407
|
+
returns one option value; a `multiple` facet returns an array of option values.
|
|
408
|
+
|
|
409
|
+
| Field | Type | Description |
|
|
410
|
+
|---|---|---|
|
|
411
|
+
| `key` | `string` | The key for this facet's value in the record. `attrs.facet` returns it. It must be unique in `facets`. |
|
|
412
|
+
| `label` | `string` | The facet's row label in the menu. |
|
|
413
|
+
| `icon` | `IconShape` | Leading icon on the facet's row. |
|
|
414
|
+
|
|
415
|
+
**`single`** (`SelectFacetSingle`) selects one option. Its value is the selected
|
|
416
|
+
option's `value`, or is absent when cleared. Selecting the current option again
|
|
417
|
+
clears it.
|
|
418
|
+
|
|
419
|
+
| Field | Type | Description |
|
|
420
|
+
|---|---|---|
|
|
421
|
+
| `kind` | `'single'` | Discriminant. |
|
|
422
|
+
| `options` | `SelectItem[]` | The choices — bare strings or `SelectOption`s (groups / submenus flatten to their leaves). Option `value`s may be `string` / `number` / `boolean`. |
|
|
423
|
+
| `filter` | `FacetFilter` | Search field over this facet's options: `true` for the built-in substring match, or `(option, query) => boolean`. |
|
|
424
|
+
|
|
425
|
+
**`multiple`** (`SelectFacetMultiple`) selects any number of options. Its value
|
|
426
|
+
is an array of the selected option values. An empty array clears it.
|
|
427
|
+
|
|
428
|
+
| Field | Type | Description |
|
|
429
|
+
|---|---|---|
|
|
430
|
+
| `kind` | `'multiple'` | Discriminant. |
|
|
431
|
+
| `options` | `SelectItem[]` | As `single`. |
|
|
432
|
+
| `filter` | `FacetFilter` | As `single`. |
|
|
433
|
+
| `selectAll` | `boolean` | A "Select all" row toggling every visible option. Default `true`. |
|
|
434
|
+
| `selectAllLabel` | `string` | Label for that row. Default `Select all`. |
|
|
435
|
+
|
|
436
|
+
**`text`** (`SelectFacetText`) accepts a free-form string. It applies on Enter or
|
|
437
|
+
blur and is absent when empty.
|
|
438
|
+
|
|
439
|
+
| Field | Type | Description |
|
|
440
|
+
|---|---|---|
|
|
441
|
+
| `kind` | `'text'` | Discriminant. |
|
|
442
|
+
| `placeholder` | `string` | Placeholder for the text field. |
|
|
443
|
+
|
|
444
|
+
**`custom`** (`SelectFacetCustom<V>`) renders the editor supplied by `render`.
|
|
445
|
+
It can store a value of type `V`, including an object such as a date range.
|
|
446
|
+
|
|
447
|
+
| Field | Type | Description |
|
|
448
|
+
|---|---|---|
|
|
449
|
+
| `kind` | `'custom'` | Discriminant. |
|
|
450
|
+
| `render` | `(ctx) => ReactNode` | Renders the editor. `ctx.value` is the current `V`; `ctx.onChange(next)` sets it, and `undefined` clears the facet. `ctx.close()` closes the menu. |
|
|
451
|
+
| `summary` | `(value: V) => ReactNode` | The chip on the facet's row while a value is set. |
|