@nxgt/material 3.67.0 → 3.70.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 +38 -0
- package/README.md +6 -4
- package/dist/{alert-dialog-BgQDxcYh.js → alert-dialog-CBkWG32g.js} +1 -1
- package/dist/{async-slice-BCZyWgGr.js → async-slice-CNZ3N55P.js} +1 -1
- package/dist/{button-2de_lrix.js → button-D3qzqDY_.js} +1 -1
- package/dist/charts.js +3 -3
- package/dist/{chip-Cs0uEjpu.js → chip-CMTe4sQd.js} +2 -2
- package/dist/{code-editor-BYhBAfcc.js → code-editor-Cs7mz1J-.js} +5 -5
- package/dist/code-editor.js +5 -5
- package/dist/{code-language-items-xhmbF6wA.js → code-language-items-D_QMgjVy.js} +2 -2
- package/dist/{components-BRWaX4Xk.js → components-BbWcozcj.js} +18104 -17600
- package/dist/components.js +28 -28
- package/dist/{dialog-Co5oksU1.js → dialog-B3BSdRci.js} +4 -4
- package/dist/dnd.js +1 -1
- package/dist/{dropdown-menu-DdzyvN76.js → dropdown-menu-BaupOWgS.js} +1 -1
- package/dist/editor.js +661 -661
- package/dist/{empty-state-CQ0CV0kA.js → empty-state-B2Um5UpI.js} +2 -2
- package/dist/{field-blur-B2y-bNYi.js → field-blur-1XLyxRPr.js} +82 -82
- package/dist/{field-blur-context-BZtwcPUV.js → field-blur-context-XNvCDnMT.js} +1 -1
- package/dist/{hooks-DjuYsY-j.js → hooks-BU21Jpaa.js} +2 -2
- package/dist/hooks.js +2 -2
- package/dist/{hover-card-BeBVy6tB.js → hover-card-Bvm3svX9.js} +1 -1
- package/dist/i18n.js +3 -3
- package/dist/{icon-badge-DbYsbzwZ.js → icon-badge-wip9bfa2.js} +3 -3
- package/dist/{icon-button-Co6ttdv1.js → icon-button-QwIeBTdw.js} +1 -1
- package/dist/lib/components/ui/collections/filter/use-filter-field-config.d.ts +3 -3
- package/dist/lib/components/ui/fields/choice-list/choice-list-field.d.ts +13 -0
- package/dist/lib/components/ui/fields/choice-list/choice-list-form-field.d.ts +19 -0
- package/dist/lib/components/ui/fields/choice-list/choice-list-names.d.ts +19 -0
- package/dist/lib/components/ui/fields/choice-list/choice-list.utils.d.ts +27 -0
- package/dist/lib/components/ui/fields/choice-list/index.d.ts +3 -0
- package/dist/lib/components/ui/fields/choice-list/partials/choice-mark.d.ts +18 -0
- package/dist/lib/components/ui/fields/choice-list/partials/choice-row.d.ts +19 -0
- package/dist/lib/components/ui/fields/choice-list/types.d.ts +44 -0
- package/dist/lib/components/ui/fields/contacts/placeholders.d.ts +1 -1
- package/dist/lib/components/ui/fields/country-field/index.d.ts +2 -0
- package/dist/lib/components/ui/fields/country-field/iso-countries.d.ts +2 -0
- package/dist/lib/components/ui/fields/index.d.ts +2 -0
- package/dist/lib/components/ui/fields/phone-field/index.d.ts +1 -1
- package/dist/lib/components/ui/fields/phone-field/parse-phone.d.ts +2 -0
- package/dist/lib/components/ui/fields/phone-field/phone-field.d.ts +8 -1
- package/dist/lib/components/ui/fields/phone-field/phone-form-field.d.ts +8 -1
- package/dist/lib/components/ui/fields/phone-field/use-phone-value.d.ts +2 -1
- package/dist/lib/components/ui/fields/postal-address/utils.d.ts +15 -0
- package/dist/lib/components/ui/fields/quantity-field/index.d.ts +3 -0
- package/dist/lib/components/ui/fields/quantity-field/quantity-field.d.ts +8 -0
- package/dist/lib/components/ui/fields/quantity-field/quantity-form-field.d.ts +8 -0
- package/dist/lib/components/ui/fields/quantity-field/quantity-row.d.ts +13 -0
- package/dist/lib/components/ui/fields/quantity-field/quantity-unit.d.ts +12 -0
- package/dist/lib/components/ui/fields/quantity-field/quantity-value.d.ts +9 -0
- package/dist/lib/components/ui/fields/quantity-field/types.d.ts +45 -0
- package/dist/lib/components/ui/media/upload/index.d.ts +1 -0
- package/dist/lib/components/ui/media/upload/network-upload-field/network-upload-field.d.ts +6 -1
- package/dist/lib/components/ui/media/upload/network-upload-field/network-upload-field.errors.d.ts +8 -0
- package/dist/lib/components/ui/media/upload/network-upload-field/network-upload-field.utils.d.ts +2 -7
- package/dist/lib/components/ui/media/upload/network-upload-field/use-start-upload.d.ts +7 -3
- package/dist/lib/components/ui/media/upload/network-upload-field/use-upload-item-handlers.d.ts +2 -1
- package/dist/lib/components/ui/media/upload/s3-upload-field/s3-upload-field.d.ts +2 -5
- package/dist/lib/components/ui/media/upload/s3-upload-field/s3-upload-field.utils.d.ts +26 -1
- package/dist/lib/components/ui/media/upload/upload-error.d.ts +33 -0
- package/dist/lib/components/ui/selection/autocomplete/autocomplete-enter.d.ts +18 -0
- package/dist/lib/components/ui/selection/autocomplete/partials/autocomplete-chip-input.d.ts +2 -1
- package/dist/lib/components/ui/selection/autocomplete/partials/autocomplete-text-input.d.ts +2 -1
- package/dist/lib/components/ui/selection/autocomplete/types.d.ts +3 -1
- package/dist/lib/components/ui/widgets/education/questions/partials/question-content.d.ts +1 -1
- package/dist/lib/components/ui/widgets/education/questions/question-text.d.ts +1 -1
- package/dist/lib/components/ui/widgets/education/questions/question.d.ts +1 -1
- package/dist/lib/components/ui/widgets/education/questions/types.d.ts +10 -0
- package/dist/lib/i18n/resources/en/en.d.ts +20 -0
- package/dist/lib/i18n/resources/en/material.json.d.ts +21 -1
- package/dist/lib/i18n/resources/fr/fr.d.ts +20 -0
- package/dist/lib/i18n/resources/fr/material.json.d.ts +21 -1
- package/dist/lib/i18n/resources/resources.d.ts +40 -0
- package/dist/lib/i18n/translator.d.ts +1 -1
- package/dist/lib.js +1 -1
- package/dist/main.js +42 -42
- package/dist/map.js +8 -8
- package/dist/material.css +1 -1
- package/dist/pdf-reader.js +1 -1
- package/dist/print.js +1 -1
- package/dist/{public-AXwv1h3v.js → public-BuGgOtAl.js} +9 -9
- package/dist/{rich-code-block-qSvkA5rI.js → rich-code-block-DdHqzzek.js} +5 -5
- package/dist/{sortable-provider-DPk-RVEU.js → sortable-provider-DMyaSfZZ.js} +2 -2
- package/dist/{spinner-PdSk7zEs.js → spinner-ByYdcukU.js} +1 -1
- package/dist/theme-picker.js +2 -2
- package/dist/{theme-toggle-CZQckLDd.js → theme-toggle-DB3aZZnO.js} +1 -1
- package/dist/{toolbar-items-BmNYNaRB.js → toolbar-items-DbzIKjo7.js} +3 -3
- package/dist/{translate-material-BlCQ6AW9.js → translate-material-Dh7MYkHT.js} +1 -1
- package/dist/{translator-ChpeKCO3.js → translator-Byhohi1S.js} +42 -2
- package/dist/{typography-C4JSRr2B.js → typography-rUf0mE5a.js} +1 -1
- package/dist/{use-chat-composer-OUa9akfM.js → use-chat-composer-uUUTRB3Z.js} +2401 -2343
- package/dist/{use-heading-spy-K-HUp4o_.js → use-heading-spy-CVzW2NM3.js} +4 -4
- package/dist/{use-material-translation-DkE9J5NE.js → use-material-translation-BAYyZOcH.js} +1 -1
- package/dist/{use-return-focus-KypnR7vv.js → use-return-focus-CQAuk9kh.js} +1 -1
- package/docs/components/form-fields.md +196 -3
- package/docs/components/media-and-files.md +55 -9
- package/docs/components/primitives.md +9 -14
- package/docs/components/rich-widgets.md +2 -1
- package/docs/components/selection-and-choice.md +10 -3
- package/docs/guides/forms.md +4 -3
- package/docs/previews/form-fields-choicelistfield.webp +0 -0
- package/docs/previews/form-fields-quantityfield.webp +0 -0
- package/docs/roadmap.md +5 -2
- package/docs/troubleshooting.md +11 -4
- package/package.json +1 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { t as e } from "./utils-BLfntITJ.js";
|
|
2
|
-
import { a as t, c as n, d as r, i, n as a, r as o, t as s, u as c } from "./alert-dialog-
|
|
3
|
-
import { a as l } from "./button-
|
|
4
|
-
import { i as u } from "./translate-material-
|
|
5
|
-
import { t as d } from "./use-material-translation-
|
|
2
|
+
import { a as t, c as n, d as r, i, n as a, r as o, t as s, u as c } from "./alert-dialog-CBkWG32g.js";
|
|
3
|
+
import { a as l } from "./button-D3qzqDY_.js";
|
|
4
|
+
import { i as u } from "./translate-material-Dh7MYkHT.js";
|
|
5
|
+
import { t as d } from "./use-material-translation-BAYyZOcH.js";
|
|
6
6
|
import { _ as f, g as p, h as m, n as h, v as g, y as _ } from "./number.utils-BG_eEt5g.js";
|
|
7
7
|
import { useCallback as v, useEffect as y, useMemo as b, useRef as x, useState as S } from "react";
|
|
8
8
|
import { jsx as C, jsxs as w } from "react/jsx-runtime";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { o as e, r as t } from "./dist-DVRQkaTK.js";
|
|
2
2
|
import { a as n, c as r, i, l as a, o, r as s, s as c } from "./dist-lmHmz_bu.js";
|
|
3
|
-
import { C as l, S as u, w as d } from "./spinner-
|
|
3
|
+
import { C as l, S as u, w as d } from "./spinner-ByYdcukU.js";
|
|
4
4
|
import { c as f, i as p, l as m, o as h, r as g, s as ee } from "./in-dialog-Vwkyp-M5.js";
|
|
5
5
|
import { r as _ } from "./escape-owner-DpaP0lm1.js";
|
|
6
6
|
import * as v from "react";
|
|
@@ -12,12 +12,14 @@ for the value type each one writes. Everything is imported from
|
|
|
12
12
|
| <img src="../previews/form-fields-field.webp" width="96" alt=""> | [Field](#field) | Renders the field matching a config `type` |
|
|
13
13
|
| <img src="../previews/form-fields-formsection.webp" width="96" alt=""> | [FormSection](#formsection) | Titled section of a form, one column on a phone |
|
|
14
14
|
| <img src="../previews/form-fields-repeatablegroup.webp" width="96" alt=""> | [RepeatableGroup](#repeatablegroup) | Rows a user adds, removes and reorders |
|
|
15
|
+
| <img src="../previews/form-fields-choicelistfield.webp" width="96" alt=""> | [ChoiceListField](#choicelistfield) | An editable list of answer choices with correct marks |
|
|
15
16
|
| <img src="../previews/form-fields-textfield.webp" width="96" alt=""> | [TextField](#textfield) | Labelled input with adornments and helper text |
|
|
16
17
|
| <img src="../previews/form-fields-textareafield.webp" width="96" alt=""> | [TextareaField](#textareafield) | Labelled textarea with row sizing and a counter |
|
|
17
18
|
| <img src="../previews/form-fields-emailfield.webp" width="96" alt=""> | [EmailField](#emailfield) | Validates on blur, `mailto:` button |
|
|
18
19
|
| <img src="../previews/form-fields-passwordfield.webp" width="96" alt=""> | [PasswordField](#passwordfield) | Show / hide toggle and a strength meter |
|
|
19
20
|
| <img src="../previews/form-fields-numberfield.webp" width="96" alt=""> | [NumberField](#numberfield) | `number \| null` with clamping and +/− buttons |
|
|
20
21
|
| <img src="../previews/form-fields-percentfield.webp" width="96" alt=""> | [PercentField](#percentfield) | `NumberField` with a `%` suffix |
|
|
22
|
+
| <img src="../previews/form-fields-quantityfield.webp" width="96" alt=""> | [QuantityField](#quantityfield) | One labelled field holding an amount and its unit |
|
|
21
23
|
| <img src="../previews/form-fields-currencyfield.webp" width="96" alt=""> | [CurrencyField](#currencyfield) | Locale-formatted amount with a currency suffix |
|
|
22
24
|
| <img src="../previews/form-fields-phonefield.webp" width="96" alt=""> | [PhoneField](#phonefield) | International number, emits E.164 |
|
|
23
25
|
| <img src="../previews/form-fields-urlfield.webp" width="96" alt=""> | [UrlField](#urlfield) | Validates on blur, open and copy buttons |
|
|
@@ -325,6 +327,104 @@ masculine one.
|
|
|
325
327
|
|
|
326
328
|
---
|
|
327
329
|
|
|
330
|
+
## ChoiceListField
|
|
331
|
+
|
|
332
|
+

|
|
333
|
+
|
|
334
|
+
An editable list of answer choices, for a quiz question: each row has a text
|
|
335
|
+
and a "correct" mark. `ChoiceListFormField` wires it to react-hook-form; the
|
|
336
|
+
value is an array of `{ id, text, correct }`.
|
|
337
|
+
|
|
338
|
+
```tsx
|
|
339
|
+
import { useForm } from 'react-hook-form';
|
|
340
|
+
import { Button, ChoiceListFormField } from '@nxgt/material/components';
|
|
341
|
+
import type { ChoiceRow } from '@nxgt/material/components';
|
|
342
|
+
|
|
343
|
+
type Values = { choices: ChoiceRow[] };
|
|
344
|
+
|
|
345
|
+
declare function save(choices: ChoiceRow[]): void;
|
|
346
|
+
|
|
347
|
+
function QuestionForm() {
|
|
348
|
+
const { control, handleSubmit } = useForm<Values>({
|
|
349
|
+
defaultValues: {
|
|
350
|
+
choices: [
|
|
351
|
+
{ id: 'a', text: 'Paris', correct: true },
|
|
352
|
+
{ id: 'b', text: 'Lyon', correct: false },
|
|
353
|
+
],
|
|
354
|
+
},
|
|
355
|
+
});
|
|
356
|
+
return (
|
|
357
|
+
<form onSubmit={handleSubmit((values) => save(values.choices))}>
|
|
358
|
+
<ChoiceListFormField
|
|
359
|
+
control={control}
|
|
360
|
+
name="choices"
|
|
361
|
+
label="Answers"
|
|
362
|
+
selection="multiple"
|
|
363
|
+
min={2}
|
|
364
|
+
max={20}
|
|
365
|
+
/>
|
|
366
|
+
<Button type="submit">Save</Button>
|
|
367
|
+
</form>
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
It is a [`RepeatableGroup`](#repeatablegroup) of rows: Add, Remove and move
|
|
373
|
+
up / down buttons, `min` and `max` rows (Remove is disabled at `min`, Add is
|
|
374
|
+
`aria-disabled` at `max`), focus that follows each change and polite
|
|
375
|
+
announcements. Without a form library, `ChoiceListField` takes `value` and
|
|
376
|
+
`onChange`.
|
|
377
|
+
|
|
378
|
+
| Prop | Type | Default | What it does |
|
|
379
|
+
| --- | --- | --- | --- |
|
|
380
|
+
| `control`, `name` | `Control<T>`, `Path<T>` | — | the form and its array (`ChoiceListFormField`) |
|
|
381
|
+
| `value`, `onChange` | `ChoiceRow[]`, `(rows) => void` | — | the rows (`ChoiceListField`) |
|
|
382
|
+
| `selection` | `'single' \| 'multiple'` | `'multiple'` | `single`: native radios sharing one name in a `radiogroup`; marking a row unmarks the others. `multiple`: checkboxes |
|
|
383
|
+
| `requireCorrect` | `boolean` | `true` | the built-in rules ask for a marked row; `false` lets a list with none pass, for a poll (`ChoiceListFormField`) |
|
|
384
|
+
| `min`, `max` | `number` | none | bounds on the row count |
|
|
385
|
+
| `reorderable` | `boolean` | `true` | move up / down buttons on each row |
|
|
386
|
+
| `label` | `ReactNode` | none | names the group (a `group`, or a `radiogroup` in `single`) |
|
|
387
|
+
| `rowLabel`, `addLabel` | `string` | "answer", "Add answer" | lower-case noun for the buttons (translated by default) |
|
|
388
|
+
| `error`, `rowErrors` | `ReactNode`, `{ [id]: ReactNode }` | none | the list's error and each row's, by id (`ChoiceListField`; the form field fills them from the form) |
|
|
389
|
+
| `onBlur` | `() => void` | none | `ChoiceListField` alone: called when the focus leaves the whole list. In a form the list's root is registered with the form's `FieldBlur`, so touched works without it |
|
|
390
|
+
| `onAssignIds` | `(rows) => void` | `onChange` | `ChoiceListField`: called once on mount with ids given to rows that had none or repeated one; the form field sets them without marking the form dirty |
|
|
391
|
+
| other props | `div` props | | passed to the group's root (`className`, `id`, `data-*`, `ref`) |
|
|
392
|
+
| `emptyTitle`, `emptyDescription`, `disabled` | | | as `RepeatableGroup` |
|
|
393
|
+
|
|
394
|
+
- **Ids:** every row keeps its `id` across edits, removals and moves, so a
|
|
395
|
+
server can match a choice to its stored one. A new row gets a generated id;
|
|
396
|
+
a row that arrives without one, or repeating an earlier row's, is given a new
|
|
397
|
+
one on mount (the form field does it with `setValue` when the `FormProvider` above is its own form, and then leaves the form clean; otherwise it calls `onChange`). Give the rows ids in `defaultValues`, the clean path: with assigned ids `defaultValues` still lack them, so `dirtyFields.choices` marks rows after the first real edit.
|
|
398
|
+
In `single`, the new row is marked when no row is. A list that arrives with
|
|
399
|
+
several rows marked is not changed: it fails the `too-many` rule.
|
|
400
|
+
- **Semantics:** each row's text is a labelled input ("Answer 2"); its mark
|
|
401
|
+
is a radio or a checkbox named "Correct: answer 2" (the visible word
|
|
402
|
+
"Correct" starts the name), so `getByRole('checkbox', { name: 'Correct: answer 2' })`
|
|
403
|
+
finds it. Buttons are named after the row ("Remove answer 2", "Move answer 2 up"):
|
|
404
|
+
the keyboard reorders with them.
|
|
405
|
+
- **Tab order:** in `single` the radios are native and share one name, so
|
|
406
|
+
Tab visits each row's text field in order; the checked radio is reached in
|
|
407
|
+
its own row and the unchecked ones are skipped. The arrow keys move between
|
|
408
|
+
the radios and check the one they land on. The radios point their `form`
|
|
409
|
+
attribute at no form, so a surrounding `<form>`'s native `FormData` gets no
|
|
410
|
+
entry for them. (A
|
|
411
|
+
roving radio group over the whole list would send Tab to the checked radio
|
|
412
|
+
first.)
|
|
413
|
+
- **Errors:** without a resolver the field validates by itself: fewer than
|
|
414
|
+
`min` rows, several rows marked in `single`, no row marked (unless
|
|
415
|
+
`requireCorrect` is `false`), or a blank row puts a message under the
|
|
416
|
+
list, and blank rows are marked invalid (only for that rule, never for a resolver's or another rule's message). With a resolver (zod), an issue on
|
|
417
|
+
`choices.2.text` shows under row 3 and an issue on `choices` under the list.
|
|
418
|
+
When both fail in one pass, react-hook-form keeps only the list's.
|
|
419
|
+
- **Translations:** `material.choice-list.*` (English and French). The
|
|
420
|
+
messages name the row with `rowLabel` ("Fill in every answer"); `too-few`
|
|
421
|
+
is a plural that adds `s` to it, and the French templates carry the
|
|
422
|
+
feminine article, so a French app overrides the keys for a masculine noun.
|
|
423
|
+
|
|
424
|
+
**Storybook:** `Form fields/ChoiceListField` · **Vue:** none yet
|
|
425
|
+
|
|
426
|
+
---
|
|
427
|
+
|
|
328
428
|
## TextField
|
|
329
429
|
|
|
330
430
|

|
|
@@ -511,6 +611,79 @@ ratio (`0.15`).
|
|
|
511
611
|
|
|
512
612
|
---
|
|
513
613
|
|
|
614
|
+
## QuantityField
|
|
615
|
+
|
|
616
|
+

|
|
617
|
+
|
|
618
|
+
One labelled field holding an amount and its unit ("2 capsule", "500 mg"): a
|
|
619
|
+
[`NumberField`](#numberfield)'s number beside a unit picker, under one label
|
|
620
|
+
and one error message. The value is `{ value: number | null, unit: string }`.
|
|
621
|
+
`QuantityFormField` wires it to react-hook-form.
|
|
622
|
+
|
|
623
|
+
```tsx
|
|
624
|
+
import { useForm } from 'react-hook-form';
|
|
625
|
+
import { Button, QuantityFormField } from '@nxgt/material/components';
|
|
626
|
+
import type { QuantityValue } from '@nxgt/material/components';
|
|
627
|
+
|
|
628
|
+
type Values = { dose: QuantityValue };
|
|
629
|
+
|
|
630
|
+
function DoseForm() {
|
|
631
|
+
const { control, handleSubmit } = useForm<Values>({
|
|
632
|
+
defaultValues: { dose: { value: null, unit: '' } },
|
|
633
|
+
});
|
|
634
|
+
return (
|
|
635
|
+
<form onSubmit={handleSubmit((values) => console.log(values.dose))}>
|
|
636
|
+
<QuantityFormField
|
|
637
|
+
control={control}
|
|
638
|
+
name="dose"
|
|
639
|
+
label="Dose"
|
|
640
|
+
min={0}
|
|
641
|
+
step={0.5}
|
|
642
|
+
units={[
|
|
643
|
+
{ value: 'mg', label: 'mg' },
|
|
644
|
+
{ value: 'ml', label: 'mL' },
|
|
645
|
+
]}
|
|
646
|
+
/>
|
|
647
|
+
<Button type="submit">Save</Button>
|
|
648
|
+
</form>
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
Without a form library, `QuantityField` takes `value` and `onChange` (or
|
|
654
|
+
`defaultValue`). The number is the one `NumberField` has: the language's
|
|
655
|
+
decimal separator, `min`, `max` and `step` (clamped on blur), and steppers
|
|
656
|
+
named after the field ("Increase Dose"). Both parts stay on one line on a
|
|
657
|
+
phone: the number shrinks, the unit keeps a fixed 7rem.
|
|
658
|
+
|
|
659
|
+
| Prop | Type | Default | What it does |
|
|
660
|
+
| --- | --- | --- | --- |
|
|
661
|
+
| `control`, `name` | `Control<T>`, `Path<T>` | — | the form and its `{ value, unit }` (`QuantityFormField`) |
|
|
662
|
+
| `value`, `defaultValue`, `onChange` | `QuantityValue`, `(value) => void` | empty | the amount and its unit; `onChange` gets both on each change |
|
|
663
|
+
| `label` | `ReactNode` | none | the one label; it names the group |
|
|
664
|
+
| `units` | `{ value, label }[]` | none | the units offered |
|
|
665
|
+
| `allowCustomUnit` | `boolean` | `false` | the unit is typed in a text input and `units` are suggestions; otherwise it is picked from `units` |
|
|
666
|
+
| `min`, `max`, `step`, `hideSteppers` | | `step` 1 | as `NumberField` |
|
|
667
|
+
| `unitLabel`, `unitPlaceholder` | `string` | "<label> unit" | the unit part's accessible name and its placeholder |
|
|
668
|
+
| `helperText`, `error` | `ReactNode`, `boolean` | none | the one message under the pair; in a form, an error on `name`, `name.value` or `name.unit` shows there |
|
|
669
|
+
| `className`, `aria-label` | `string` | none | `className` goes on the group; `aria-label` names it when `label` is not plain text |
|
|
670
|
+
| `name`, `onBlur` | | none | `QuantityField` only (the form field takes them from the form): `name` names the parts `<name>.value` and `<name>.unit`; `onBlur` fires when the number input loses focus |
|
|
671
|
+
| `required`, `disabled`, `placeholder`, `id`, `ref` | | | `placeholder`, `id` and `ref` reach the number input (React Hook Form's `field.ref` focuses it on an error) |
|
|
672
|
+
|
|
673
|
+
- **Semantics:** a `<fieldset>` (role `group`) named by the label; the number
|
|
674
|
+
is a textbox named after the label, the unit a combobox (or, with
|
|
675
|
+
`allowCustomUnit`, a text input) named "Dose unit". The message describes
|
|
676
|
+
the group, the number and the unit, and both are `aria-invalid` on an error.
|
|
677
|
+
- **Translations:** `material.quantity-field.unit` and `.unit-named` (English
|
|
678
|
+
and French) name the unit part.
|
|
679
|
+
|
|
680
|
+
**Emits:** `{ value: number | null, unit: string }`.
|
|
681
|
+
**Form wrapper:** `QuantityFormField` — `control` and `name`, see
|
|
682
|
+
[forms](../guides/forms.md#the-pair) and [what each field emits](../guides/forms.md#what-each-field-emits).
|
|
683
|
+
**Storybook:** `Form fields/QuantityField` · **Vue:** none yet (#2106)
|
|
684
|
+
|
|
685
|
+
---
|
|
686
|
+
|
|
514
687
|
## CurrencyField
|
|
515
688
|
|
|
516
689
|

|
|
@@ -566,9 +739,19 @@ export function Mobile() {
|
|
|
566
739
|
| `value` / `defaultValue` | `string \| null` | `null` | an E.164 number (`+33612345678`) |
|
|
567
740
|
| `onChange` | `(value: string \| null) => void` | — | `null` until the typed number is possible for the country |
|
|
568
741
|
| `defaultCountry` | `string` (ISO 3166-1) | `'FR'` | the country used when the value has no `+` prefix |
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
**
|
|
742
|
+
| `onValidityChange` | `(incomplete: boolean) => void` | — | `true` while the draft is not empty but not yet a possible number (`onChange` gives `null` for it, as for an empty field); `false` once it is complete or empty. Fires before the matching `onChange`; not called while the field has never been incomplete |
|
|
743
|
+
|
|
744
|
+
**Emits:** E.164 `string`, or `null` while the number is empty or incomplete.
|
|
745
|
+
Outside a form, tell the two apart with `onValidityChange` (or
|
|
746
|
+
`isIncompletePhone(draft, iso)`).
|
|
747
|
+
**Form wrapper:** `PhoneFormField` — `control` and `name`; an empty field is
|
|
748
|
+
`null`, and a half-typed number is a field error « Incomplete number »
|
|
749
|
+
(`material.phone-field.incomplete`, « Numéro incomplet » in French) that blocks
|
|
750
|
+
the submit, so a stored number is never cleared silently. `PhoneFormField` takes `rules`: your own `rules.validate` (a function or an
|
|
751
|
+
object of them) runs beside the incomplete check, as its `custom` key for a
|
|
752
|
+
function. A `rules.required` is checked first by React Hook Form, so a half-typed
|
|
753
|
+
number reads as the required message there. A form `reset()` to `null` does not clear a half-typed draft (the
|
|
754
|
+
value did not change), so an incomplete draft stays an error until it is edited. See
|
|
572
755
|
[forms](../guides/forms.md#the-pair) and [what each field emits](../guides/forms.md#what-each-field-emits).
|
|
573
756
|
**Storybook:** `Form fields/PhoneField` (and `/Field look`) · **Vue:** `PhoneField`, `PhoneFormField`
|
|
574
757
|
|
|
@@ -1771,6 +1954,16 @@ Parts: `PostalAddress` (the card), `PostalAddressDialog` (the edit form).
|
|
|
1771
1954
|
| `onEdit` | `() => void` | — | shows the edit button |
|
|
1772
1955
|
| `disabled` | `boolean` | — | hides it |
|
|
1773
1956
|
|
|
1957
|
+
**One line and country codes:** `formatPostalAddress(address, { language?, separator? })`
|
|
1958
|
+
returns the address as one line for a read view, `"12 rue du Lac, 74000 Annecy, France"`:
|
|
1959
|
+
street, city line, then the country named in `language` (default `'en'`, as
|
|
1960
|
+
`postalAddressCountryName`). The city line puts the postal code before the city
|
|
1961
|
+
(FR, BE, CH, DE and any other country) or after the city and region (GB, US, CA,
|
|
1962
|
+
IE, AU, NZ, IN: `"London SW1A 2AA"`). Empty parts are skipped; `separator`
|
|
1963
|
+
defaults to `', '`. `ISO_COUNTRY_CODES` (the 249 ISO 3166-1 alpha-2 codes) and
|
|
1964
|
+
`isCountryCode(code)` (a guard to `string`, upper case only) are exported from
|
|
1965
|
+
`@nxgt/material/components` too.
|
|
1966
|
+
|
|
1774
1967
|
`PostalAddressFormField` and `PostalAddressDialog` take `cancelVariant` (the
|
|
1775
1968
|
dialog's Cancel button, a `Button` `variant`, default `'tonal'`; `'outlined'`
|
|
1776
1969
|
restores the earlier look).
|
|
@@ -65,9 +65,10 @@ function Attachments() {
|
|
|
65
65
|
<NetworkUploadField
|
|
66
66
|
mode="single"
|
|
67
67
|
label="Contract"
|
|
68
|
-
onUpload={async (file, onProgress) => {
|
|
68
|
+
onUpload={async (file, onProgress, signal) => {
|
|
69
|
+
const res = await fetch('/api/files', { method: 'POST', body: file, signal });
|
|
69
70
|
onProgress?.(100);
|
|
70
|
-
return
|
|
71
|
+
return (await res.json()).url;
|
|
71
72
|
}}
|
|
72
73
|
/>;
|
|
73
74
|
```
|
|
@@ -75,8 +76,8 @@ function Attachments() {
|
|
|
75
76
|
| Prop | Type | Default | Effect |
|
|
76
77
|
| --- | --- | --- | --- |
|
|
77
78
|
| `mode` | `'single' \| 'multiple'` | — (required) | `value` is a `string` or a `string[]` |
|
|
78
|
-
| `onUpload` | `(file, onProgress?) => Promise<string>` | — | `NetworkUploadField` only; the resolved string becomes the value |
|
|
79
|
-
| `getPresignedUrl` | `(file) => Promise<{ uploadUrl, publicUrl, headers? }
|
|
79
|
+
| `onUpload` | `(file, onProgress?, signal?) => Promise<string>` | — | `NetworkUploadField` only; the resolved string becomes the value. Removing an uploading row or unmounting the field aborts `signal`: pass it to your request, or the upload still completes (its value is dropped). An abort after the request completed cannot unsend it: the value is dropped and `onRemove` is not called. The S3 fields abort their request themselves and reject with an `UploadError` of kind `aborted` (named `AbortError`, no longer a `DOMException`) |
|
|
80
|
+
| `getPresignedUrl` | `(file) => Promise<PresignedUploadTarget>` | — | `S3UploadField` only; `{ uploadUrl, publicUrl, headers? }` PUTs the file and stores `publicUrl`; `{ method: 'POST', url, fields, value }` posts a form and stores `value` (see [presigned POST](#presigned-post)) |
|
|
80
81
|
| `accept` | `string` | — | file input `accept`, also shown under the drop zone |
|
|
81
82
|
| `maxSize` | `number` (bytes) | — | larger files are rejected before upload |
|
|
82
83
|
| `maxFiles` | `number` | — | `mode="multiple"` only; the drop zone is disabled once reached |
|
|
@@ -95,9 +96,51 @@ description instead, so the error is still said: with `error` and that
|
|
|
95
96
|
group, so to set files in a test, reach the input through it:
|
|
96
97
|
`getByRole('group', { name: 'Contract' }).locator('input[type=file]')`.
|
|
97
98
|
|
|
99
|
+
### Presigned POST
|
|
100
|
+
|
|
101
|
+
A backend that signs a POST policy (so the bucket enforces content type and
|
|
102
|
+
size) returns `{ method: 'POST', url, fields, value }`. The field sends
|
|
103
|
+
`multipart/form-data` with `fields` in their order and the file appended last
|
|
104
|
+
under the name `file`, then stores `value` (the key). The same target works for `S3UploadFormField`,
|
|
105
|
+
`ImageS3Field`, `ImageS3FormField`, `FileListS3Field` and `FileListS3FormField`;
|
|
106
|
+
the PUT shape stays valid. Progress, retry, cancel and errors behave as for a PUT.
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
<S3UploadFormField
|
|
110
|
+
control={control}
|
|
111
|
+
name="file"
|
|
112
|
+
label="Contract"
|
|
113
|
+
getPresignedUrl={async (file) => {
|
|
114
|
+
const { url, fields, key } = await api.presignPost({ name: file.name, contentType: file.type });
|
|
115
|
+
return { method: 'POST', url, fields, value: key }; // fields exactly as signed
|
|
116
|
+
}}
|
|
117
|
+
/>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
A refused policy (`403`, or `400 EntityTooLarge`) reaches `onError(file, error)`
|
|
121
|
+
and the field's error state; when S3 answers with its XML error, the message
|
|
122
|
+
carries the code. The S3 helpers reject with an `UploadError` (`kind`:
|
|
123
|
+
`status` | `network` | `aborted`, plus `status` and `code`), and the field words
|
|
124
|
+
it in the active language from `material.upload.error-status`,
|
|
125
|
+
`error-status-code`, `error-network` and `error-aborted` (`Échec de l'envoi
|
|
126
|
+
(statut 400, EntityTooLarge)` in French). `error.message` stays English, for
|
|
127
|
+
logs. An error thrown by your own `onUpload` or `getPresignedUrl` is not an
|
|
128
|
+
`UploadError`: its `message` is shown as is, so translate it yourself. A stored
|
|
129
|
+
`value` that is a key needs `resolveUrl` to preview it.
|
|
130
|
+
|
|
131
|
+
The bucket must allow the browser to POST from your origin:
|
|
132
|
+
|
|
133
|
+
```json
|
|
134
|
+
[{ "AllowedOrigins": ["https://app.example.com"], "AllowedMethods": ["POST"], "AllowedHeaders": ["*"] }]
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
If the app sends a `Content-Security-Policy`, add the bucket host to
|
|
138
|
+
`connect-src` (uploads use `XMLHttpRequest`), for example
|
|
139
|
+
`connect-src 'self' https://my-bucket.s3.eu-west-1.amazonaws.com`.
|
|
140
|
+
|
|
98
141
|
**Form wrapper:** `S3UploadFormField`, `NetworkUploadFormField` — `control` and `name`, see
|
|
99
142
|
[forms](../guides/forms.md).
|
|
100
|
-
**Storybook:** `Media & files/S3UploadField`, `Media & files/NetworkUploadField` · **Vue:** `S3UploadField`, `NetworkUploadField`, `S3UploadFormField`, `NetworkUploadFormField`
|
|
143
|
+
**Storybook:** `Media & files/S3UploadField`, `Media & files/NetworkUploadField` · **Vue:** `S3UploadField`, `NetworkUploadField`, `S3UploadFormField`, `NetworkUploadFormField` (PUT only, #2095)
|
|
101
144
|
|
|
102
145
|
---
|
|
103
146
|
|
|
@@ -199,7 +242,9 @@ function Attachments() {
|
|
|
199
242
|
```
|
|
200
243
|
|
|
201
244
|
Parts: `FileList` (display only), `FileListField` (uploads through `onUpload`),
|
|
202
|
-
`FileListS3Field` (uploads through `getPresignedUrl`, like `S3UploadField
|
|
245
|
+
`FileListS3Field` (uploads through `getPresignedUrl`, like `S3UploadField`;
|
|
246
|
+
with a POST target the value is `value`, a key, so pass `resolveUrl` to preview
|
|
247
|
+
saved images).
|
|
203
248
|
|
|
204
249
|
| Prop | Type | Default | Effect |
|
|
205
250
|
| --- | --- | --- | --- |
|
|
@@ -224,7 +269,7 @@ dropzone as well read the error twice, and the group still says it once
|
|
|
224
269
|
`maxFiles` hides the dropzone. The dropzone is outlined through `data-invalid`.
|
|
225
270
|
|
|
226
271
|
**Form wrapper:** `FileListFormField`, `FileListS3FormField` — see [forms](../guides/forms.md).
|
|
227
|
-
**Storybook:** `Media & files/FileList`, `Media & files/FileListField` · **Vue:** `FileList`, `FileListField`, `FileListS3Field`, `FileListFormField`, `FileListS3FormField`
|
|
272
|
+
**Storybook:** `Media & files/FileList`, `Media & files/FileListField` · **Vue:** `FileList`, `FileListField`, `FileListS3Field`, `FileListFormField`, `FileListS3FormField` (S3 variants PUT only, #2095)
|
|
228
273
|
|
|
229
274
|
---
|
|
230
275
|
|
|
@@ -364,7 +409,8 @@ function Avatar() {
|
|
|
364
409
|
```
|
|
365
410
|
|
|
366
411
|
Parts: `ImageField` (uploads through `onUpload`), `ImageS3Field` (through
|
|
367
|
-
`getPresignedUrl`
|
|
412
|
+
`getPresignedUrl`; with a POST target the value is `value`, a key, so pass
|
|
413
|
+
`resolveUrl` to preview saved images).
|
|
368
414
|
|
|
369
415
|
| Prop | Type | Default | Effect |
|
|
370
416
|
| --- | --- | --- | --- |
|
|
@@ -389,7 +435,7 @@ with no message of its own, by a visually hidden "This value is not valid."
|
|
|
389
435
|
(`material.zod.default`, translated), and outlined through `data-invalid`.
|
|
390
436
|
|
|
391
437
|
**Form wrapper:** `ImageFormField`, `ImageS3FormField` — see [forms](../guides/forms.md).
|
|
392
|
-
**Storybook:** `Media & files/ImageField` · **Vue:** `ImageField`, `ImageS3Field`, `ImageFormField`, `ImageS3FormField`
|
|
438
|
+
**Storybook:** `Media & files/ImageField` · **Vue:** `ImageField`, `ImageS3Field`, `ImageFormField`, `ImageS3FormField` (S3 variants PUT only, #2095)
|
|
393
439
|
|
|
394
440
|
---
|
|
395
441
|
|
|
@@ -788,7 +788,7 @@ hides the indicator.
|
|
|
788
788
|
|
|
789
789
|
For tabs that go to routes, use [TabsNav](#tabsnav): real links with `aria-current`, not tabs with panels.
|
|
790
790
|
|
|
791
|
-
`TabsTrigger asChild` makes your link the tab (`<TabsTrigger value="a" asChild><Link to="/a">A</Link></TabsTrigger>`): the indicator and the icon and label spans go inside it. `ToggleGroupItem`, `AccordionTrigger`, `SelectTrigger` and `ExtendedLabel` take `asChild` the same way.
|
|
791
|
+
`TabsTrigger asChild` makes your link the tab (`<TabsTrigger value="a" asChild><Link to="/a">A</Link></TabsTrigger>`): the indicator and the icon and label spans go inside it. `ToggleGroupItem`, `AccordionTrigger`, `SelectTrigger` and `ExtendedLabel` take `asChild` the same way. For links, use [SegmentedNav](#segmentednav), not `ToggleGroupItem asChild`: a radio group of links is announced wrongly.
|
|
792
792
|
|
|
793
793
|
`dir="rtl"` on `Tabs` (or Radix's `DirectionProvider`) runs the tabs from the
|
|
794
794
|
right: the indicator follows them, and the Left arrow key moves to the next
|
|
@@ -849,21 +849,19 @@ Use `Tabs` when the panels are on the page and `TabsNav` when each tab is a
|
|
|
849
849
|
place to go: every tab is a real link, so it opens in a new window and its
|
|
850
850
|
address can be copied. `aria-label` names the navigation and is required.
|
|
851
851
|
|
|
852
|
-
- **Current link**:
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
`data-state="active"`, which is what the indicator follows.
|
|
852
|
+
- **Current link**: a link's `current` boolean overrides the nav's `value`. The
|
|
853
|
+
current link carries `aria-current="page"` and `data-state="active"`, which
|
|
854
|
+
is what the indicator follows.
|
|
856
855
|
- **Keyboard**: the links are plain links, so Tab moves from one to the next;
|
|
857
856
|
the arrow keys do nothing (they are not tabs, there is no `tablist`).
|
|
858
857
|
- **Looks**: `variant="underline"` (the default) or `"segmented"`, as `Tabs`.
|
|
859
858
|
A nav wider than its container scrolls inside itself, with the same edge
|
|
860
859
|
fades and scroll buttons as `Tabs`. On a coarse pointer each link is 44px
|
|
861
|
-
tall at least.
|
|
860
|
+
tall at least. In forced colours the current link keeps a `Highlight` mark.
|
|
862
861
|
- **The link**: `TabsNavLink` takes `LinkButton`'s props (`to`, `replace`,
|
|
863
862
|
`state`, `target`, `reloadDocument`, `download`, `linkComponent`) and an
|
|
864
863
|
`icon`. It renders React Router's `Link`, or the `LinkComponentProvider`'s
|
|
865
|
-
component (given `href`).
|
|
866
|
-
tab keeps the scroll position; pass `preventScrollReset={false}` to reset.
|
|
864
|
+
component (given `href`).
|
|
867
865
|
- A tab bound to the URL's search params: [`useTabParam`](#route-tabs-in-the-url-usetabparam).
|
|
868
866
|
|
|
869
867
|
**It needs a Router.** Without a `LinkComponentProvider`, `TabsNavLink` renders
|
|
@@ -993,9 +991,7 @@ each segment is a place to go: every item is a real link, so it opens in a
|
|
|
993
991
|
new window and its address can be copied. A `ToggleGroup` of links would
|
|
994
992
|
announce each as a radio. `aria-label` names the navigation and is required.
|
|
995
993
|
|
|
996
|
-
- **Current item**: `
|
|
997
|
-
`value`; an item's `current` boolean overrides it (`current={false}` to
|
|
998
|
-
switch one off). The current item carries `aria-current="page"` and
|
|
994
|
+
- **Current item**: an item's `current` boolean overrides the nav's `value`. The current item carries `aria-current="page"` and
|
|
999
995
|
`data-state="active"`, and draws the selected pill (its label in
|
|
1000
996
|
`primary-text`); it does not glide, since the nav may scroll.
|
|
1001
997
|
- **Keyboard**: the items are plain links, so Tab moves from one to the next;
|
|
@@ -1007,8 +1003,7 @@ announce each as a radio. `aria-label` names the navigation and is required.
|
|
|
1007
1003
|
- **The link**: `SegmentedNavItem` takes `LinkButton`'s props (`to`, `replace`,
|
|
1008
1004
|
`state`, `target`, `reloadDocument`, `download`, `linkComponent`). It renders
|
|
1009
1005
|
React Router's `Link`, or the `LinkComponentProvider`'s component (given
|
|
1010
|
-
`href`).
|
|
1011
|
-
the scroll position; pass `preventScrollReset={false}` to reset.
|
|
1006
|
+
`href`).
|
|
1012
1007
|
- A segment bound to the URL's search params: [`useTabParam`](#route-tabs-in-the-url-usetabparam) (`useTabParam({ param: 'p', values })`), as above.
|
|
1013
1008
|
|
|
1014
1009
|
**It needs a Router.** Without a `LinkComponentProvider`, `SegmentedNavItem`
|
|
@@ -1079,7 +1074,7 @@ colour.
|
|
|
1079
1074
|
Everything is CSS on data attributes (`data-variant`, `data-size`,
|
|
1080
1075
|
`data-orientation`): nothing is measured, no inline style, the server
|
|
1081
1076
|
writes the same markup. Motion is off under `prefers-reduced-motion`, and
|
|
1082
|
-
forced colours draw the selected item in `SelectedItem`.
|
|
1077
|
+
forced colours draw the selected item in `SelectedItem`. The segmented look also follows `aria-current="page"`, which is what [SegmentedNav](#segmentednav) uses for links.
|
|
1083
1078
|
|
|
1084
1079
|
```tsx
|
|
1085
1080
|
import {
|
|
@@ -2457,9 +2457,10 @@ French bundle — see [i18n](../guides/i18n.md).
|
|
|
2457
2457
|
| `QuestionCard` `renderCode` | `(field: QuestionCodeField) => ReactNode` | — | the code field of a `code` question; `field` is `{ value, onChange, label, labelledBy, describedBy, language, maxLength, readOnly, disabled }`. Keeps Monaco out of the card's bundle |
|
|
2458
2458
|
| `Question` `description` | `string` | — | `open`, `code`: a persistent helper line under the answer field ("A person will mark this answer"), linked to it by `aria-describedby` and passed to `renderCode` as `field.describedBy`; always shown, unlike `hint` (a reveal button). Its id is `${promptId}-description` |
|
|
2459
2459
|
| `QuestionCard` `promptId` | `string` | generated | the id of the prompt text: point a wrapping section's `aria-labelledby` at it (and `aria-describedby` at `${promptId}-description`) |
|
|
2460
|
+
| `QuestionCard` `preview` | `boolean` | `false` | an author's preview, `<QuestionCard question={q} preview />`: forces `revealed` and `readOnly`, drops `onValueChange`, and shows the marks (the `question.correct` options marked, the expected text or number filled in) and the `explanation`, with no verdict banner. `verdict` and `showFeedback` are always ignored in a preview; `value` is ignored when the question has a `correct` — an `open` or `code` question has none, so pass its reference answer as `value`. Not for a learner's attempt |
|
|
2460
2461
|
| `QuestionCard` `readOnly` / `busy` | `boolean` | `false` | `readOnly`: an answer already submitted, shown without the revealed marks; `busy`: read-only and `aria-busy` while it is saved; every kind honours both |
|
|
2461
2462
|
| new strings | `material.education.kind-open`, `kind-code`, `code-placeholder` | — | English fallbacks and a French bundle |
|
|
2462
|
-
| `QuestionCard` `variant` / `density` / `showShortcuts` / `number` / `showFeedback` | — | `'card'`, `'comfortable'`, `false`, —, `true` | framed or plain, spacing, key badges, "Question 2", the verdict banner |
|
|
2463
|
+
| `QuestionCard` `variant` / `density` / `showShortcuts` / `number` / `showFeedback` | — | `'card'`, `'comfortable'`, `false`, —, `true` | framed or plain, spacing, key badges, "Question 2", the verdict banner (for a card with marks and no banner, use `preview`) |
|
|
2463
2464
|
|
|
2464
2465
|
An open answer and a code answer, with the code editor from its own subpath
|
|
2465
2466
|
(it is client-only, so load it lazily in an SSR app):
|
|
@@ -417,7 +417,7 @@ disabled with the field; Backspace in the empty input removes the last one.
|
|
|
417
417
|
| `getOptionValue`, `getOptionLabel` | `(option: T) => string` | `.value`, `.label` | for other option shapes |
|
|
418
418
|
| `getOptionLeading` | `(option: T) => ReactNode` | — | an avatar (or a flag) before the label, in the list and in the chip; `renderOption` replaces the whole row |
|
|
419
419
|
| `filter` | `'accent-insensitive' \| 'none' \| (option: T, query: string) => boolean` | `'accent-insensitive'`, `'none'` with `onSearch` | how the list is filtered as the user types: label or value, ignoring case and accents (`e` finds "École") in the provider's language; `'none'` shows `options` as given; a function gets the trimmed query, never an empty one |
|
|
420
|
-
| `freeSolo` | `boolean` | — | accept a typed value that is not an option |
|
|
420
|
+
| `freeSolo` | `boolean` | — | accept a typed value that is not an option; Enter adds it |
|
|
421
421
|
| `onSearch` | `(query: string, { signal }: { signal: AbortSignal }) => Promise<T[]>` | — | server search: called once typing pauses; its results become the list. Each search aborts the previous one, and a response that lands after a newer one is dropped |
|
|
422
422
|
| `onSearchError` | `(error: unknown) => void` | — | `onSearch` rejected (an abort is not a rejection) |
|
|
423
423
|
| `minQueryLength` | `number` | `1` | `onSearch` waits for this many characters; below it the list says "Type at least N characters to search." |
|
|
@@ -433,6 +433,12 @@ disabled with the field; Backspace in the empty input removes the last one.
|
|
|
433
433
|
| `disabled` | `boolean` | — | disables the input and each chip's remove button |
|
|
434
434
|
| `error` | `boolean` | — | error border on the chips box, error helper text, `aria-invalid` on the input |
|
|
435
435
|
|
|
436
|
+
Enter in a form: it acts on the list and does not submit the form while it has
|
|
437
|
+
something to do (WAI-ARIA combobox practice). It picks the highlighted option;
|
|
438
|
+
with `freeSolo` and no option highlighted it adds the typed text; with the list
|
|
439
|
+
open it is only default-prevented. On a closed list with nothing typed it
|
|
440
|
+
submits, as a plain input does.
|
|
441
|
+
|
|
436
442
|
An avatar in the rows and the chips:
|
|
437
443
|
`getOptionLeading={(person) => <Avatar src={person.photo} alt="" />}`; see
|
|
438
444
|
[the field look](../guides/field-look-2-x.md#autocomplete-and-the-chips-box).
|
|
@@ -471,8 +477,9 @@ text, as a `Select` shows it, and keeps the search:
|
|
|
471
477
|
gets the label back on leave: the value can only be replaced by another pick.
|
|
472
478
|
- The option's `getOptionLeading` (or `renderSelectedLeading`) avatar shows
|
|
473
479
|
before the text while it is picked.
|
|
474
|
-
- `freeSolo`
|
|
475
|
-
not added is dropped on leave, as
|
|
480
|
+
- `freeSolo` adds through its "Add" button, or with Enter when no option is
|
|
481
|
+
highlighted; text left in the input and not added is dropped on leave, as
|
|
482
|
+
any other typed text.
|
|
476
483
|
- Keep the selected option in `options` (as with `onSearch` and chips): a value
|
|
477
484
|
with no matching option has no label, so the input stays empty.
|
|
478
485
|
- The input is the combobox: `aria-expanded` follows the list,
|
package/docs/guides/forms.md
CHANGED
|
@@ -115,7 +115,7 @@ export function PlanForm() {
|
|
|
115
115
|
| The field | Touched when |
|
|
116
116
|
| --- | --- |
|
|
117
117
|
| one input (`TextFormField`, `EmailFormField`, `TextareaFormField`, `SwitchFormField`, `CheckboxFormField`, …) | its input loses focus, as a native `blur` |
|
|
118
|
-
| several controls (`OpeningHoursFormField`, inline by default: focus leaves the week editor and its phone sheet; `ContactsFormField`, inline by default: focus leaves the rows and the phone sheet; `AttributesFormField`, inline by default: focus leaves the rows, their open fields and the phone sheet; `PasswordFormField` and its eye button, `NumberFormField` and its steppers, `PhoneFormField`, `OtpFormField`, `SliderFormField` with two thumbs, `DurationFormField`, `RatingFormField`, `RadioGroupFormField`, `CheckboxGroupFormField`, `ChipGroupFormField`, `ChoiceCardGroupFormField`, `SortableListFormField`, `TransferListFormField`, `InlineEditFormField`, `LocalizedTextFormField`, `AttributeValuesFormFieldSet`, …) | focus leaves all of them; moving between them is not leaving |
|
|
118
|
+
| several controls (`OpeningHoursFormField`, inline by default: focus leaves the week editor and its phone sheet; `ContactsFormField`, inline by default: focus leaves the rows and the phone sheet; `AttributesFormField`, inline by default: focus leaves the rows, their open fields and the phone sheet; `PasswordFormField` and its eye button, `NumberFormField` and its steppers, `QuantityFormField`, `PhoneFormField`, `OtpFormField`, `SliderFormField` with two thumbs, `DurationFormField`, `RatingFormField`, `RadioGroupFormField`, `CheckboxGroupFormField`, `ChipGroupFormField`, `ChoiceCardGroupFormField`, `SortableListFormField`, `TransferListFormField`, `InlineEditFormField`, `LocalizedTextFormField`, `AttributeValuesFormFieldSet`, …) | focus leaves all of them; moving between them is not leaving |
|
|
119
119
|
| a picker (`SelectFormField`, `ComboboxFormField`, `AutocompleteFormField`, `TreeSelectFormField`, `CountryFormField`, `TimezoneFormField`, `ColorFormField`, `DateFormField`, `DateRangeFormField`, `TimeFormField`, …) | focus leaves the trigger **and** the popover it opened. Opening the popover, or picking in it, is not leaving; closing it hands focus back to the trigger, and the next Tab or click elsewhere is. A typed `DateFormField`, `DateRangeFormField` or `TimeFormField` is one trigger: moving between its segments (both ends of a range) and its buttons is not leaving |
|
|
120
120
|
| a dialog editor (`PostalAddressFormField`, `ContactsFormField` with `editing="dialog"`, `OpeningHoursFormField` with `editing="dialog"`, `AttributesFormField` with `editing="dialog"`, and `LocationFormField` from `@nxgt/material/map`) | focus leaves its buttons and the dialog they open. Closing the dialog hands focus back to the button that opened it |
|
|
121
121
|
| an upload (`UploadFormField`, `NetworkUploadFormField`, `S3UploadFormField`, `ImageFormField`, `ImageS3FormField`, `FileListFormField`, `FileListS3FormField`) | focus leaves the drop zone and its buttons. Choosing a file in the browser's own dialog is not leaving |
|
|
@@ -485,8 +485,9 @@ or fails validation.
|
|
|
485
485
|
| `PasswordField` · `PasswordFormField` | `string` | `''` |
|
|
486
486
|
| `NumberField` · `NumberFormField` | `number \| null`. `null` when cleared; clamped to `min` / `max` on blur | `null` |
|
|
487
487
|
| `PercentField` · `PercentFormField` | `number \| null`, `min` 0, `max` 100, `step` 1 by default | `null` |
|
|
488
|
+
| `QuantityField` · `QuantityFormField` | `{ value: number \| null, unit: string }`. An error on the field, `.value` or `.unit` shows as one message under the pair | `{ value: null, unit: '' }` |
|
|
488
489
|
| `CurrencyField` · `CurrencyFormField` | `number \| null`, the amount only, not rounded. `currency` (default `'EUR'`) and `fractionDigits` (the currency's own decimals by default) are display | `null` |
|
|
489
|
-
| `PhoneField` · `PhoneFormField` | E.164 `string` (`'+33612345678'`), or `null`
|
|
490
|
+
| `PhoneField` · `PhoneFormField` | E.164 `string` (`'+33612345678'`), or `null` when empty. `PhoneFormField` reports a non-empty number that is not yet possible as the field error `material.phone-field.incomplete`; `PhoneField` signals it with `onValidityChange`. `defaultCountry` is `'FR'` | `null` |
|
|
490
491
|
| `UrlField` · `UrlFormField` | `string` (as typed) | `''` |
|
|
491
492
|
| `SearchField` · `SearchFormField` | `string`, after `debounceMs` (default `0`) | `''` |
|
|
492
493
|
| `ColorField` · `ColorFormField` | `string` (hex, `'#1e293b'`) | `''` |
|
|
@@ -535,7 +536,7 @@ or fails validation.
|
|
|
535
536
|
| Field · wrapper | Form value | Empty default |
|
|
536
537
|
| --- | --- | --- |
|
|
537
538
|
| `NetworkUploadField` · `NetworkUploadFormField` | `mode="single"`: the `string` your `onUpload` resolved (a URL or a key), or `null` after removal. `mode="multiple"`: `string[]` | `undefined` / `[]` |
|
|
538
|
-
| `S3UploadField` · `S3UploadFormField` | as above; the value is `publicUrl` from `getPresignedUrl` | `undefined` / `[]` |
|
|
539
|
+
| `S3UploadField` · `S3UploadFormField` | as above; the value is `publicUrl` (PUT) or `value` (POST) from `getPresignedUrl` | `undefined` / `[]` |
|
|
539
540
|
| `ImageField`, `ImageS3Field` · `ImageFormField`, `ImageS3FormField` | `string \| null` (the uploaded URL) | `undefined` |
|
|
540
541
|
| `FileListField`, `FileListS3Field` · `FileListFormField`, `FileListS3FormField` | `string[]` | `[]` |
|
|
541
542
|
| `FilesFormField` | `mode="single"`: `string \| null`. `mode="multiple"`: `string[]` (typed or picked URLs) | `null` / `[]` |
|
|
Binary file
|
|
Binary file
|
package/docs/roadmap.md
CHANGED
|
@@ -6,8 +6,6 @@ version they shipped in, and planned entries may name the major they target.
|
|
|
6
6
|
|
|
7
7
|
## Now
|
|
8
8
|
|
|
9
|
-
- **Link navigation** — `TabsNav` and `SegmentedNav` render route tabs and segmented navigation as a `nav` of links with `aria-current="page"`, looking like Tabs and the segmented ToggleGroup, plus `useTabParam` to keep the current tab in the URL. Tracked in [#2029](https://github.com/softistx/nxgt-material/issues/2029).
|
|
10
|
-
|
|
11
9
|
- **Barcodes and label sheets** — `@nxgt/material/barcode` draws Code 128
|
|
12
10
|
and Data Matrix as accessible SVG (themed, dark-aware, black on white in
|
|
13
11
|
print) and lays labels out on a sheet to print: label size, columns, rows,
|
|
@@ -248,6 +246,9 @@ version they shipped in, and planned entries may name the major they target.
|
|
|
248
246
|
|
|
249
247
|
## Later
|
|
250
248
|
|
|
249
|
+
- **One `onUpload` options object (next major)** — the upload fields'
|
|
250
|
+
`onUpload(file, onProgress?, signal?)` becomes `onUpload(file, { onProgress,
|
|
251
|
+
signal })`, like the editor's upload context.
|
|
251
252
|
- **A strict theme `motif` (4.0)** — a theme background's `motif` with an
|
|
252
253
|
unknown key, a `layout` or `density` outside its list or a bad `icons` is
|
|
253
254
|
dropped in 3.22 (`@nxgt/material/themes`'s `defineTheme` warns for each one in
|
|
@@ -363,6 +364,8 @@ version they shipped in, and planned entries may name the major they target.
|
|
|
363
364
|
|
|
364
365
|
## Shipped
|
|
365
366
|
|
|
367
|
+
- **Link navigation (3.66.0)** — `TabsNav` and `SegmentedNav` render route tabs and segmented navigation as a `nav` of links with `aria-current="page"`, looking like Tabs and the segmented ToggleGroup, plus `useTabParam`, `skipTabChange` and `tabIds` to keep the current tab in the URL. Tracked in [#2029](https://github.com/softistx/nxgt-material/issues/2029).
|
|
368
|
+
|
|
366
369
|
- **Smaller, easier-to-read components (3.58.0)** — the library's own source
|
|
367
370
|
is kept to files under 200 lines and functions under 80, enforced by a
|
|
368
371
|
check, so a component is quicker to read and to fix. No API or behaviour
|
package/docs/troubleshooting.md
CHANGED
|
@@ -4675,7 +4675,12 @@ expect(await within(button.parentElement!).findByRole('status')).toHaveTextConte
|
|
|
4675
4675
|
error)`. The status may be any non-2xx.
|
|
4676
4676
|
**Why:** the storage rejected the `PUT` to the presigned URL. For `403` the
|
|
4677
4677
|
usual causes are an expired URL, or a `Content-Type` that differs from the
|
|
4678
|
-
one the URL was signed with.
|
|
4678
|
+
one the URL was signed with. With a presigned POST target, the policy refused
|
|
4679
|
+
the file (size, content type, expiry) and the message names the S3 code when
|
|
4680
|
+
it is readable, such as `Upload failed with status 400 (EntityTooLarge)`.
|
|
4681
|
+
The field shows it translated (`material.upload.error-status`, `error-status-code`);
|
|
4682
|
+
`error.message` in `onError` stays English, and `error` is an `UploadError`
|
|
4683
|
+
(`kind`, `status`, `code`).
|
|
4679
4684
|
**Fix:**
|
|
4680
4685
|
|
|
4681
4686
|
```ts
|
|
@@ -4690,12 +4695,14 @@ const getPresignedUrl = async (file: File) => {
|
|
|
4690
4695
|
|
|
4691
4696
|
**When:** an `S3UploadField` upload fails immediately. The browser console
|
|
4692
4697
|
shows a CORS error for the storage host.
|
|
4693
|
-
**Why:** the browser blocked the cross-origin `PUT
|
|
4694
|
-
do not allow your origin, the
|
|
4698
|
+
**Why:** the browser blocked the cross-origin `PUT` (or `POST` with a presigned
|
|
4699
|
+
POST target). The bucket's CORS rules do not allow your origin, the method, or
|
|
4700
|
+
the headers you send; a `Content-Security-Policy` without the bucket host in
|
|
4701
|
+
`connect-src` fails the same way.
|
|
4695
4702
|
**Fix:**
|
|
4696
4703
|
|
|
4697
4704
|
```json
|
|
4698
|
-
[{ "AllowedOrigins": ["https://app.example.com"], "AllowedMethods": ["PUT"], "AllowedHeaders": ["*"] }]
|
|
4705
|
+
[{ "AllowedOrigins": ["https://app.example.com"], "AllowedMethods": ["PUT", "POST"], "AllowedHeaders": ["*"] }]
|
|
4699
4706
|
```
|
|
4700
4707
|
|
|
4701
4708
|
Apply it on the bucket, with your own origin.
|