@agentero/design-system 0.31.4 → 0.31.6
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/mcp/manifests/components.html +534 -89
- package/mcp/manifests/components.json +47 -35
- package/package.json +1 -1
- package/src/combobox/combobox.d.ts +36 -8
- package/src/combobox/combobox.js +83 -49
- package/src/combobox/icons.d.ts +8 -0
- package/src/combobox/icons.js +11 -1
- package/src/combobox/index.d.ts +2 -2
- package/src/command/command.d.ts +37 -1
- package/src/command/command.js +23 -22
- package/src/command/index.d.ts +2 -1
- package/src/command/index.js +3 -3
- package/src/field/context.d.ts +7 -0
- package/src/field/field.d.ts +3 -1
- package/src/field/field.js +8 -6
- package/src/field-text/field-text.js +9 -9
- package/src/form-text/form-text.js +7 -7
|
@@ -588,13 +588,13 @@
|
|
|
588
588
|
<header>
|
|
589
589
|
<div class="wrap">
|
|
590
590
|
<h1>Manifest Debugger</h1>
|
|
591
|
-
<div class="summary"><a class="filter-pill all" data-k="all" href="#filter-all">All</a><span class="filter-pill ok" aria-disabled="true">33 components ok</span><span class="filter-pill ok" aria-disabled="true">
|
|
591
|
+
<div class="summary"><a class="filter-pill all" data-k="all" href="#filter-all">All</a><span class="filter-pill ok" aria-disabled="true">33 components ok</span><span class="filter-pill ok" aria-disabled="true">266 stories ok</span></div>
|
|
592
592
|
</div>
|
|
593
593
|
</header>
|
|
594
594
|
<main>
|
|
595
595
|
<div class="wrap">
|
|
596
596
|
<div class="note ok" style="margin-bottom: 16px;">
|
|
597
|
-
Using <code>react-component-meta</code>. Generation took <strong>
|
|
597
|
+
Using <code>react-component-meta</code>. Generation took <strong>3.9s</strong>.
|
|
598
598
|
</div>
|
|
599
599
|
<h2 class="section-title">Components</h2>
|
|
600
600
|
<div class="grid" role="list">
|
|
@@ -720,7 +720,12 @@ value?: string | string[] | undefined</code></pre>
|
|
|
720
720
|
|
|
721
721
|
<div class="hint">Exclusive accordion: one item open at a time, `collapsible` lets it close.</div>
|
|
722
722
|
<pre><code>const Default = () => <Accordion.Root type="single" collapsible enclosed={false} disabled={false}>
|
|
723
|
-
|
|
723
|
+
{FAQS.map(({ value, question, answer }) => (
|
|
724
|
+
<Accordion.Item key={value} value={value}>
|
|
725
|
+
<Accordion.Trigger>{question}</Accordion.Trigger>
|
|
726
|
+
<Accordion.Content>{answer}</Accordion.Content>
|
|
727
|
+
</Accordion.Item>
|
|
728
|
+
))}
|
|
724
729
|
</Accordion.Root>;</code></pre>
|
|
725
730
|
</div>
|
|
726
731
|
<div class="note ok">
|
|
@@ -732,7 +737,12 @@ value?: string | string[] | undefined</code></pre>
|
|
|
732
737
|
<div class="hint">`type="multiple"` lets several sections stay open at once.</div>
|
|
733
738
|
<pre><code>const Multiple = () => (
|
|
734
739
|
<Accordion.Root type="multiple" defaultValue={['covered', 'payment']}>
|
|
735
|
-
|
|
740
|
+
{FAQS.map(({ value, question, answer }) => (
|
|
741
|
+
<Accordion.Item key={value} value={value}>
|
|
742
|
+
<Accordion.Trigger>{question}</Accordion.Trigger>
|
|
743
|
+
<Accordion.Content>{answer}</Accordion.Content>
|
|
744
|
+
</Accordion.Item>
|
|
745
|
+
))}
|
|
736
746
|
</Accordion.Root>
|
|
737
747
|
);</code></pre>
|
|
738
748
|
</div>
|
|
@@ -746,10 +756,20 @@ value?: string | string[] | undefined</code></pre>
|
|
|
746
756
|
<pre><code>const Enclosed = () => (
|
|
747
757
|
<div className="flex flex-col gap-8">
|
|
748
758
|
<Accordion.Root type="multiple" defaultValue={['covered']}>
|
|
749
|
-
|
|
759
|
+
{FAQS.map(({ value, question, answer }) => (
|
|
760
|
+
<Accordion.Item key={value} value={value}>
|
|
761
|
+
<Accordion.Trigger>{question}</Accordion.Trigger>
|
|
762
|
+
<Accordion.Content>{answer}</Accordion.Content>
|
|
763
|
+
</Accordion.Item>
|
|
764
|
+
))}
|
|
750
765
|
</Accordion.Root>
|
|
751
766
|
<Accordion.Root type="multiple" enclosed defaultValue={['claim']}>
|
|
752
|
-
|
|
767
|
+
{FAQS.map(({ value, question, answer }) => (
|
|
768
|
+
<Accordion.Item key={value} value={value}>
|
|
769
|
+
<Accordion.Trigger>{question}</Accordion.Trigger>
|
|
770
|
+
<Accordion.Content>{answer}</Accordion.Content>
|
|
771
|
+
</Accordion.Item>
|
|
772
|
+
))}
|
|
753
773
|
</Accordion.Root>
|
|
754
774
|
</div>
|
|
755
775
|
);</code></pre>
|
|
@@ -801,7 +821,12 @@ value?: string | string[] | undefined</code></pre>
|
|
|
801
821
|
|
|
802
822
|
<div class="hint">`collapsible` lets clicking the open item close it (reach an all-closed state).</div>
|
|
803
823
|
<pre><code>const ExpandCollapse = () => <Accordion.Root type="single" collapsible>
|
|
804
|
-
|
|
824
|
+
{FAQS.map(({ value, question, answer }) => (
|
|
825
|
+
<Accordion.Item key={value} value={value}>
|
|
826
|
+
<Accordion.Trigger>{question}</Accordion.Trigger>
|
|
827
|
+
<Accordion.Content>{answer}</Accordion.Content>
|
|
828
|
+
</Accordion.Item>
|
|
829
|
+
))}
|
|
805
830
|
</Accordion.Root>;</code></pre>
|
|
806
831
|
</div>
|
|
807
832
|
<div class="note ok">
|
|
@@ -812,7 +837,12 @@ value?: string | string[] | undefined</code></pre>
|
|
|
812
837
|
|
|
813
838
|
<div class="hint">Without `collapsible`, clicking the already-open item keeps it open.</div>
|
|
814
839
|
<pre><code>const NonCollapsible = () => <Accordion.Root type="single" collapsible={false} defaultValue="covered">
|
|
815
|
-
|
|
840
|
+
{FAQS.map(({ value, question, answer }) => (
|
|
841
|
+
<Accordion.Item key={value} value={value}>
|
|
842
|
+
<Accordion.Trigger>{question}</Accordion.Trigger>
|
|
843
|
+
<Accordion.Content>{answer}</Accordion.Content>
|
|
844
|
+
</Accordion.Item>
|
|
845
|
+
))}
|
|
816
846
|
</Accordion.Root>;</code></pre>
|
|
817
847
|
</div>
|
|
818
848
|
</div>
|
|
@@ -2827,7 +2857,7 @@ import { CheckList } from '@agentero/design-system/check-list';
|
|
|
2827
2857
|
<div class="badges">
|
|
2828
2858
|
<label for="c-7-components-combobox-props" class="badge ok as-toggle">38 prop types</label>
|
|
2829
2859
|
|
|
2830
|
-
<label for="c-7-components-combobox-stories" class="badge ok as-toggle">
|
|
2860
|
+
<label for="c-7-components-combobox-stories" class="badge ok as-toggle">8 stories</label>
|
|
2831
2861
|
|
|
2832
2862
|
|
|
2833
2863
|
</div>
|
|
@@ -2836,8 +2866,9 @@ import { CheckList } from '@agentero/design-system/check-list';
|
|
|
2836
2866
|
<div>Root provider for an input-triggered filterable list</div>
|
|
2837
2867
|
<div class="hint">Combobox is a text input that filters a list and writes the chosen option back
|
|
2838
2868
|
into itself. Focusing the input or typing opens the list; picking a row fills the
|
|
2839
|
-
input and closes it.
|
|
2840
|
-
|
|
2869
|
+
input and closes it. With `multiple` the rows become checkbox-like toggles and the
|
|
2870
|
+
list stays open between picks. Built on Base UI, whose Combobox primitive owns
|
|
2871
|
+
the filtering, the keyboard navigation and the `role="combobox"` wiring.</div>
|
|
2841
2872
|
<div class="kv"><span class="chip">summary: Root provider for an input-triggered filterable list</span><span class="chip">see: {@link https://base-ui.com/react/components/combobox Base UI Combobox}</span><span class="chip">example: ```tsx
|
|
2842
2873
|
import { Combobox } from '@agentero/design-system/combobox';
|
|
2843
2874
|
|
|
@@ -3118,7 +3149,17 @@ virtualized?: boolean = false</code></pre>
|
|
|
3118
3149
|
<div class="row">
|
|
3119
3150
|
<span class="ex-name">Imports</span>
|
|
3120
3151
|
</div>
|
|
3121
|
-
<pre><code>import {
|
|
3152
|
+
<pre><code>import {
|
|
3153
|
+
Button,
|
|
3154
|
+
Combobox,
|
|
3155
|
+
Field,
|
|
3156
|
+
FieldText,
|
|
3157
|
+
IconSearch,
|
|
3158
|
+
Label,
|
|
3159
|
+
Modal,
|
|
3160
|
+
Skeleton,
|
|
3161
|
+
Tag,
|
|
3162
|
+
} from "@agentero/design-system";
|
|
3122
3163
|
import { Suspense } from "react";</code></pre>
|
|
3123
3164
|
</div>
|
|
3124
3165
|
|
|
@@ -3170,6 +3211,48 @@ children only while nothing matches. It needs `items` on `Root` to know that.</d
|
|
|
3170
3211
|
</Combobox.Content>
|
|
3171
3212
|
</Combobox.Root>
|
|
3172
3213
|
);</code></pre>
|
|
3214
|
+
</div>
|
|
3215
|
+
<div class="note ok">
|
|
3216
|
+
<div class="row">
|
|
3217
|
+
<span class="ex-name">Multiple Selection</span>
|
|
3218
|
+
<span class="badge ok">story ok</span>
|
|
3219
|
+
</div>
|
|
3220
|
+
|
|
3221
|
+
<div class="hint">Several values from one list, the way Lines of Authority or locations are
|
|
3222
|
+
assigned: each row leads with a checkbox-shaped indicator, picking a row toggles
|
|
3223
|
+
it, and the list stays open with the query intact so the next pick is one click
|
|
3224
|
+
away. The input never shows the selection; the field renders it alongside,
|
|
3225
|
+
here as tags.</div>
|
|
3226
|
+
<pre><code>const MultipleSelection = () => {
|
|
3227
|
+
const [selected, setSelected] = useState<string[]>([]);
|
|
3228
|
+
|
|
3229
|
+
return (
|
|
3230
|
+
<div className="flex flex-col gap-4">
|
|
3231
|
+
<Combobox.Root items={STATES} multiple value={selected} onValueChange={setSelected}>
|
|
3232
|
+
<Combobox.Input placeholder="Assign states" aria-label="Assign states" />
|
|
3233
|
+
<Combobox.Content>
|
|
3234
|
+
<Combobox.Empty>No matches found</Combobox.Empty>
|
|
3235
|
+
<Combobox.List aria-label="States">
|
|
3236
|
+
{(state: string) => (
|
|
3237
|
+
<Combobox.Item key={state} value={state}>
|
|
3238
|
+
{state}
|
|
3239
|
+
</Combobox.Item>
|
|
3240
|
+
)}
|
|
3241
|
+
</Combobox.List>
|
|
3242
|
+
</Combobox.Content>
|
|
3243
|
+
</Combobox.Root>
|
|
3244
|
+
{selected.length > 0 && (
|
|
3245
|
+
<ul aria-label="Selected states" className="flex flex-wrap gap-2">
|
|
3246
|
+
{selected.map(state => (
|
|
3247
|
+
<li key={state}>
|
|
3248
|
+
<Tag size="sm">{state}</Tag>
|
|
3249
|
+
</li>
|
|
3250
|
+
))}
|
|
3251
|
+
</ul>
|
|
3252
|
+
)}
|
|
3253
|
+
</div>
|
|
3254
|
+
);
|
|
3255
|
+
};</code></pre>
|
|
3173
3256
|
</div>
|
|
3174
3257
|
<div class="note ok">
|
|
3175
3258
|
<div class="row">
|
|
@@ -3182,7 +3265,29 @@ that suspends on the request, so a `Suspense` boundary inside `Content` shows
|
|
|
3182
3265
|
placeholder rows until they land. Nothing tracks `loading` by hand and no stale
|
|
3183
3266
|
list lingers under the wait: the data is read by query, which is also what makes
|
|
3184
3267
|
out-of-order responses harmless. Drop a `useSuspenseQuery` where `use()` is.</div>
|
|
3185
|
-
<pre><code>const AsyncSearch = () =>
|
|
3268
|
+
<pre><code>const AsyncSearch = () => {
|
|
3269
|
+
const [query, setQuery] = useState('');
|
|
3270
|
+
|
|
3271
|
+
return (
|
|
3272
|
+
// No `items` and `filter={null}`: the server did the filtering, the child renders what came
|
|
3273
|
+
// back, and `isItemEqualToValue` keeps a re-fetched copy counting as the selected one.
|
|
3274
|
+
<Combobox.Root
|
|
3275
|
+
filter={null}
|
|
3276
|
+
itemToStringLabel={(agency: Agency) => agency.name}
|
|
3277
|
+
isItemEqualToValue={(a: Agency, b: Agency) => a.id === b.id}
|
|
3278
|
+
onInputValueChange={(value, { reason }) => {
|
|
3279
|
+
// Picking a row writes its label into the input; that is not a new search.
|
|
3280
|
+
if (reason !== 'item-press') setQuery(value);
|
|
3281
|
+
}}>
|
|
3282
|
+
<Combobox.Input placeholder="Search agencies" aria-label="Search agencies" />
|
|
3283
|
+
<Combobox.Content>
|
|
3284
|
+
<Suspense fallback={<SkeletonRows />}>
|
|
3285
|
+
<AgencyItems query={query} />
|
|
3286
|
+
</Suspense>
|
|
3287
|
+
</Combobox.Content>
|
|
3288
|
+
</Combobox.Root>
|
|
3289
|
+
);
|
|
3290
|
+
};</code></pre>
|
|
3186
3291
|
</div>
|
|
3187
3292
|
<div class="note ok">
|
|
3188
3293
|
<div class="row">
|
|
@@ -3194,7 +3299,54 @@ out-of-order responses harmless. Drop a `useSuspenseQuery` where `use()` is.</di
|
|
|
3194
3299
|
its data layer instead: `filter={null}`, the rows arrive through `filteredItems`,
|
|
3195
3300
|
`Content` is marked busy and `Status` announces the wait. The list is cleared
|
|
3196
3301
|
before each request and a request that is no longer the latest is dropped.</div>
|
|
3197
|
-
<pre><code>const AsyncSearchWithStatus = () =>
|
|
3302
|
+
<pre><code>const AsyncSearchWithStatus = () => {
|
|
3303
|
+
const [results, setResults] = useState<Agency[]>([]);
|
|
3304
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
3305
|
+
// Only the latest request may land; fast typing returns responses out of order.
|
|
3306
|
+
const latestRequest = useRef(0);
|
|
3307
|
+
|
|
3308
|
+
const onInputValueChange = async (query: string) => {
|
|
3309
|
+
const request = ++latestRequest.current;
|
|
3310
|
+
|
|
3311
|
+
// The previous rows go before the wait starts: a list that no longer matches what is typed
|
|
3312
|
+
// must not sit under the loading state.
|
|
3313
|
+
setResults([]);
|
|
3314
|
+
setIsLoading(true);
|
|
3315
|
+
const agencies = await searchAgencies(query);
|
|
3316
|
+
if (request !== latestRequest.current) return;
|
|
3317
|
+
|
|
3318
|
+
setResults(agencies);
|
|
3319
|
+
setIsLoading(false);
|
|
3320
|
+
};
|
|
3321
|
+
|
|
3322
|
+
return (
|
|
3323
|
+
// `filter={null}` hands filtering to the server; `filteredItems` is what came back.
|
|
3324
|
+
<Combobox.Root
|
|
3325
|
+
items={results}
|
|
3326
|
+
filteredItems={results}
|
|
3327
|
+
filter={null}
|
|
3328
|
+
itemToStringLabel={(agency: Agency) => agency.name}
|
|
3329
|
+
isItemEqualToValue={(a: Agency, b: Agency) => a.id === b.id}
|
|
3330
|
+
onInputValueChange={onInputValueChange}>
|
|
3331
|
+
<Combobox.Input placeholder="Search agencies" aria-label="Search agencies" />
|
|
3332
|
+
<Combobox.Content aria-busy={isLoading || undefined}>
|
|
3333
|
+
{/* The rows show the wait; `Status` only announces it. */}
|
|
3334
|
+
<Combobox.Status className="sr-only">
|
|
3335
|
+
{isLoading ? 'Searching…' : undefined}
|
|
3336
|
+
</Combobox.Status>
|
|
3337
|
+
{isLoading && <SkeletonRows />}
|
|
3338
|
+
<Combobox.Empty>{isLoading ? undefined : 'No agencies found'}</Combobox.Empty>
|
|
3339
|
+
<Combobox.List>
|
|
3340
|
+
{(agency: Agency) => (
|
|
3341
|
+
<Combobox.Item key={agency.id} value={agency}>
|
|
3342
|
+
{agency.name}
|
|
3343
|
+
</Combobox.Item>
|
|
3344
|
+
)}
|
|
3345
|
+
</Combobox.List>
|
|
3346
|
+
</Combobox.Content>
|
|
3347
|
+
</Combobox.Root>
|
|
3348
|
+
);
|
|
3349
|
+
};</code></pre>
|
|
3198
3350
|
</div>
|
|
3199
3351
|
<div class="note ok">
|
|
3200
3352
|
<div class="row">
|
|
@@ -3204,7 +3356,10 @@ before each request and a request that is no longer the latest is dropped.</div>
|
|
|
3204
3356
|
|
|
3205
3357
|
<div class="hint">Dropped into a `FieldText`, the input takes the label, the description and
|
|
3206
3358
|
the error from the field with nothing wired by hand: `Combobox.Input` reads the
|
|
3207
|
-
same `InputContext` the plain `Input` does
|
|
3359
|
+
same `InputContext` the plain `Input` does. It also points `aria-labelledby`
|
|
3360
|
+
at the label, so the name survives the list opening: Base UI then hides the
|
|
3361
|
+
rest of the page from assistive technology, label included, and a
|
|
3362
|
+
`<label for>` alone would stop naming the input.</div>
|
|
3208
3363
|
<pre><code>const InsideFieldText = () => (
|
|
3209
3364
|
<FieldText invalid required>
|
|
3210
3365
|
<Label>Agency</Label>
|
|
@@ -3242,7 +3397,7 @@ padded past it.</div>
|
|
|
3242
3397
|
<div className="relative">
|
|
3243
3398
|
<IconSearch
|
|
3244
3399
|
aria-hidden
|
|
3245
|
-
className="pointer-events-none absolute top-1/2 left-3 z-
|
|
3400
|
+
className="pointer-events-none absolute top-1/2 left-3 -z-hide size-6 -translate-y-1/2 [&>path]:fill-icon-default-base-tertiary"
|
|
3246
3401
|
/>
|
|
3247
3402
|
<Combobox.Input className="pl-11" placeholder="Search states" aria-label="Search states" />
|
|
3248
3403
|
</div>
|
|
@@ -3313,7 +3468,7 @@ list nor treats a click in it as a click outside and closes.</div>
|
|
|
3313
3468
|
<div class="badges">
|
|
3314
3469
|
<label for="c-8-components-command-props" class="badge ok as-toggle">10 prop types</label>
|
|
3315
3470
|
|
|
3316
|
-
<label for="c-8-components-command-stories" class="badge ok as-toggle">
|
|
3471
|
+
<label for="c-8-components-command-stories" class="badge ok as-toggle">7 stories</label>
|
|
3317
3472
|
|
|
3318
3473
|
|
|
3319
3474
|
</div>
|
|
@@ -3322,7 +3477,9 @@ list nor treats a click in it as a click outside and closes.</div>
|
|
|
3322
3477
|
<div>Filterable command palette container</div>
|
|
3323
3478
|
<div class="hint">Command is a filterable list built on `cmdk`. Type to narrow the options,
|
|
3324
3479
|
navigate with the arrow keys, select with Enter. It carries its own panel
|
|
3325
|
-
chrome, so dropping it inside a `Popover.Content` yields a combobox
|
|
3480
|
+
chrome, so dropping it inside a `Popover.Content` yields a combobox.
|
|
3481
|
+
`useCommandState` reads the search text and results of the surrounding
|
|
3482
|
+
`Command.Root` from any component rendered inside it.</div>
|
|
3326
3483
|
<div class="kv"><span class="chip">summary: Filterable command palette container</span><span class="chip">see: {@link https://github.com/pacocoursey/cmdk cmdk}</span><span class="chip">example: ```tsx
|
|
3327
3484
|
import { Command } from '@agentero/design-system/command';
|
|
3328
3485
|
|
|
@@ -3544,6 +3701,23 @@ list does not invite the pointer.</div>
|
|
|
3544
3701
|
</Command.Item>
|
|
3545
3702
|
</Command.List>
|
|
3546
3703
|
<Command.Empty>No results found.</Command.Empty>
|
|
3704
|
+
</Command.Root>;</code></pre>
|
|
3705
|
+
</div>
|
|
3706
|
+
<div class="note ok">
|
|
3707
|
+
<div class="row">
|
|
3708
|
+
<span class="ex-name">Reading State</span>
|
|
3709
|
+
<span class="badge ok">story ok</span>
|
|
3710
|
+
</div>
|
|
3711
|
+
|
|
3712
|
+
<div class="hint">`useCommandState` reads the surrounding `Root` from a component inside it. Here the row
|
|
3713
|
+
offering to create an agency appears only when the search text matches no existing one.</div>
|
|
3714
|
+
<pre><code>const ReadingState = () => <Command.Root label="Search agencies">
|
|
3715
|
+
<Command.Input placeholder="Search agencies..." />
|
|
3716
|
+
<Command.List>
|
|
3717
|
+
<Command.Item>Pinnacle Shield Insurance</Command.Item>
|
|
3718
|
+
<Command.Item>Horizon Risk Solutions</Command.Item>
|
|
3719
|
+
<CreateItem />
|
|
3720
|
+
</Command.List>
|
|
3547
3721
|
</Command.Root>;</code></pre>
|
|
3548
3722
|
</div>
|
|
3549
3723
|
</div>
|
|
@@ -5089,7 +5263,7 @@ required?: boolean = false</code></pre>
|
|
|
5089
5263
|
<div class="row">
|
|
5090
5264
|
<span class="ex-name">Imports</span>
|
|
5091
5265
|
</div>
|
|
5092
|
-
<pre><code>import { Field, FieldContext, Label } from "@agentero/design-system";</code></pre>
|
|
5266
|
+
<pre><code>import { Field, FieldContext, Label, Switch } from "@agentero/design-system";</code></pre>
|
|
5093
5267
|
</div>
|
|
5094
5268
|
|
|
5095
5269
|
|
|
@@ -5317,7 +5491,7 @@ label with its description, or a control with its messages, in
|
|
|
5317
5491
|
`Field.Content`. A control sets its own width: `Input` is `w-full`, so it
|
|
5318
5492
|
gets one here.</div>
|
|
5319
5493
|
<pre><code>const Horizontal = () => (
|
|
5320
|
-
<Field.Group className="w-
|
|
5494
|
+
<Field.Group className="w-160">
|
|
5321
5495
|
<Field.Root orientation="horizontal" data-testid="short">
|
|
5322
5496
|
<Label>Full name</Label>
|
|
5323
5497
|
<DemoInput defaultValue="Rafa Moro" className="w-72" />
|
|
@@ -5336,7 +5510,7 @@ gets one here.</div>
|
|
|
5336
5510
|
<Label>Auto-renew</Label>
|
|
5337
5511
|
<Field.Description>Renews the policy automatically before it expires.</Field.Description>
|
|
5338
5512
|
</Field.Content>
|
|
5339
|
-
<
|
|
5513
|
+
<DemoSwitch className="self-center" />
|
|
5340
5514
|
</Field.Root>
|
|
5341
5515
|
|
|
5342
5516
|
<div className="w-[20rem]" data-testid="narrow">
|
|
@@ -5360,7 +5534,7 @@ measures the field itself, so it needs no particular wrapper. A width that
|
|
|
5360
5534
|
should only apply once horizontal goes behind `@md/field:`.</div>
|
|
5361
5535
|
<pre><code>const Responsive = () => (
|
|
5362
5536
|
<div className="flex flex-col gap-8">
|
|
5363
|
-
<div className="w-full max-w-
|
|
5537
|
+
<div className="w-full max-w-160" data-testid="wide">
|
|
5364
5538
|
<Field.Root orientation="responsive">
|
|
5365
5539
|
<Label>Email</Label>
|
|
5366
5540
|
<Field.Content className="@md/field:w-72">
|
|
@@ -5455,7 +5629,27 @@ part wins over what `Field.Root` provides.</div>
|
|
|
5455
5629
|
errors, a bare `<Field.Error />` renders them — here with `useState` and no
|
|
5456
5630
|
form library. `InputContext` extends the same way with `value`, `onChange`,
|
|
5457
5631
|
`onBlur` and `name`; see `FieldText`.</div>
|
|
5458
|
-
<pre><code>const ErrorsFromContext = () =>
|
|
5632
|
+
<pre><code>const ErrorsFromContext = () => {
|
|
5633
|
+
const [value, setValue] = useState('');
|
|
5634
|
+
const [touched, setTouched] = useState(false);
|
|
5635
|
+
|
|
5636
|
+
const errors = touched && !value ? [{ message: 'Required' }] : undefined;
|
|
5637
|
+
|
|
5638
|
+
return (
|
|
5639
|
+
<Field.Root invalid={!!errors} required>
|
|
5640
|
+
<Label>Email</Label>
|
|
5641
|
+
<DemoInput
|
|
5642
|
+
type="email"
|
|
5643
|
+
value={value}
|
|
5644
|
+
onChange={event => setValue(event.target.value)}
|
|
5645
|
+
onBlur={() => setTouched(true)}
|
|
5646
|
+
/>
|
|
5647
|
+
<ErrorProvider errors={errors}>
|
|
5648
|
+
<Field.Error />
|
|
5649
|
+
</ErrorProvider>
|
|
5650
|
+
</Field.Root>
|
|
5651
|
+
);
|
|
5652
|
+
};</code></pre>
|
|
5459
5653
|
</div>
|
|
5460
5654
|
</div>
|
|
5461
5655
|
|
|
@@ -5703,7 +5897,29 @@ through untouched.</div>
|
|
|
5703
5897
|
`<Input />` becomes controlled, and the errors to `FieldContext`, so a bare
|
|
5704
5898
|
`<Field.Error />` renders them — here with `useState` and no form library.
|
|
5705
5899
|
The input's own `onChange` still runs, chained after the provider's.</div>
|
|
5706
|
-
<pre><code>const ControlledFromContext = () =>
|
|
5900
|
+
<pre><code>const ControlledFromContext = () => {
|
|
5901
|
+
const [value, setValue] = useState('');
|
|
5902
|
+
const [touched, setTouched] = useState(false);
|
|
5903
|
+
|
|
5904
|
+
const errors = touched && !value ? [{ message: 'Required' }] : undefined;
|
|
5905
|
+
|
|
5906
|
+
return (
|
|
5907
|
+
<FieldText invalid={!!errors} required>
|
|
5908
|
+
<ContextExtension
|
|
5909
|
+
inputProps={{
|
|
5910
|
+
name: 'email',
|
|
5911
|
+
value,
|
|
5912
|
+
onChange: event => setValue(event.target.value),
|
|
5913
|
+
onBlur: () => setTouched(true)
|
|
5914
|
+
}}
|
|
5915
|
+
errors={errors}>
|
|
5916
|
+
<Label>Email</Label>
|
|
5917
|
+
<Input type="email" onChange={onOwnChange} />
|
|
5918
|
+
<Field.Error />
|
|
5919
|
+
</ContextExtension>
|
|
5920
|
+
</FieldText>
|
|
5921
|
+
);
|
|
5922
|
+
};</code></pre>
|
|
5707
5923
|
</div>
|
|
5708
5924
|
</div>
|
|
5709
5925
|
|
|
@@ -5820,7 +6036,34 @@ onSubmit?: SubmitHandler<AgencyValues></code></pre>
|
|
|
5820
6036
|
both errors, marks both inputs invalid and focuses the first one — proof that
|
|
5821
6037
|
react-hook-form's `ref` reached the inputs. Once filled in, `onSubmit`
|
|
5822
6038
|
receives the typed values.</div>
|
|
5823
|
-
<pre><code>const Default = () =>
|
|
6039
|
+
<pre><code>const Default = () => {
|
|
6040
|
+
const methods = useForm<AgencyValues>({ defaultValues: { agencyName: '', email: '' } });
|
|
6041
|
+
|
|
6042
|
+
return (
|
|
6043
|
+
<Form.Root
|
|
6044
|
+
methods={methods}
|
|
6045
|
+
onSubmit={onSubmit}
|
|
6046
|
+
aria-label="Agency profile"
|
|
6047
|
+
className="flex flex-col items-start gap-7">
|
|
6048
|
+
<Field.Group>
|
|
6049
|
+
<FormText
|
|
6050
|
+
name="agencyName"
|
|
6051
|
+
label="Agency name"
|
|
6052
|
+
required
|
|
6053
|
+
rules={{ required: 'Enter the agency name.' }}
|
|
6054
|
+
/>
|
|
6055
|
+
<FormText
|
|
6056
|
+
name="email"
|
|
6057
|
+
label="Email"
|
|
6058
|
+
required
|
|
6059
|
+
rules={{ required: 'Enter the agency email.' }}
|
|
6060
|
+
inputProps={{ type: 'email', autoComplete: 'email' }}
|
|
6061
|
+
/>
|
|
6062
|
+
</Field.Group>
|
|
6063
|
+
<Button type="submit">Save</Button>
|
|
6064
|
+
</Form.Root>
|
|
6065
|
+
);
|
|
6066
|
+
};</code></pre>
|
|
5824
6067
|
</div>
|
|
5825
6068
|
<div class="note ok">
|
|
5826
6069
|
<div class="row">
|
|
@@ -5832,7 +6075,20 @@ receives the typed values.</div>
|
|
|
5832
6075
|
announces it, but the form renders `noValidate`: without `rules`, submitting
|
|
5833
6076
|
the field empty is not blocked by the browser and `onSubmit` still runs.
|
|
5834
6077
|
Validation is react-hook-form's alone.</div>
|
|
5835
|
-
<pre><code>const NoValidate = () =>
|
|
6078
|
+
<pre><code>const NoValidate = () => {
|
|
6079
|
+
const methods = useForm<NicknameValues>({ defaultValues: { nickname: '' } });
|
|
6080
|
+
|
|
6081
|
+
return (
|
|
6082
|
+
<Form.Root
|
|
6083
|
+
methods={methods}
|
|
6084
|
+
onSubmit={onSubmit}
|
|
6085
|
+
aria-label="Display name"
|
|
6086
|
+
className="flex flex-col items-start gap-7">
|
|
6087
|
+
<FormText name="nickname" label="Display name" required />
|
|
6088
|
+
<Button type="submit">Save</Button>
|
|
6089
|
+
</Form.Root>
|
|
6090
|
+
);
|
|
6091
|
+
};</code></pre>
|
|
5836
6092
|
</div>
|
|
5837
6093
|
</div>
|
|
5838
6094
|
|
|
@@ -5907,7 +6163,7 @@ with the `FieldText`, `Label`, `Input` and `Field.*` primitives directly.</div>
|
|
|
5907
6163
|
* a `Form.Root`. A `control` whose resolver transforms the values is
|
|
5908
6164
|
* accepted as is.
|
|
5909
6165
|
*/
|
|
5910
|
-
control?: Control<FieldValues, any,
|
|
6166
|
+
control?: Control<FieldValues, any, unknown>
|
|
5911
6167
|
|
|
5912
6168
|
/**
|
|
5913
6169
|
* `id` for the control. Generated when omitted. Set it here, not on the
|
|
@@ -6033,14 +6289,29 @@ tooltipSide?: "top" | "bottom" | "left" | "ri
|
|
|
6033
6289
|
<div class="hint">The field consumers write: a `name`, a `label` and the validation rule.
|
|
6034
6290
|
Submitting empty renders the error and marks the input invalid; typing
|
|
6035
6291
|
clears both and the value reaches `onSubmit`.</div>
|
|
6036
|
-
<pre><code>const Default = () =>
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
|
|
6040
|
-
|
|
6041
|
-
|
|
6042
|
-
|
|
6043
|
-
|
|
6292
|
+
<pre><code>const Default = () => {
|
|
6293
|
+
const methods = useForm<AgencyValues>({ defaultValues: { agencyName: '' } });
|
|
6294
|
+
|
|
6295
|
+
return (
|
|
6296
|
+
<Form.Root
|
|
6297
|
+
methods={methods}
|
|
6298
|
+
onSubmit={onSubmit}
|
|
6299
|
+
aria-label="Agency"
|
|
6300
|
+
className="flex flex-col items-start gap-7">
|
|
6301
|
+
<FormText
|
|
6302
|
+
rules={{ required: 'Enter the agency name.' }}
|
|
6303
|
+
inputProps={{ placeholder: 'Acme Insurance' }}
|
|
6304
|
+
label="Agency name"
|
|
6305
|
+
orientation="vertical"
|
|
6306
|
+
required
|
|
6307
|
+
optional={false}
|
|
6308
|
+
disabled={false}
|
|
6309
|
+
readOnly={false}
|
|
6310
|
+
name="agencyName" />
|
|
6311
|
+
<Button type="submit">Save</Button>
|
|
6312
|
+
</Form.Root>
|
|
6313
|
+
);
|
|
6314
|
+
};</code></pre>
|
|
6044
6315
|
</div>
|
|
6045
6316
|
<div class="note ok">
|
|
6046
6317
|
<div class="row">
|
|
@@ -6052,7 +6323,21 @@ clears both and the value reaches `onSubmit`.</div>
|
|
|
6052
6323
|
`aria-describedby`. `tooltip` is an info button beside the label for details
|
|
6053
6324
|
worth a click; it is a sibling of the `<label>`, so the field's accessible
|
|
6054
6325
|
name stays the label text.</div>
|
|
6055
|
-
<pre><code>const WithDescriptionAndTooltip = () =>
|
|
6326
|
+
<pre><code>const WithDescriptionAndTooltip = () => {
|
|
6327
|
+
const methods = useForm<TaxValues>({ defaultValues: { taxId: '' } });
|
|
6328
|
+
|
|
6329
|
+
return (
|
|
6330
|
+
<Form.Root methods={methods} aria-label="Tax details">
|
|
6331
|
+
<FormText
|
|
6332
|
+
name="taxId"
|
|
6333
|
+
label="Tax ID"
|
|
6334
|
+
tooltip="The EIN the IRS issued to the agency."
|
|
6335
|
+
description="Nine digits, with or without the dash."
|
|
6336
|
+
inputProps={{ inputMode: 'numeric', autoComplete: 'off' }}
|
|
6337
|
+
/>
|
|
6338
|
+
</Form.Root>
|
|
6339
|
+
);
|
|
6340
|
+
};</code></pre>
|
|
6056
6341
|
</div>
|
|
6057
6342
|
<div class="note ok">
|
|
6058
6343
|
<div class="row">
|
|
@@ -6063,7 +6348,15 @@ name stays the label text.</div>
|
|
|
6063
6348
|
<div class="hint">The forms mark optional fields rather than required ones: `optional`
|
|
6064
6349
|
appends a muted suffix to the label and leaves the input without the
|
|
6065
6350
|
`required` attribute.</div>
|
|
6066
|
-
<pre><code>const Optional = () =>
|
|
6351
|
+
<pre><code>const Optional = () => {
|
|
6352
|
+
const methods = useForm<PhoneValues>({ defaultValues: { phone: '' } });
|
|
6353
|
+
|
|
6354
|
+
return (
|
|
6355
|
+
<Form.Root methods={methods} aria-label="Contact">
|
|
6356
|
+
<FormText name="phone" label="Phone number" optional inputProps={{ type: 'tel' }} />
|
|
6357
|
+
</Form.Root>
|
|
6358
|
+
);
|
|
6359
|
+
};</code></pre>
|
|
6067
6360
|
</div>
|
|
6068
6361
|
<div class="note ok">
|
|
6069
6362
|
<div class="row">
|
|
@@ -6074,7 +6367,28 @@ appends a muted suffix to the label and leaves the input without the
|
|
|
6074
6367
|
<div class="hint">`rules` are react-hook-form's, and `name` accepts nested paths typed against
|
|
6075
6368
|
the form's values through the generic. `rules.required` validates but does
|
|
6076
6369
|
not mark the field required: that stays an explicit `required` prop.</div>
|
|
6077
|
-
<pre><code>const WithRules = () =>
|
|
6370
|
+
<pre><code>const WithRules = () => {
|
|
6371
|
+
const methods = useForm<LicenseValues>({ defaultValues: { agency: { npn: '' } } });
|
|
6372
|
+
|
|
6373
|
+
return (
|
|
6374
|
+
<Form.Root
|
|
6375
|
+
methods={methods}
|
|
6376
|
+
onSubmit={onSubmit}
|
|
6377
|
+
aria-label="License"
|
|
6378
|
+
className="flex flex-col items-start gap-7">
|
|
6379
|
+
<FormText<LicenseValues>
|
|
6380
|
+
name="agency.npn"
|
|
6381
|
+
label="NPN"
|
|
6382
|
+
rules={{
|
|
6383
|
+
required: 'Enter your NPN.',
|
|
6384
|
+
pattern: { value: /^\d{6,10}$/, message: 'An NPN is 6 to 10 digits.' }
|
|
6385
|
+
}}
|
|
6386
|
+
inputProps={{ inputMode: 'numeric' }}
|
|
6387
|
+
/>
|
|
6388
|
+
<Button type="submit">Save</Button>
|
|
6389
|
+
</Form.Root>
|
|
6390
|
+
);
|
|
6391
|
+
};</code></pre>
|
|
6078
6392
|
</div>
|
|
6079
6393
|
<div class="note ok">
|
|
6080
6394
|
<div class="row">
|
|
@@ -6086,7 +6400,36 @@ not mark the field required: that stays an explicit `required` prop.</div>
|
|
|
6086
6400
|
that failed instead of stopping at the first, and the field lists them all.
|
|
6087
6401
|
With the default `'firstError'` a single message is rendered, as in the other
|
|
6088
6402
|
stories.</div>
|
|
6089
|
-
<pre><code>const AllErrors = () =>
|
|
6403
|
+
<pre><code>const AllErrors = () => {
|
|
6404
|
+
const methods = useForm<PasswordValues>({
|
|
6405
|
+
defaultValues: { password: '' },
|
|
6406
|
+
criteriaMode: 'all'
|
|
6407
|
+
});
|
|
6408
|
+
|
|
6409
|
+
return (
|
|
6410
|
+
<Form.Root
|
|
6411
|
+
methods={methods}
|
|
6412
|
+
onSubmit={onSubmit}
|
|
6413
|
+
aria-label="Account security"
|
|
6414
|
+
className="flex flex-col items-start gap-7">
|
|
6415
|
+
<FormText
|
|
6416
|
+
name="password"
|
|
6417
|
+
label="Password"
|
|
6418
|
+
required
|
|
6419
|
+
inputProps={{ type: 'password' }}
|
|
6420
|
+
rules={{
|
|
6421
|
+
required: 'Enter a password.',
|
|
6422
|
+
validate: {
|
|
6423
|
+
uppercase: value => /[A-Z]/.test(value) || 'At least one uppercase letter.',
|
|
6424
|
+
number: value => /[0-9]/.test(value) || 'At least one number.',
|
|
6425
|
+
length: value => value.length >= 10 || 'At least 10 characters.'
|
|
6426
|
+
}
|
|
6427
|
+
}}
|
|
6428
|
+
/>
|
|
6429
|
+
<Button type="submit">Save</Button>
|
|
6430
|
+
</Form.Root>
|
|
6431
|
+
);
|
|
6432
|
+
};</code></pre>
|
|
6090
6433
|
</div>
|
|
6091
6434
|
<div class="note ok">
|
|
6092
6435
|
<div class="row">
|
|
@@ -6098,7 +6441,25 @@ stories.</div>
|
|
|
6098
6441
|
`maxLength`, `size`… — and its handlers. A consumer `onChange` runs after
|
|
6099
6442
|
react-hook-form's: by the time it fires, `getValues` already returns the new
|
|
6100
6443
|
value.</div>
|
|
6101
|
-
<pre><code>const InputPropsChaining = () =>
|
|
6444
|
+
<pre><code>const InputPropsChaining = () => {
|
|
6445
|
+
const methods = useForm<PhoneValues>({ defaultValues: { phone: '' } });
|
|
6446
|
+
|
|
6447
|
+
return (
|
|
6448
|
+
<Form.Root methods={methods} aria-label="Mobile">
|
|
6449
|
+
<FormText
|
|
6450
|
+
name="phone"
|
|
6451
|
+
label="Mobile"
|
|
6452
|
+
inputProps={{
|
|
6453
|
+
type: 'tel',
|
|
6454
|
+
placeholder: '5550000000',
|
|
6455
|
+
maxLength: 10,
|
|
6456
|
+
size: 'lg',
|
|
6457
|
+
onChange: event => onOwnChange(methods.getValues('phone'), event.target.value)
|
|
6458
|
+
}}
|
|
6459
|
+
/>
|
|
6460
|
+
</Form.Root>
|
|
6461
|
+
);
|
|
6462
|
+
};</code></pre>
|
|
6102
6463
|
</div>
|
|
6103
6464
|
<div class="note ok">
|
|
6104
6465
|
<div class="row">
|
|
@@ -6110,7 +6471,33 @@ value.</div>
|
|
|
6110
6471
|
control in a row, and `className` on the root or in `inputProps` sizes them.
|
|
6111
6472
|
The `Input` is full-width by default, so a settings row gives it a width.
|
|
6112
6473
|
`responsive` stacks the field below 28rem and rows it from there.</div>
|
|
6113
|
-
<pre><code>const Horizontal = () =>
|
|
6474
|
+
<pre><code>const Horizontal = () => {
|
|
6475
|
+
const methods = useForm<ProfileValues>({
|
|
6476
|
+
defaultValues: { fullName: 'Rafa Moro', title: '' }
|
|
6477
|
+
});
|
|
6478
|
+
|
|
6479
|
+
return (
|
|
6480
|
+
<Form.Root methods={methods} aria-label="Profile" className="w-[40rem]">
|
|
6481
|
+
<Field.Group>
|
|
6482
|
+
<FormText
|
|
6483
|
+
name="fullName"
|
|
6484
|
+
label="Full name"
|
|
6485
|
+
orientation="horizontal"
|
|
6486
|
+
inputProps={{ className: 'w-72' }}
|
|
6487
|
+
data-testid="short"
|
|
6488
|
+
/>
|
|
6489
|
+
<FormText
|
|
6490
|
+
name="title"
|
|
6491
|
+
label="Title"
|
|
6492
|
+
description="Shown on your public profile."
|
|
6493
|
+
orientation="horizontal"
|
|
6494
|
+
inputProps={{ className: 'w-72', placeholder: 'Software engineer' }}
|
|
6495
|
+
data-testid="long"
|
|
6496
|
+
/>
|
|
6497
|
+
</Field.Group>
|
|
6498
|
+
</Form.Root>
|
|
6499
|
+
);
|
|
6500
|
+
};</code></pre>
|
|
6114
6501
|
</div>
|
|
6115
6502
|
<div class="note ok">
|
|
6116
6503
|
<div class="row">
|
|
@@ -6121,7 +6508,25 @@ The `Input` is full-width by default, so a settings row gives it a width.
|
|
|
6121
6508
|
<div class="hint">`disabled` disables the input and marks the field, and the value still
|
|
6122
6509
|
reaches `onSubmit` — unlike react-hook-form's own `disabled` option, which
|
|
6123
6510
|
drops it. Use `readOnly` for a value the user can copy but not edit.</div>
|
|
6124
|
-
<pre><code>const Disabled = () =>
|
|
6511
|
+
<pre><code>const Disabled = () => {
|
|
6512
|
+
const methods = useForm<AccountValues>({
|
|
6513
|
+
defaultValues: { agencyId: 'AG-48213', email: 'ops@acme-insurance.com' }
|
|
6514
|
+
});
|
|
6515
|
+
|
|
6516
|
+
return (
|
|
6517
|
+
<Form.Root
|
|
6518
|
+
methods={methods}
|
|
6519
|
+
onSubmit={onSubmit}
|
|
6520
|
+
aria-label="Account"
|
|
6521
|
+
className="flex flex-col items-start gap-7">
|
|
6522
|
+
<Field.Group>
|
|
6523
|
+
<FormText name="agencyId" label="Agency ID" disabled />
|
|
6524
|
+
<FormText name="email" label="Email" inputProps={{ type: 'email' }} />
|
|
6525
|
+
</Field.Group>
|
|
6526
|
+
<Button type="submit">Save</Button>
|
|
6527
|
+
</Form.Root>
|
|
6528
|
+
);
|
|
6529
|
+
};</code></pre>
|
|
6125
6530
|
</div>
|
|
6126
6531
|
<div class="note ok">
|
|
6127
6532
|
<div class="row">
|
|
@@ -6132,7 +6537,26 @@ drops it. Use `readOnly` for a value the user can copy but not edit.</div>
|
|
|
6132
6537
|
<div class="hint">react-hook-form's `ref` reaches the `<input>`, so `setFocus` and the focus
|
|
6133
6538
|
on the first invalid field after submit both work with no ref written by
|
|
6134
6539
|
hand.</div>
|
|
6135
|
-
<pre><code>const SetFocus = () =>
|
|
6540
|
+
<pre><code>const SetFocus = () => {
|
|
6541
|
+
const methods = useForm<AgencyValues & PhoneValues>({
|
|
6542
|
+
defaultValues: { agencyName: '', phone: '' }
|
|
6543
|
+
});
|
|
6544
|
+
|
|
6545
|
+
return (
|
|
6546
|
+
<Form.Root
|
|
6547
|
+
methods={methods}
|
|
6548
|
+
aria-label="Agency contact"
|
|
6549
|
+
className="flex flex-col items-start gap-7">
|
|
6550
|
+
<Field.Group>
|
|
6551
|
+
<FormText name="agencyName" label="Agency name" />
|
|
6552
|
+
<FormText name="phone" label="Phone number" inputProps={{ type: 'tel' }} />
|
|
6553
|
+
</Field.Group>
|
|
6554
|
+
<Button type="button" variant="secondary" onClick={() => methods.setFocus('phone')}>
|
|
6555
|
+
Focus phone
|
|
6556
|
+
</Button>
|
|
6557
|
+
</Form.Root>
|
|
6558
|
+
);
|
|
6559
|
+
};</code></pre>
|
|
6136
6560
|
</div>
|
|
6137
6561
|
</div>
|
|
6138
6562
|
|
|
@@ -7101,7 +7525,30 @@ open?: boolean</code></pre>
|
|
|
7101
7525
|
</div>
|
|
7102
7526
|
|
|
7103
7527
|
<div class="hint">Controlled with `open`/`onOpenChange` — the dominant pattern in both apps. Also closes on `Escape`.</div>
|
|
7104
|
-
<pre><code>const Controlled = () =>
|
|
7528
|
+
<pre><code>const Controlled = () => {
|
|
7529
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
7530
|
+
|
|
7531
|
+
return (
|
|
7532
|
+
<>
|
|
7533
|
+
<Button variant="secondary" onClick={() => setIsOpen(true)}>
|
|
7534
|
+
Open controlled
|
|
7535
|
+
</Button>
|
|
7536
|
+
<Modal.Root open={isOpen} onOpenChange={setIsOpen}>
|
|
7537
|
+
<Modal.Content>
|
|
7538
|
+
<Modal.Title>Controlled modal</Modal.Title>
|
|
7539
|
+
<Modal.Body>
|
|
7540
|
+
<p>The open state lives in the consumer.</p>
|
|
7541
|
+
</Modal.Body>
|
|
7542
|
+
<Modal.Footer>
|
|
7543
|
+
<Button variant="primary" onClick={() => setIsOpen(false)}>
|
|
7544
|
+
Done
|
|
7545
|
+
</Button>
|
|
7546
|
+
</Modal.Footer>
|
|
7547
|
+
</Modal.Content>
|
|
7548
|
+
</Modal.Root>
|
|
7549
|
+
</>
|
|
7550
|
+
);
|
|
7551
|
+
};</code></pre>
|
|
7105
7552
|
</div>
|
|
7106
7553
|
<div class="note ok">
|
|
7107
7554
|
<div class="row">
|
|
@@ -8273,52 +8720,50 @@ keeps its padding.</div>
|
|
|
8273
8720
|
</div>
|
|
8274
8721
|
<div>Expandable rows with a detail panel</div>
|
|
8275
8722
|
<div class="hint">`Table.ExpandButton` toggles a `Table.ExpandedRow` detail panel under its row.</div>
|
|
8276
|
-
<pre><code>const ExpandableRows =
|
|
8277
|
-
const
|
|
8278
|
-
|
|
8279
|
-
|
|
8280
|
-
|
|
8281
|
-
|
|
8282
|
-
|
|
8283
|
-
|
|
8284
|
-
|
|
8285
|
-
|
|
8286
|
-
|
|
8287
|
-
|
|
8288
|
-
|
|
8289
|
-
|
|
8290
|
-
|
|
8291
|
-
|
|
8292
|
-
|
|
8293
|
-
|
|
8294
|
-
|
|
8295
|
-
|
|
8296
|
-
|
|
8297
|
-
|
|
8298
|
-
|
|
8299
|
-
|
|
8300
|
-
|
|
8723
|
+
<pre><code>const ExpandableRows = () => {
|
|
8724
|
+
const [openId, setOpenId] = useState<string | null>(ROWS[0]!.id);
|
|
8725
|
+
|
|
8726
|
+
return (
|
|
8727
|
+
<Table.Root size="md" embed={false}>
|
|
8728
|
+
<Table.Head>
|
|
8729
|
+
<Table.Row>
|
|
8730
|
+
<Table.Header>
|
|
8731
|
+
<span className="sr-only">Expand</span>
|
|
8732
|
+
</Table.Header>
|
|
8733
|
+
<Table.Header>Name</Table.Header>
|
|
8734
|
+
<Table.Header>Email</Table.Header>
|
|
8735
|
+
<Table.Header>Role</Table.Header>
|
|
8736
|
+
</Table.Row>
|
|
8737
|
+
</Table.Head>
|
|
8738
|
+
<Table.Body>
|
|
8739
|
+
{ROWS.slice(0, 4).map(row => (
|
|
8740
|
+
<Fragment key={row.id}>
|
|
8741
|
+
<Table.Row>
|
|
8742
|
+
<Table.Cell>
|
|
8743
|
+
<Table.ExpandButton
|
|
8744
|
+
isExpanded={openId === row.id}
|
|
8745
|
+
toggleExpanded={() => setOpenId(openId === row.id ? null : row.id)}
|
|
8746
|
+
aria-label={`Toggle ${row.name}`}
|
|
8747
|
+
/>
|
|
8748
|
+
</Table.Cell>
|
|
8749
|
+
<Table.Cell className="font-bold">{row.name}</Table.Cell>
|
|
8750
|
+
<Table.Cell className="text-text-default-base-secondary">{row.email}</Table.Cell>
|
|
8751
|
+
<Table.Cell>{row.role}</Table.Cell>
|
|
8752
|
+
</Table.Row>
|
|
8753
|
+
{openId === row.id && (
|
|
8754
|
+
<Table.ExpandedRow>
|
|
8755
|
+
<Table.Cell colSpan={4}>
|
|
8756
|
+
<div className="py-2 text-text-default-base-secondary">
|
|
8757
|
+
Extended details for {row.name} — {row.amount} lifetime value.
|
|
8758
|
+
</div>
|
|
8301
8759
|
</Table.Cell>
|
|
8302
|
-
|
|
8303
|
-
|
|
8304
|
-
|
|
8305
|
-
|
|
8306
|
-
|
|
8307
|
-
|
|
8308
|
-
|
|
8309
|
-
<div className="py-2 text-text-default-base-secondary">
|
|
8310
|
-
Extended details for {row.name} — {row.amount} lifetime value.
|
|
8311
|
-
</div>
|
|
8312
|
-
</Table.Cell>
|
|
8313
|
-
</Table.ExpandedRow>
|
|
8314
|
-
)}
|
|
8315
|
-
</Fragment>
|
|
8316
|
-
))}
|
|
8317
|
-
</Table.Body>
|
|
8318
|
-
</Table.Root>
|
|
8319
|
-
);
|
|
8320
|
-
};
|
|
8321
|
-
return <Demo />;
|
|
8760
|
+
</Table.ExpandedRow>
|
|
8761
|
+
)}
|
|
8762
|
+
</Fragment>
|
|
8763
|
+
))}
|
|
8764
|
+
</Table.Body>
|
|
8765
|
+
</Table.Root>
|
|
8766
|
+
);
|
|
8322
8767
|
};</code></pre>
|
|
8323
8768
|
</div>
|
|
8324
8769
|
</div>
|