@nxgt/material 2.18.1 → 2.20.0
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/CHANGELOG.md +101 -0
- package/README.md +1 -0
- package/dist/{_Uint8Array-C7apDrb7.js → _Uint8Array-DGVY6NfN.js} +1 -1
- package/dist/{_overRest-CCAbV-eT.js → _overRest-DCKNcrF0.js} +1 -1
- package/dist/{async-slice-Cuekts5f.js → async-slice-DAVXvBm1.js} +3 -3
- package/dist/{blockDiagram-BEXU5L5S-iNiTd2F6.js → blockDiagram-BEXU5L5S-BrCVxVRd.js} +2 -2
- package/dist/{chunk-3FUC2YCW-C3_88DyD.js → chunk-3FUC2YCW-DWaNSANO.js} +2 -2
- package/dist/{chunk-7M6MHVWA-DSA5AJyK.js → chunk-7M6MHVWA-x-N4hnls.js} +1 -1
- package/dist/{chunk-GNY47TPC-AFxr6Xhs.js → chunk-GNY47TPC-Dzk3o85s.js} +4 -4
- package/dist/{chunk-NETBCI7D-DUZYHta0.js → chunk-NETBCI7D-WvAAUJne.js} +1 -1
- package/dist/{chunk-Z7XXMR3K-DDFBvLN4.js → chunk-Z7XXMR3K-DY6U0Uq4.js} +1 -1
- package/dist/{classDiagram-v2-NBCMYWYE-Cx1q9jwX.js → classDiagram-v2-NBCMYWYE-QeFmLU9L.js} +2 -2
- package/dist/components/base-content.css +64 -4
- package/dist/components/base.css +3 -1
- package/dist/{components-CEXxj5QP.js → components-DH5dpaU8.js} +24401 -23964
- package/dist/components.js +6 -6
- package/dist/{dagre-6A5THRUB-B3FAoh2m.js → dagre-6A5THRUB-D-NdHe_L.js} +11 -11
- package/dist/{diagram-22UHCM2B-B1mxqDwI.js → diagram-22UHCM2B-DZ4jejn3.js} +2 -2
- package/dist/{diagram-3UASUU5V-DKbBxWI5.js → diagram-3UASUU5V-DRoFk6qc.js} +1 -1
- package/dist/dnd.js +1 -1
- package/dist/{elk-276RUBZZ-CeiRDj87.js → elk-276RUBZZ-BG0kHR2f.js} +3 -3
- package/dist/{erDiagram-OPXOYQCR-nyCpuYg6.js → erDiagram-OPXOYQCR-B6XRmJuU.js} +2 -2
- package/dist/{field-blur-DNHXOY0J.js → field-blur-B1VUvTws.js} +972 -946
- package/dist/{flowDiagram-KWPJA3E3-CAt7iv16.js → flowDiagram-KWPJA3E3-BghxpN0G.js} +3 -3
- package/dist/{ganttDiagram-FUAMR5RP-g4YEKVC-.js → ganttDiagram-FUAMR5RP-C3aoaoXQ.js} +1 -1
- package/dist/{graphlib-DxjKaXeR.js → graphlib-Dc5Siwxn.js} +6 -6
- package/dist/hooks.js +2 -2
- package/dist/{i18n-provider-Dv9ZSmgO.js → i18n-provider-Cq_VScPv.js} +1 -1
- package/dist/i18n.js +6 -6
- package/dist/{isArrayLikeObject-H8k6W51O.js → isArrayLikeObject-DeHDPOEB.js} +3 -3
- package/dist/{isEmpty-B0Bavg-e.js → isEmpty-Sic-RRp7.js} +3 -3
- package/dist/{journeyDiagram-ZHPQQLJL-v7NsJ_vl.js → journeyDiagram-ZHPQQLJL-DcUX0asi.js} +1 -1
- package/dist/lib/components/ui/collections/data-table/data-table-density.d.ts +15 -0
- package/dist/lib/components/ui/collections/data-table/data-table.d.ts +1 -1
- package/dist/lib/components/ui/collections/data-table/types.d.ts +7 -0
- package/dist/lib/components/ui/fields/index.d.ts +1 -0
- package/dist/lib/components/ui/fields/repeatable-group/index.d.ts +2 -0
- package/dist/lib/components/ui/fields/repeatable-group/partials/repeatable-group-footer.d.ts +14 -0
- package/dist/lib/components/ui/fields/repeatable-group/partials/repeatable-group-row.d.ts +19 -0
- package/dist/lib/components/ui/fields/repeatable-group/partials/repeatable-group-rows.d.ts +21 -0
- package/dist/lib/components/ui/fields/repeatable-group/partials/repeatable-group-status.d.ts +11 -0
- package/dist/lib/components/ui/fields/repeatable-group/repeatable-group-classes.d.ts +33 -0
- package/dist/lib/components/ui/fields/repeatable-group/repeatable-group-form-field.d.ts +28 -0
- package/dist/lib/components/ui/fields/repeatable-group/repeatable-group-names.d.ts +16 -0
- package/dist/lib/components/ui/fields/repeatable-group/repeatable-group.d.ts +12 -0
- package/dist/lib/components/ui/fields/repeatable-group/types.d.ts +45 -0
- package/dist/lib/components/ui/fields/repeatable-group/use-repeatable-group.d.ts +32 -0
- package/dist/lib/components/ui/fields/search-field/search-field.d.ts +7 -1
- package/dist/lib/components/ui/overlays/command/command-item.d.ts +10 -0
- package/dist/lib/components/ui/overlays/command/command.d.ts +5 -2
- package/dist/lib/components/ui/overlays/dialog/dialog-outside.d.ts +4 -2
- package/dist/lib/components/ui/overlays/dialog/dialog-parts.d.ts +7 -0
- package/dist/lib/components/ui/overlays/dialog/dialog.d.ts +2 -5
- package/dist/lib/components/ui/overlays/dialog/in-dialog.d.ts +13 -0
- package/dist/lib/components/ui/overlays/menubar/menubar-items.d.ts +18 -0
- package/dist/lib/components/ui/overlays/menubar/menubar-sub.d.ts +8 -0
- package/dist/lib/components/ui/overlays/menubar/menubar.d.ts +3 -21
- package/dist/lib/components/ui/overlays/sheet/sheet-parts.d.ts +7 -0
- package/dist/lib/components/ui/overlays/sheet/sheet.d.ts +2 -5
- package/dist/lib/components/ui/primitives/typography/typography.d.ts +2 -1
- package/dist/lib/components/ui/selection/autocomplete/partials/autocomplete-chips.d.ts +2 -0
- package/dist/lib/components/ui/selection/autocomplete/partials/autocomplete-popup.d.ts +2 -1
- package/dist/lib/components/ui/selection/checkbox-group/checkbox-group.d.ts +2 -0
- package/dist/lib/components/ui/selection/combobox/combobox-content.d.ts +14 -0
- package/dist/lib/components/ui/selection/combobox/combobox.d.ts +1 -4
- package/dist/lib/components/ui/selection/option-count/option-count.d.ts +18 -0
- package/dist/lib/components/ui/selection/radio-group/radio-group.d.ts +2 -0
- package/dist/lib/components/ui/selection/select-chip-field/next-selection.d.ts +10 -0
- package/dist/lib/components/ui/selection/select-chip-field/select-chip-field.d.ts +8 -1
- package/dist/lib/components/ui/selection/select-chip-field/select-chip-header.d.ts +15 -0
- package/dist/lib/components/ui/selection/select-chip-field/select-chip-option.d.ts +13 -0
- package/dist/lib/i18n/resources/en/en.d.ts +15 -0
- package/dist/lib/i18n/resources/en/material.json.d.ts +15 -0
- package/dist/lib/i18n/resources/fr/fr.d.ts +15 -0
- package/dist/lib/i18n/resources/fr/material.json.d.ts +15 -0
- package/dist/lib/i18n/resources/resources.d.ts +30 -0
- package/dist/lib/i18n/translator.d.ts +1 -1
- package/dist/lib.js +2 -2
- package/dist/main.js +16 -16
- package/dist/map.js +8 -8
- package/dist/material.css +1 -1
- package/dist/{merge-Js8537dg.js → merge-VyOzRzYB.js} +4 -4
- package/dist/{mermaid.core-BHmjz3IJ.js → mermaid.core-DLu6PrQ9.js} +21 -21
- package/dist/{mindmap-definition-NLK3R4M7-xOeGZSSo.js → mindmap-definition-NLK3R4M7-B7Y8Yx12.js} +2 -2
- package/dist/{pieDiagram-5QR66LMP-5nOKLXqH.js → pieDiagram-5QR66LMP-Bv9VMy5P.js} +1 -1
- package/dist/print.js +2 -2
- package/dist/{quadrantDiagram-O4NWA36T-B0RNQlBB.js → quadrantDiagram-O4NWA36T-Dr2CLVD8.js} +1 -1
- package/dist/{requirementDiagram-PLB6GJNP-Cw85zSzP.js → requirementDiagram-PLB6GJNP-XuNWo4K8.js} +2 -2
- package/dist/{sankeyDiagram-IPEJSGJF-APZoNvA9.js → sankeyDiagram-IPEJSGJF-DzXzUNs5.js} +1 -1
- package/dist/{sortable-provider-CUT5Wfdl.js → sortable-provider-XneXCqY1.js} +1 -1
- package/dist/{stateDiagram-v2-GCMORJYK-CfSjJkc3.js → stateDiagram-v2-GCMORJYK-BN2OBfZa.js} +2 -2
- package/dist/{stored-language-BKoohv2_.js → stored-language-DqlfjXt8.js} +26 -0
- package/dist/{swimlanes-2SLR337P-Bv2VKn_d.js → swimlanes-2SLR337P-Ma7iLhhV.js} +3 -3
- package/dist/{swimlanesDiagram-TC7HE7FX-Jk8do14f.js → swimlanesDiagram-TC7HE7FX-DWBnIppx.js} +3 -3
- package/dist/{timeline-definition-EJHVYXUP-Bc40K9sk.js → timeline-definition-EJHVYXUP-BBwvY1Xf.js} +1 -1
- package/dist/{translate-material-CCJg3yfc.js → translate-material-B6HO6ykL.js} +2 -2
- package/dist/{translator-DEde7BKz.js → translator-B_lmafJN.js} +1 -1
- package/dist/{use-heading-spy-XkSExwoT.js → use-heading-spy-D1Ifooi5.js} +2 -2
- package/dist/{use-material-translation-Cndoq_iK.js → use-material-translation-Bp4ECeT4.js} +2 -2
- package/dist/{usecaseDiagram-POWQR4AR-CKd5UWWw.js → usecaseDiagram-POWQR4AR-B1HduAbh.js} +2 -2
- package/dist/{xychartDiagram-PMCCYNJV-CQJbSV7N.js → xychartDiagram-PMCCYNJV-ByjPOY-5.js} +1 -1
- package/docs/components/README.md +1 -1
- package/docs/components/data-table-filter-and-lists.md +3 -1
- package/docs/components/form-fields.md +146 -0
- package/docs/components/overlays-and-menus.md +2 -0
- package/docs/components/primitives.md +3 -1
- package/docs/components/selection-and-choice.md +20 -4
- package/docs/guides/forms.md +84 -0
- package/docs/guides/i18n.md +2 -1
- package/docs/previews/form-fields-repeatablegroup.webp +0 -0
- package/docs/roadmap.md +7 -0
- package/docs/troubleshooting.md +69 -0
- package/lib/components/base-content.css +64 -4
- package/lib/components/base.css +3 -1
- package/package.json +2 -2
|
@@ -609,6 +609,8 @@ Parts: `Command`, `CommandDialog`, `CommandInput`, `CommandList`,
|
|
|
609
609
|
| --- | --- | --- | --- |
|
|
610
610
|
| `onSelect` (`CommandItem`) | `(value: string) => void` | — | Enter or click on the item |
|
|
611
611
|
| `value` / `keywords` (`CommandItem`) | `string` / `string[]` | the item's text | what the filter matches |
|
|
612
|
+
| `checked` (`CommandItem`) | `boolean` | — | sets `aria-checked` and shows a checkbox indicator, for a multi-choice list. cmdk's `aria-selected` keeps meaning "highlighted" |
|
|
613
|
+
| `multiselectable` (`CommandList`) | `boolean` | — | sets `aria-multiselectable` on the list |
|
|
612
614
|
| `shouldFilter` (`Command`) | `boolean` | `true` | `false` when you filter the items yourself (server search) |
|
|
613
615
|
| `title` / `description` (`CommandDialog`) | `string` | `"Command Palette"` / `"Search for a command to run..."` (translated) | the dialog's title and description, inside the dialog and visually hidden |
|
|
614
616
|
| `open`, `onOpenChange` (`CommandDialog`) | as `Dialog` | — | `CommandDialog` takes every `Dialog` prop |
|
|
@@ -176,14 +176,16 @@ import { Typography } from '@nxgt/material/components';
|
|
|
176
176
|
| Prop | Type | Default | Effect |
|
|
177
177
|
| --- | --- | --- | --- |
|
|
178
178
|
| `variant` | `'huge-large' \| 'huge-medium' \| 'huge-small' \| 'headline-larger' \| 'headline-large' \| 'headline-medium' \| 'headline-small' \| 'title-large' \| 'title-medium' \| 'title-small' \| 'body-medium' \| 'body-small' \| 'caption' \| 'normal'` | `'normal'` | size and weight; `caption` is also muted |
|
|
179
|
+
| `tone` | `'default' \| 'muted' \| 'primary' \| 'error'` | `'default'` | text colour: `muted` is `text-muted-foreground`, `primary` is `text-primary`, `error` is `text-error-text`; `default` inherits (a `className` colour still wins) |
|
|
179
180
|
| `as` | an intrinsic tag name (`keyof JSX.IntrinsicElements`) | `'div'` | the element to render, keeping the variant, e.g. `as="h2"` or `as="span"` |
|
|
180
181
|
| `asChild` | `boolean` | `false` | merge the classes onto the single child instead of rendering an element |
|
|
181
182
|
|
|
182
|
-
It renders a `<div>` unless `as` or `asChild` says otherwise:
|
|
183
|
+
It renders a `<div>` unless `as` or `asChild` says otherwise. Use `as="span"` inside a `<button>` or a link, where a `<div>` is invalid HTML. It sets no `overflow`, so it is never a scroll region: text with break points wraps, while an unbreakable string can overflow its box (add `wrap-anywhere` or `min-w-0`; put your own wrapper with `overflow-auto` and `tabIndex={0}` around it to scroll):
|
|
183
184
|
|
|
184
185
|
```tsx
|
|
185
186
|
<Typography as="span" variant="title-small">Inline</Typography>
|
|
186
187
|
<Typography asChild variant="body-medium"><a href="/docs">Docs</a></Typography>
|
|
188
|
+
<Typography tone="muted" variant="body-medium">Jan 3 to Jan 9</Typography>
|
|
187
189
|
```
|
|
188
190
|
|
|
189
191
|
The variants are also exported as `typographyVariants` for use on your own elements.
|
|
@@ -427,7 +427,7 @@ export function Frameworks() {
|
|
|
427
427
|
|
|
428
428
|
| Prop | Type | Default | Effect |
|
|
429
429
|
| --- | --- | --- | --- |
|
|
430
|
-
| `options` | `{ value: string; label: ReactNode }[]` | — | the boxes |
|
|
430
|
+
| `options` | `{ value: string; label: ReactNode; count?: number }[]` | — | the boxes; `count`: see [option counts](#option-counts). |
|
|
431
431
|
| `value` | `string[]` | `[]` | the checked values |
|
|
432
432
|
| `onValueChange` | `(value: string[]) => void` | — | the new list on each toggle |
|
|
433
433
|
| `orientation` | `'vertical' \| 'horizontal'` | `'horizontal'` | layout |
|
|
@@ -446,6 +446,10 @@ error with no message (`setError(name, { type: 'server' })`) keeps your
|
|
|
446
446
|
the group's description is "<helperText> This value is not valid." and the error
|
|
447
447
|
is not only a red colour.
|
|
448
448
|
|
|
449
|
+
#### Option counts
|
|
450
|
+
|
|
451
|
+
`count` on an option of `CheckboxGroup`, `RadioGroup` or `SelectChipField` shows a number after the label, in muted `tabular-nums` text, and joins the accessible name: the number as plain text plus a visually hidden unit word, so the option is named "CE1 24 results" (it starts with the visible label). The unit is the ICU plural `material.option-count.label` (`result` / `results`; `résultat` / `résultats` in French), so override it in your resources for another noun. `0` is a valid count.
|
|
452
|
+
|
|
449
453
|
**Emits:** `string[]`.
|
|
450
454
|
**Form wrapper:** `CheckboxGroupFormField` — `control` and `name`, see
|
|
451
455
|
[forms](../guides/forms.md#the-pair) and [what each field emits](../guides/forms.md#what-each-field-emits).
|
|
@@ -482,7 +486,7 @@ full naming alignment is planned for 3.0.
|
|
|
482
486
|
|
|
483
487
|
| Prop | Type | Default | Effect |
|
|
484
488
|
| --- | --- | --- | --- |
|
|
485
|
-
| `options` | `{ value: string; label: ReactNode; disabled?: boolean }[]` | — | the radios; `disabled` on an option disables only it |
|
|
489
|
+
| `options` | `{ value: string; label: ReactNode; disabled?: boolean; count?: number }[]` | — | the radios; `disabled` on an option disables only it; `count`: see [option counts](#option-counts). |
|
|
486
490
|
| `value` / `defaultValue` | `string` | — | Radix radio group state |
|
|
487
491
|
| `onValueChange` | `(value: string) => void` | — | |
|
|
488
492
|
| `orientation` | `'vertical' \| 'horizontal'` | `'horizontal'` | layout |
|
|
@@ -610,12 +614,24 @@ export function Topics() {
|
|
|
610
614
|
}
|
|
611
615
|
```
|
|
612
616
|
|
|
617
|
+
With counts, and a single choice that stays chosen when pressed again:
|
|
618
|
+
|
|
619
|
+
```tsx
|
|
620
|
+
const levels = [
|
|
621
|
+
{ value: 'ce1', label: 'CE1', count: 24 },
|
|
622
|
+
{ value: 'ce2', label: 'CE2', count: 1 },
|
|
623
|
+
];
|
|
624
|
+
|
|
625
|
+
<SelectChipField label="Level" options={levels} value={value} onValueChange={setValue} deselectable={false} />
|
|
626
|
+
```
|
|
627
|
+
|
|
613
628
|
| Prop | Type | Default | Effect |
|
|
614
629
|
| --- | --- | --- | --- |
|
|
615
|
-
| `mode` | `'single' \| 'multiple'` | `'single'` | clicking the selected chip in `single` mode clears it |
|
|
630
|
+
| `mode` | `'single' \| 'multiple'` | `'single'` | clicking the selected chip in `single` mode clears it, unless `deselectable` is `false` |
|
|
631
|
+
| `deselectable` | `boolean` | `true` | `false`: in `single` mode a second press keeps the chosen chip chosen, as a radio group does; another chip still replaces it |
|
|
616
632
|
| `value` | `string[]` | `[]` | the selected values, even in `single` mode |
|
|
617
633
|
| `onValueChange` | `(value: string[]) => void` | — | |
|
|
618
|
-
| `options` | `{ value
|
|
634
|
+
| `options` | `{ value: string; label: string; icon?: ReactNode; count?: number }[]` | — | the chips; `count`: see [option counts](#option-counts). |
|
|
619
635
|
|
|
620
636
|
**Accessibility:** the chips are toggle buttons in a `role="group"` named by
|
|
621
637
|
`label` and described by the help text: a chosen chip is
|
package/docs/guides/forms.md
CHANGED
|
@@ -270,6 +270,55 @@ in `[data-slot="form-actions"]`) and put each group of fields in a
|
|
|
270
270
|
fields still take a row of their own. A rule separates each section from the
|
|
271
271
|
one before it (`separated={false}` drops it).
|
|
272
272
|
|
|
273
|
+
### One scroll, sticky actions
|
|
274
|
+
|
|
275
|
+
The form grid has no height of its own and its fields no scroll box: a long
|
|
276
|
+
form grows with the page, and the page is the one scroll. The actions
|
|
277
|
+
(`[data-slot="form-actions"]`) are sticky at the bottom of whatever scrolls
|
|
278
|
+
the form, on a translucent, blurred background, so Cancel and Save stay in
|
|
279
|
+
reach while the fields scroll under them; a form shorter than the screen
|
|
280
|
+
shows them right under its fields.
|
|
281
|
+
|
|
282
|
+
- In a `DialogContent` or a `SheetContent`, which are capped to the screen
|
|
283
|
+
and scroll themselves, the actions stick to the dialog's or the sheet's
|
|
284
|
+
bottom. A `DrawerContent` does not scroll: a form that is its direct child
|
|
285
|
+
is the scroll box there.
|
|
286
|
+
- In an `AlertDialogContent` (the rich text editor's edit dialogs use one),
|
|
287
|
+
the actions stick to its bottom the same way, over its padding.
|
|
288
|
+
- `ConfirmationDialog` holds a form when you pass it `formOptions` (a schema, its `defaultValues` and `renderForm`): it renders the
|
|
289
|
+
fields in a `.form-grid-layout` with no `form-actions`, so its footer
|
|
290
|
+
(Cancel, Confirm) is what sticks to the dialog's bottom. Any `Dialog` or
|
|
291
|
+
`AlertDialog` whose content holds a `.form-grid-layout` without
|
|
292
|
+
`form-actions` gets the same sticky footer.
|
|
293
|
+
- A focused control or link is scrolled into view clear of the actions and
|
|
294
|
+
a line of helper text below it (`scroll-margin-bottom` on the fields'
|
|
295
|
+
controls: 6rem, plus a dialog's bottom padding, 7.5rem above a
|
|
296
|
+
`ConfirmationDialog`'s stacked footer), so a field you tab to is never
|
|
297
|
+
hidden behind them (WCAG 2.4.11). It is set on the controls, so it holds
|
|
298
|
+
in a scroll box of your own too.
|
|
299
|
+
- Taller actions (a third button, labels that wrap) or helper text on
|
|
300
|
+
several lines need more room: raise `--form-actions-clearance` on the
|
|
301
|
+
form. A dialog whose bottom padding is not the default 1.5rem (`p-0` and
|
|
302
|
+
`p-4` are recognised) sets `--form-actions-fill` to it:
|
|
303
|
+
|
|
304
|
+
```css
|
|
305
|
+
.edit-school-form {
|
|
306
|
+
--form-actions-clearance: 9rem;
|
|
307
|
+
}
|
|
308
|
+
.edit-school-dialog {
|
|
309
|
+
--form-actions-fill: 2rem; /* its pb-8 */
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
```tsx
|
|
314
|
+
<form className="form-grid-layout edit-school-form">
|
|
315
|
+
```
|
|
316
|
+
- To scroll a form in a box of your own, give it a height and the scroll
|
|
317
|
+
(`className="form-grid-layout max-h-[60vh] overflow-y-auto"`); to keep the
|
|
318
|
+
actions in place, give them `className="static"`. The
|
|
319
|
+
[troubleshooting entry](../troubleshooting.md#a-forms-fields-no-longer-scroll-in-their-own-box-after-upgrading)
|
|
320
|
+
has the details.
|
|
321
|
+
|
|
273
322
|
## Validation messages
|
|
274
323
|
|
|
275
324
|
`zodLocaleError()` from `@nxgt/material/lib` is a Zod v4 error map that
|
|
@@ -567,6 +616,41 @@ export function SupplierForm({ onSave }: { onSave: (values: Values) => void }) {
|
|
|
567
616
|
renders one input per attribute and writes `{ [attribute.name]: value }` to
|
|
568
617
|
its `name`.
|
|
569
618
|
|
|
619
|
+
### Rows a user adds and removes
|
|
620
|
+
|
|
621
|
+
When the rows are fields of their own — a school year's periods, each with a
|
|
622
|
+
name, a start date and an end date — use
|
|
623
|
+
[`RepeatableGroupFormField`](../components/form-fields.md#repeatablegroup)
|
|
624
|
+
over `useFieldArray`, and render each row's fields with the form's own
|
|
625
|
+
wrappers:
|
|
626
|
+
|
|
627
|
+
```tsx
|
|
628
|
+
<RepeatableGroupFormField
|
|
629
|
+
control={form.control}
|
|
630
|
+
name="periods"
|
|
631
|
+
label="Periods"
|
|
632
|
+
rowLabel="period"
|
|
633
|
+
max={6}
|
|
634
|
+
reorderable
|
|
635
|
+
newRow={() => ({ name: '' })}
|
|
636
|
+
renderRow={({ index }) => (
|
|
637
|
+
<>
|
|
638
|
+
<TextFormField control={form.control} name={`periods.${index}.name`} label="Name" />
|
|
639
|
+
<DateFormField control={form.control} name={`periods.${index}.start`} label="Start date" />
|
|
640
|
+
</>
|
|
641
|
+
)}
|
|
642
|
+
/>
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
- A row's field shows its own error (`periods.1.name`); the array's own error
|
|
646
|
+
(`.min(2)` on the array, or a `useFieldArray` rule) shows under the rows and
|
|
647
|
+
describes the group.
|
|
648
|
+
- The group moves the focus as rows come and go (an added row's first field,
|
|
649
|
+
a removed row's neighbour or Add, a moved row's move button) and announces each change politely
|
|
650
|
+
("Added period 3"). A row your own code appends takes
|
|
651
|
+
`{ shouldFocus: false }`
|
|
652
|
+
([why](../troubleshooting.md#the-focus-jumps-away-from-a-new-row-of-a-repeatablegroupformfield)).
|
|
653
|
+
|
|
570
654
|
## Config-driven forms
|
|
571
655
|
|
|
572
656
|
`FormField` renders the field that matches a config `type`, which is useful
|
package/docs/guides/i18n.md
CHANGED
|
@@ -209,7 +209,8 @@ single value's input, when nothing names the slider, `material.slider.value`
|
|
|
209
209
|
("Value", "Valeur"). `CopyButton` reads `material.copy.copy`, `copied` and `failed` ("Copy", "Copied",
|
|
210
210
|
"Copy failed"). The other icon-only buttons are named from it too: the
|
|
211
211
|
`Sidebar` toggle (`material.sidebar.toggle`), the data table's row expander
|
|
212
|
-
(`material.table.labels.toggle-row`)
|
|
212
|
+
(`material.table.labels.toggle-row`) and its header, a hidden "Expand" / "Déplier"
|
|
213
|
+
(`material.table.labels.expand`), its loading status, "Loading data…" /
|
|
213
214
|
"Chargement des données…" (`material.table.loading`), its scroll region,
|
|
214
215
|
"Scrollable table" / "Tableau défilant"
|
|
215
216
|
(`material.table.labels.scroll-region`, or the `scrollLabel` prop), a chip's remove button — `Chip`,
|
|
Binary file
|
package/docs/roadmap.md
CHANGED
|
@@ -157,6 +157,13 @@ version they shipped in, and planned entries may name the major they target.
|
|
|
157
157
|
|
|
158
158
|
## Shipped
|
|
159
159
|
|
|
160
|
+
- **Rows a user adds, removes and reorders** — `RepeatableGroup` and
|
|
161
|
+
`RepeatableGroupFormField` over the form library's field array: rows from
|
|
162
|
+
`renderRow`, a list for assistive tech, Remove and move buttons named after
|
|
163
|
+
the row ("Remove period 2"), `max` and `min`, an empty state, focus that
|
|
164
|
+
follows each change, polite announcements, and the array's own error under
|
|
165
|
+
the rows. One column on a phone. 2.19.0.
|
|
166
|
+
|
|
160
167
|
- **Signed-out pages and two-step verification** — `AuthCard`, the frame of a
|
|
161
168
|
sign-in page (one `main`, the card's title its `h1`, a status or alert
|
|
162
169
|
notice, safe-area padding); `EnrolmentSecret`, a TOTP enrolment's QR code,
|
package/docs/troubleshooting.md
CHANGED
|
@@ -28,6 +28,7 @@ words in your error.
|
|
|
28
28
|
- [A Content-Security-Policy reports a `data:` image on every form with a required field](#a-content-security-policy-reports-a-data-image-on-every-form-with-a-required-field)
|
|
29
29
|
- [A Button's label overflows a card on a phone](#a-buttons-label-overflows-a-card-on-a-phone)
|
|
30
30
|
- [A dialog or sheet scrolls, or clips a child, after upgrading](#a-dialog-or-sheet-scrolls-or-clips-a-child-after-upgrading)
|
|
31
|
+
- [A form's fields no longer scroll in their own box after upgrading](#a-forms-fields-no-longer-scroll-in-their-own-box-after-upgrading)
|
|
31
32
|
|
|
32
33
|
**Feedback**
|
|
33
34
|
|
|
@@ -130,6 +131,7 @@ words in your error.
|
|
|
130
131
|
- ["Required" shows as soon as the calendar opens, or while a date is being picked](#required-shows-as-soon-as-the-calendar-opens-or-while-a-date-is-being-picked)
|
|
131
132
|
- [`TypeError: values.map is not a function` (a `SliderFormField`)](#typeerror-valuesmap-is-not-a-function)
|
|
132
133
|
- [An end-to-end test no longer finds `aria-expanded` on a `ResponsiveButton`](#an-end-to-end-test-no-longer-finds-aria-expanded-on-a-responsivebutton)
|
|
134
|
+
- [The focus jumps away from a new row of a `RepeatableGroupFormField`](#the-focus-jumps-away-from-a-new-row-of-a-repeatablegroupformfield)
|
|
133
135
|
- [A multi-step form reopens at a later step](#a-multi-step-form-reopens-at-a-later-step)
|
|
134
136
|
- [An end-to-end test no longer finds a `LinkButton` by the `button` role](#an-end-to-end-test-no-longer-finds-a-linkbutton-by-the-button-role)
|
|
135
137
|
- [A test no longer finds a date or time picker by the `button` role, or a slider thumb by the exact name `Minimum`](#a-test-no-longer-finds-a-date-or-time-picker-by-the-button-role-or-a-slider-thumb-by-the-exact-name-minimum)
|
|
@@ -162,6 +164,7 @@ words in your error.
|
|
|
162
164
|
- [A `className` on `ActivityLayout` now styles the whole shell](#a-classname-on-activitylayout-now-styles-the-whole-shell)
|
|
163
165
|
- [A test finds a new heading or a second `main`, or no longer finds a title in a `div`](#a-test-finds-a-new-heading-or-a-second-main-or-no-longer-finds-a-title-in-a-div)
|
|
164
166
|
- [`[data-test="typogaphy"]` finds nothing](#data-testtypogaphy-finds-nothing)
|
|
167
|
+
- [Long text in `Typography` no longer scrolls](#long-text-in-typography-no-longer-scrolls)
|
|
165
168
|
- [A test no longer finds a tooltip on an entry of an expanded sidebar](#a-test-no-longer-finds-a-tooltip-on-an-entry-of-an-expanded-sidebar)
|
|
166
169
|
- [A test no longer finds a calendar day by its number, or by "Today" or "selected"](#a-test-no-longer-finds-a-calendar-day-by-its-number-or-by-today-or-selected)
|
|
167
170
|
- [`#<name>` no longer finds a `SwitchFormField` or `CheckboxFormField`](#name-no-longer-finds-a-switchformfield-or-checkboxformfield)
|
|
@@ -641,6 +644,32 @@ with `className="max-h-none overflow-visible"`; to change the cap, pass your own
|
|
|
641
644
|
For a dialog that should grow taller than the screen and scroll inside the
|
|
642
645
|
overlay instead of capping, use `DialogScrollContent`.
|
|
643
646
|
|
|
647
|
+
### A form's fields no longer scroll in their own box after upgrading
|
|
648
|
+
|
|
649
|
+
**When:** after upgrading, a `.form-grid-layout` form grows with the page
|
|
650
|
+
instead of scrolling its fields in a box of its own, Cancel and Save stay at
|
|
651
|
+
the bottom of the screen, or a form inside a box you gave a fixed height
|
|
652
|
+
overflows it.
|
|
653
|
+
**Why:** the form grid used to cap its height
|
|
654
|
+
(`max-h-[calc(100lvh-184px)]`, more from `md`) and scroll
|
|
655
|
+
`[data-slot="form-fields"]` inside it, so a page had two scrolls, one inside
|
|
656
|
+
the other. It now has no cap and no scroll of its own: the page, or the
|
|
657
|
+
`DialogContent` / `SheetContent` around it, is the one scroll, and
|
|
658
|
+
`[data-slot="form-actions"]` is sticky at its bottom on a blurred background.
|
|
659
|
+
A `Drawer`'s content does not scroll, so a form that is a direct child of
|
|
660
|
+
`DrawerContent` is the scroll box there.
|
|
661
|
+
**Fix:** to scroll a form inside a box again, give the box (or the form) a
|
|
662
|
+
height and the scroll; the actions then stick to its bottom. To keep the
|
|
663
|
+
actions where they are, make them static:
|
|
664
|
+
|
|
665
|
+
```tsx
|
|
666
|
+
<form className="form-grid-layout max-h-[60vh] overflow-y-auto">
|
|
667
|
+
<div data-slot="form-actions" className="static">
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
An app's own fixed bottom bar or floating action button now sits under the
|
|
671
|
+
sticky actions (`z-index: 10`): give it a z-index above 10 (`className="z-20"`).
|
|
672
|
+
|
|
644
673
|
## Feedback
|
|
645
674
|
|
|
646
675
|
### A toast with an action never goes away
|
|
@@ -2396,6 +2425,26 @@ On an older version, give the field an array default and keep an array:
|
|
|
2396
2425
|
useForm({ defaultValues: { volume: [40] } }); // an array stays an array
|
|
2397
2426
|
```
|
|
2398
2427
|
|
|
2428
|
+
### The focus jumps away from a new row of a `RepeatableGroupFormField`
|
|
2429
|
+
|
|
2430
|
+
**When:** a row is added to a `RepeatableGroupFormField` by your own code (a
|
|
2431
|
+
second "Add" button, a "Duplicate" action) through `useFieldArray`'s `append`,
|
|
2432
|
+
`prepend` or `insert` on the same `name`, and the focus lands on another field
|
|
2433
|
+
than the one you expected, or the page scrolls to it.
|
|
2434
|
+
**Why:** react-hook-form's `append` (and `prepend`, `insert`) default to
|
|
2435
|
+
`shouldFocus: true` and focus the new row's first registered field. The group
|
|
2436
|
+
moves the focus itself only after its own buttons (the new row's first field,
|
|
2437
|
+
a removed row's neighbour, a move button), so a second `useFieldArray` that
|
|
2438
|
+
focuses on its own fights with it.
|
|
2439
|
+
**Fix:** let the group's Add button add rows (`newRow` is what it appends).
|
|
2440
|
+
When your own code must add one, pass `{ shouldFocus: false }` and move the
|
|
2441
|
+
focus yourself if you want it moved:
|
|
2442
|
+
|
|
2443
|
+
```tsx
|
|
2444
|
+
const { append } = useFieldArray({ control, name: 'periods' });
|
|
2445
|
+
append({ name: '' }, { shouldFocus: false });
|
|
2446
|
+
```
|
|
2447
|
+
|
|
2399
2448
|
### A multi-step form reopens at a later step
|
|
2400
2449
|
|
|
2401
2450
|
**When:** a wizard built on `createStepFormSlice` opens at the step (and with
|
|
@@ -3107,6 +3156,26 @@ with its own `<main>` passes `contentAs="div"`.
|
|
|
3107
3156
|
**Fix:** select `[data-test="typography"]` — or better, the text or the role
|
|
3108
3157
|
the user sees.
|
|
3109
3158
|
|
|
3159
|
+
### Long text in `Typography` no longer scrolls
|
|
3160
|
+
|
|
3161
|
+
**When:** after upgrading, a `Typography` with a fixed height or width used to
|
|
3162
|
+
scroll its overflowing text; it now overflows its box (or `truncate` and
|
|
3163
|
+
`line-clamp-*` are the only things that clip it).
|
|
3164
|
+
**Why:** `Typography` always carried `overflow-auto`, which made every block
|
|
3165
|
+
`Typography` a scroll region: Firefox counted a subpixel overflow, and axe
|
|
3166
|
+
reported `scrollable-region-focusable`. It sets no `overflow` now, so it is no
|
|
3167
|
+
longer a block formatting context (its margins can collapse with its
|
|
3168
|
+
neighbours'), and a text flex or grid item no longer has the zero minimum width a scroll container has.
|
|
3169
|
+
**Fix:** where you want scrolling, wrap the text in an element that owns it,
|
|
3170
|
+
focusable and named so keyboard users can scroll it; where you want a flex
|
|
3171
|
+
item to shrink, give it `min-w-0`:
|
|
3172
|
+
|
|
3173
|
+
```tsx
|
|
3174
|
+
<div className="max-h-40 overflow-auto" tabIndex={0} aria-label="Release notes">
|
|
3175
|
+
<Typography>{longText}</Typography>
|
|
3176
|
+
</div>
|
|
3177
|
+
```
|
|
3178
|
+
|
|
3110
3179
|
### A test no longer finds a tooltip on an entry of an expanded sidebar
|
|
3111
3180
|
|
|
3112
3181
|
**When:** after upgrading, a test hovers or focuses a `SidebarMenuItem` of an
|
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
.form-grid-layout {
|
|
98
98
|
@apply relative flex flex-col gap-2 @container;
|
|
99
99
|
[data-slot="form-fields"] {
|
|
100
|
-
@apply grid @2xl:grid-cols-2 @4xl:grid-cols-3 @6xl:grid-cols-4 gap-2 bg-background/80 p-4 place-items-start
|
|
100
|
+
@apply grid @2xl:grid-cols-2 @4xl:grid-cols-3 @6xl:grid-cols-4 gap-2 bg-background/80 p-4 place-items-start;
|
|
101
101
|
|
|
102
102
|
[data-slot="separator"] {
|
|
103
103
|
@apply col-start-full;
|
|
@@ -125,13 +125,73 @@
|
|
|
125
125
|
@apply col-start-full;
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
|
+
/* A focused control scrolls into view clear of the actions and a line
|
|
129
|
+
of helper text, in any scroll box (WCAG 2.4.11 Focus Not Obscured). */
|
|
130
|
+
[data-slot="form-fields"]
|
|
131
|
+
:is(
|
|
132
|
+
input,
|
|
133
|
+
textarea,
|
|
134
|
+
select,
|
|
135
|
+
button,
|
|
136
|
+
a[href],
|
|
137
|
+
summary,
|
|
138
|
+
[tabindex],
|
|
139
|
+
[contenteditable]
|
|
140
|
+
) {
|
|
141
|
+
scroll-margin-bottom: calc(
|
|
142
|
+
var(--form-actions-clearance, 6rem) +
|
|
143
|
+
var(--form-actions-fill, 0px)
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
/* Sticky at the bottom of whatever scrolls the form: in reach. */
|
|
128
147
|
[data-slot="form-actions"] {
|
|
129
|
-
@apply flex w-full justify-end gap-2
|
|
148
|
+
@apply sticky bottom-0 z-10 flex w-full justify-end gap-2 bg-background/90 p-4 backdrop-blur-sm max-sm:*:flex-1;
|
|
130
149
|
}
|
|
131
150
|
}
|
|
132
151
|
|
|
133
|
-
|
|
134
|
-
|
|
152
|
+
/* No height cap and no scroll of its own: the page (or the overlay) is
|
|
153
|
+
the one scroll. A Drawer's content does not scroll: the form does. */
|
|
154
|
+
[data-slot="drawer-content"] > .form-grid-layout {
|
|
155
|
+
@apply min-h-0 overflow-y-auto;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/* In a dialog, the stuck actions sit above its bottom padding
|
|
159
|
+
(`--form-actions-fill`: p-6 by default; set it for another one). */
|
|
160
|
+
:is([data-slot="dialog-content"], [data-slot="alert-dialog-content"]) {
|
|
161
|
+
--form-actions-fill: 1.5rem;
|
|
162
|
+
&:is(.p-0, .py-0, .pb-0) {
|
|
163
|
+
--form-actions-fill: 0px;
|
|
164
|
+
}
|
|
165
|
+
&:is(.p-4, .py-4, .pb-4) {
|
|
166
|
+
--form-actions-fill: 1rem;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/* ConfirmationDialog's form has no actions of its own: the dialog's
|
|
171
|
+
footer (two stacked buttons on a phone) sticks instead. */
|
|
172
|
+
:is([data-slot="dialog-content"], [data-slot="alert-dialog-content"]):has(
|
|
173
|
+
.form-grid-layout
|
|
174
|
+
):not(:has([data-slot="form-actions"])) {
|
|
175
|
+
--form-actions-clearance: 7.5rem;
|
|
176
|
+
> :is([data-slot="dialog-footer"], [data-slot="alert-dialog-footer"]) {
|
|
177
|
+
@apply sticky bottom-0 z-10 bg-background/90 backdrop-blur-sm;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/* A sticky element stops at the scroll box's padding: in a dialog, an
|
|
182
|
+
`::after` as tall as that padding fills the strip below the stuck
|
|
183
|
+
actions, so no field shows there, and adds no overflow. */
|
|
184
|
+
:is([data-slot="dialog-content"], [data-slot="alert-dialog-content"])
|
|
185
|
+
.form-grid-layout
|
|
186
|
+
> [data-slot="form-actions"]::after,
|
|
187
|
+
:is([data-slot="dialog-content"], [data-slot="alert-dialog-content"]):has(
|
|
188
|
+
.form-grid-layout
|
|
189
|
+
):not(:has([data-slot="form-actions"]))
|
|
190
|
+
> :is(
|
|
191
|
+
[data-slot="dialog-footer"],
|
|
192
|
+
[data-slot="alert-dialog-footer"]
|
|
193
|
+
)::after {
|
|
194
|
+
@apply absolute inset-x-0 top-full h-(--form-actions-fill) bg-background content-[""];
|
|
135
195
|
}
|
|
136
196
|
|
|
137
197
|
/* No height cap on `DataTable` here (it nested a scroll in the page's):
|
package/lib/components/base.css
CHANGED
|
@@ -55,7 +55,9 @@
|
|
|
55
55
|
font-size: var(--text-xs);
|
|
56
56
|
font-weight: var(--font-weight-medium);
|
|
57
57
|
}
|
|
58
|
-
a:
|
|
58
|
+
/* A sidebar entry is a link styled as a menu entry: the prose link style
|
|
59
|
+
(underline, info tint on hover and focus) must not reach it (#566). */
|
|
60
|
+
a:not(:has(> button)):not([data-slot="sidebar-menu-item"]) {
|
|
59
61
|
@apply focus:underline underline-offset-4 hover:underline cursor-pointer;
|
|
60
62
|
@apply text-info [&_svg]:data-[slot=icon]:text-info hover:bg-info/10 active:bg-info/5 focus:bg-info/15;
|
|
61
63
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nxgt/material",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.20.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"bugs": {
|
|
6
6
|
"email": "stevegires@gmail.com"
|
|
@@ -119,7 +119,7 @@
|
|
|
119
119
|
"docs:previews": "vitest run --project previews"
|
|
120
120
|
},
|
|
121
121
|
"dependencies": {
|
|
122
|
-
"@nxgt/material-core": "^0.
|
|
122
|
+
"@nxgt/material-core": "^0.11.0",
|
|
123
123
|
"@base-ui/react": "^1.6.0",
|
|
124
124
|
"@dnd-kit/core": "^6.3.1",
|
|
125
125
|
"@dnd-kit/sortable": "^10.0.0",
|