@nxgt/material 3.68.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 +28 -0
- package/README.md +5 -4
- package/dist/{alert-dialog-BhARh_qC.js → alert-dialog-CBkWG32g.js} +1 -1
- package/dist/{async-slice-CHyRU5V3.js → async-slice-CNZ3N55P.js} +1 -1
- package/dist/{button-Du7fJ9i_.js → button-D3qzqDY_.js} +1 -1
- package/dist/charts.js +3 -3
- package/dist/{chip-BVB6UYl-.js → chip-CMTe4sQd.js} +2 -2
- package/dist/{code-editor-D-GBs2yu.js → code-editor-Cs7mz1J-.js} +5 -5
- package/dist/code-editor.js +5 -5
- package/dist/{code-language-items-xrgq1Am0.js → code-language-items-D_QMgjVy.js} +2 -2
- package/dist/{components-iNCh4A6M.js → components-BbWcozcj.js} +17652 -17382
- package/dist/components.js +28 -28
- package/dist/{dialog-DFY_nsqF.js → dialog-B3BSdRci.js} +4 -4
- package/dist/dnd.js +1 -1
- package/dist/{dropdown-menu-B6Yh4XJu.js → dropdown-menu-BaupOWgS.js} +1 -1
- package/dist/editor.js +661 -661
- package/dist/{empty-state-DGtwE50c.js → empty-state-B2Um5UpI.js} +2 -2
- package/dist/{field-blur-Br7ij-a1.js → field-blur-1XLyxRPr.js} +1 -1
- package/dist/{field-blur-context-CucMAZLj.js → field-blur-context-XNvCDnMT.js} +1 -1
- package/dist/{hooks-CYQJkKGc.js → hooks-BU21Jpaa.js} +2 -2
- package/dist/hooks.js +2 -2
- package/dist/{hover-card-C2J0GwWf.js → hover-card-Bvm3svX9.js} +1 -1
- package/dist/i18n.js +3 -3
- package/dist/{icon-badge-LzO5K6Wu.js → icon-badge-wip9bfa2.js} +3 -3
- package/dist/{icon-button-vLJ4YrEW.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/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 +1 -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 +9 -0
- package/dist/lib/i18n/resources/en/material.json.d.ts +10 -1
- package/dist/lib/i18n/resources/fr/fr.d.ts +9 -0
- package/dist/lib/i18n/resources/fr/material.json.d.ts +10 -1
- package/dist/lib/i18n/resources/resources.d.ts +18 -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/pdf-reader.js +1 -1
- package/dist/print.js +1 -1
- package/dist/{public-BHthpdpe.js → public-BuGgOtAl.js} +9 -9
- package/dist/{rich-code-block-DoW7l2ND.js → rich-code-block-DdHqzzek.js} +5 -5
- package/dist/{sortable-provider-FLNMfr0o.js → sortable-provider-DMyaSfZZ.js} +2 -2
- package/dist/{spinner-DCgDw8eq.js → spinner-ByYdcukU.js} +1 -1
- package/dist/theme-picker.js +2 -2
- package/dist/{theme-toggle-BMDAwMLh.js → theme-toggle-DB3aZZnO.js} +1 -1
- package/dist/{toolbar-items-CkPEhQq7.js → toolbar-items-DbzIKjo7.js} +3 -3
- package/dist/{translate-material-DkcuQBNr.js → translate-material-Dh7MYkHT.js} +1 -1
- package/dist/{translator-DXdVRdMm.js → translator-Byhohi1S.js} +20 -2
- package/dist/{typography-BNS_IbFI.js → typography-rUf0mE5a.js} +1 -1
- package/dist/{use-chat-composer-B-tGRLkI.js → use-chat-composer-uUUTRB3Z.js} +2401 -2343
- package/dist/{use-heading-spy-DaqibCCm.js → use-heading-spy-CVzW2NM3.js} +4 -4
- package/dist/{use-material-translation-Bxl8sv1n.js → use-material-translation-BAYyZOcH.js} +1 -1
- package/dist/{use-return-focus-Y_Kx0QlW.js → use-return-focus-CQAuk9kh.js} +1 -1
- package/docs/components/form-fields.md +97 -3
- package/docs/components/media-and-files.md +55 -9
- 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-quantityfield.webp +0 -0
- package/docs/roadmap.md +3 -0
- 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";
|
|
@@ -19,6 +19,7 @@ for the value type each one writes. Everything is imported from
|
|
|
19
19
|
| <img src="../previews/form-fields-passwordfield.webp" width="96" alt=""> | [PasswordField](#passwordfield) | Show / hide toggle and a strength meter |
|
|
20
20
|
| <img src="../previews/form-fields-numberfield.webp" width="96" alt=""> | [NumberField](#numberfield) | `number \| null` with clamping and +/− buttons |
|
|
21
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 |
|
|
22
23
|
| <img src="../previews/form-fields-currencyfield.webp" width="96" alt=""> | [CurrencyField](#currencyfield) | Locale-formatted amount with a currency suffix |
|
|
23
24
|
| <img src="../previews/form-fields-phonefield.webp" width="96" alt=""> | [PhoneField](#phonefield) | International number, emits E.164 |
|
|
24
25
|
| <img src="../previews/form-fields-urlfield.webp" width="96" alt=""> | [UrlField](#urlfield) | Validates on blur, open and copy buttons |
|
|
@@ -610,6 +611,79 @@ ratio (`0.15`).
|
|
|
610
611
|
|
|
611
612
|
---
|
|
612
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
|
+
|
|
613
687
|
## CurrencyField
|
|
614
688
|
|
|
615
689
|

|
|
@@ -665,9 +739,19 @@ export function Mobile() {
|
|
|
665
739
|
| `value` / `defaultValue` | `string \| null` | `null` | an E.164 number (`+33612345678`) |
|
|
666
740
|
| `onChange` | `(value: string \| null) => void` | — | `null` until the typed number is possible for the country |
|
|
667
741
|
| `defaultCountry` | `string` (ISO 3166-1) | `'FR'` | the country used when the value has no `+` prefix |
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
**
|
|
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
|
|
671
755
|
[forms](../guides/forms.md#the-pair) and [what each field emits](../guides/forms.md#what-each-field-emits).
|
|
672
756
|
**Storybook:** `Form fields/PhoneField` (and `/Field look`) · **Vue:** `PhoneField`, `PhoneFormField`
|
|
673
757
|
|
|
@@ -1870,6 +1954,16 @@ Parts: `PostalAddress` (the card), `PostalAddressDialog` (the edit form).
|
|
|
1870
1954
|
| `onEdit` | `() => void` | — | shows the edit button |
|
|
1871
1955
|
| `disabled` | `boolean` | — | hides it |
|
|
1872
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
|
+
|
|
1873
1967
|
`PostalAddressFormField` and `PostalAddressDialog` take `cancelVariant` (the
|
|
1874
1968
|
dialog's Cancel button, a `Button` `variant`, default `'tonal'`; `'outlined'`
|
|
1875
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
|
|
|
@@ -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
|
package/docs/roadmap.md
CHANGED
|
@@ -246,6 +246,9 @@ version they shipped in, and planned entries may name the major they target.
|
|
|
246
246
|
|
|
247
247
|
## Later
|
|
248
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.
|
|
249
252
|
- **A strict theme `motif` (4.0)** — a theme background's `motif` with an
|
|
250
253
|
unknown key, a `layout` or `density` outside its list or a bad `icons` is
|
|
251
254
|
dropped in 3.22 (`@nxgt/material/themes`'s `defineTheme` warns for each one in
|
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.
|