@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.
Files changed (94) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +5 -4
  3. package/dist/{alert-dialog-BhARh_qC.js → alert-dialog-CBkWG32g.js} +1 -1
  4. package/dist/{async-slice-CHyRU5V3.js → async-slice-CNZ3N55P.js} +1 -1
  5. package/dist/{button-Du7fJ9i_.js → button-D3qzqDY_.js} +1 -1
  6. package/dist/charts.js +3 -3
  7. package/dist/{chip-BVB6UYl-.js → chip-CMTe4sQd.js} +2 -2
  8. package/dist/{code-editor-D-GBs2yu.js → code-editor-Cs7mz1J-.js} +5 -5
  9. package/dist/code-editor.js +5 -5
  10. package/dist/{code-language-items-xrgq1Am0.js → code-language-items-D_QMgjVy.js} +2 -2
  11. package/dist/{components-iNCh4A6M.js → components-BbWcozcj.js} +17652 -17382
  12. package/dist/components.js +28 -28
  13. package/dist/{dialog-DFY_nsqF.js → dialog-B3BSdRci.js} +4 -4
  14. package/dist/dnd.js +1 -1
  15. package/dist/{dropdown-menu-B6Yh4XJu.js → dropdown-menu-BaupOWgS.js} +1 -1
  16. package/dist/editor.js +661 -661
  17. package/dist/{empty-state-DGtwE50c.js → empty-state-B2Um5UpI.js} +2 -2
  18. package/dist/{field-blur-Br7ij-a1.js → field-blur-1XLyxRPr.js} +1 -1
  19. package/dist/{field-blur-context-CucMAZLj.js → field-blur-context-XNvCDnMT.js} +1 -1
  20. package/dist/{hooks-CYQJkKGc.js → hooks-BU21Jpaa.js} +2 -2
  21. package/dist/hooks.js +2 -2
  22. package/dist/{hover-card-C2J0GwWf.js → hover-card-Bvm3svX9.js} +1 -1
  23. package/dist/i18n.js +3 -3
  24. package/dist/{icon-badge-LzO5K6Wu.js → icon-badge-wip9bfa2.js} +3 -3
  25. package/dist/{icon-button-vLJ4YrEW.js → icon-button-QwIeBTdw.js} +1 -1
  26. package/dist/lib/components/ui/collections/filter/use-filter-field-config.d.ts +3 -3
  27. package/dist/lib/components/ui/fields/contacts/placeholders.d.ts +1 -1
  28. package/dist/lib/components/ui/fields/country-field/index.d.ts +2 -0
  29. package/dist/lib/components/ui/fields/country-field/iso-countries.d.ts +2 -0
  30. package/dist/lib/components/ui/fields/index.d.ts +1 -0
  31. package/dist/lib/components/ui/fields/phone-field/index.d.ts +1 -1
  32. package/dist/lib/components/ui/fields/phone-field/parse-phone.d.ts +2 -0
  33. package/dist/lib/components/ui/fields/phone-field/phone-field.d.ts +8 -1
  34. package/dist/lib/components/ui/fields/phone-field/phone-form-field.d.ts +8 -1
  35. package/dist/lib/components/ui/fields/phone-field/use-phone-value.d.ts +2 -1
  36. package/dist/lib/components/ui/fields/postal-address/utils.d.ts +15 -0
  37. package/dist/lib/components/ui/fields/quantity-field/index.d.ts +3 -0
  38. package/dist/lib/components/ui/fields/quantity-field/quantity-field.d.ts +8 -0
  39. package/dist/lib/components/ui/fields/quantity-field/quantity-form-field.d.ts +8 -0
  40. package/dist/lib/components/ui/fields/quantity-field/quantity-row.d.ts +13 -0
  41. package/dist/lib/components/ui/fields/quantity-field/quantity-unit.d.ts +12 -0
  42. package/dist/lib/components/ui/fields/quantity-field/quantity-value.d.ts +9 -0
  43. package/dist/lib/components/ui/fields/quantity-field/types.d.ts +45 -0
  44. package/dist/lib/components/ui/media/upload/index.d.ts +1 -0
  45. package/dist/lib/components/ui/media/upload/network-upload-field/network-upload-field.d.ts +6 -1
  46. package/dist/lib/components/ui/media/upload/network-upload-field/network-upload-field.errors.d.ts +8 -0
  47. package/dist/lib/components/ui/media/upload/network-upload-field/network-upload-field.utils.d.ts +2 -7
  48. package/dist/lib/components/ui/media/upload/network-upload-field/use-start-upload.d.ts +7 -3
  49. package/dist/lib/components/ui/media/upload/network-upload-field/use-upload-item-handlers.d.ts +2 -1
  50. package/dist/lib/components/ui/media/upload/s3-upload-field/s3-upload-field.d.ts +2 -5
  51. package/dist/lib/components/ui/media/upload/s3-upload-field/s3-upload-field.utils.d.ts +26 -1
  52. package/dist/lib/components/ui/media/upload/upload-error.d.ts +33 -0
  53. package/dist/lib/components/ui/selection/autocomplete/autocomplete-enter.d.ts +18 -0
  54. package/dist/lib/components/ui/selection/autocomplete/partials/autocomplete-chip-input.d.ts +2 -1
  55. package/dist/lib/components/ui/selection/autocomplete/partials/autocomplete-text-input.d.ts +2 -1
  56. package/dist/lib/components/ui/selection/autocomplete/types.d.ts +3 -1
  57. package/dist/lib/components/ui/widgets/education/questions/partials/question-content.d.ts +1 -1
  58. package/dist/lib/components/ui/widgets/education/questions/question-text.d.ts +1 -1
  59. package/dist/lib/components/ui/widgets/education/questions/question.d.ts +1 -1
  60. package/dist/lib/components/ui/widgets/education/questions/types.d.ts +10 -0
  61. package/dist/lib/i18n/resources/en/en.d.ts +9 -0
  62. package/dist/lib/i18n/resources/en/material.json.d.ts +10 -1
  63. package/dist/lib/i18n/resources/fr/fr.d.ts +9 -0
  64. package/dist/lib/i18n/resources/fr/material.json.d.ts +10 -1
  65. package/dist/lib/i18n/resources/resources.d.ts +18 -0
  66. package/dist/lib/i18n/translator.d.ts +1 -1
  67. package/dist/lib.js +1 -1
  68. package/dist/main.js +42 -42
  69. package/dist/map.js +8 -8
  70. package/dist/pdf-reader.js +1 -1
  71. package/dist/print.js +1 -1
  72. package/dist/{public-BHthpdpe.js → public-BuGgOtAl.js} +9 -9
  73. package/dist/{rich-code-block-DoW7l2ND.js → rich-code-block-DdHqzzek.js} +5 -5
  74. package/dist/{sortable-provider-FLNMfr0o.js → sortable-provider-DMyaSfZZ.js} +2 -2
  75. package/dist/{spinner-DCgDw8eq.js → spinner-ByYdcukU.js} +1 -1
  76. package/dist/theme-picker.js +2 -2
  77. package/dist/{theme-toggle-BMDAwMLh.js → theme-toggle-DB3aZZnO.js} +1 -1
  78. package/dist/{toolbar-items-CkPEhQq7.js → toolbar-items-DbzIKjo7.js} +3 -3
  79. package/dist/{translate-material-DkcuQBNr.js → translate-material-Dh7MYkHT.js} +1 -1
  80. package/dist/{translator-DXdVRdMm.js → translator-Byhohi1S.js} +20 -2
  81. package/dist/{typography-BNS_IbFI.js → typography-rUf0mE5a.js} +1 -1
  82. package/dist/{use-chat-composer-B-tGRLkI.js → use-chat-composer-uUUTRB3Z.js} +2401 -2343
  83. package/dist/{use-heading-spy-DaqibCCm.js → use-heading-spy-CVzW2NM3.js} +4 -4
  84. package/dist/{use-material-translation-Bxl8sv1n.js → use-material-translation-BAYyZOcH.js} +1 -1
  85. package/dist/{use-return-focus-Y_Kx0QlW.js → use-return-focus-CQAuk9kh.js} +1 -1
  86. package/docs/components/form-fields.md +97 -3
  87. package/docs/components/media-and-files.md +55 -9
  88. package/docs/components/rich-widgets.md +2 -1
  89. package/docs/components/selection-and-choice.md +10 -3
  90. package/docs/guides/forms.md +4 -3
  91. package/docs/previews/form-fields-quantityfield.webp +0 -0
  92. package/docs/roadmap.md +3 -0
  93. package/docs/troubleshooting.md +11 -4
  94. 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-BhARh_qC.js";
3
- import { a as l } from "./button-Du7fJ9i_.js";
4
- import { i as u } from "./translate-material-DkcuQBNr.js";
5
- import { t as d } from "./use-material-translation-Bxl8sv1n.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-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,4 +1,4 @@
1
- import { i as e, t } from "./translate-material-DkcuQBNr.js";
1
+ import { i as e, t } from "./translate-material-Dh7MYkHT.js";
2
2
  import { useCallback as n } from "react";
3
3
  //#region lib/i18n/use-material-translation.ts
4
4
  function r() {
@@ -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-DCgDw8eq.js";
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
+ ![QuantityField](../previews/form-fields-quantityfield.webp)
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
  ![CurrencyField](../previews/form-fields-currencyfield.webp)
@@ -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
- **Emits:** E.164 `string`, or `null` while the number is incomplete.
670
- **Form wrapper:** `PhoneFormField` — `control` and `name`, see
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 `https://files.example.com/${file.name}`;
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? }>` | — | `S3UploadField` only; PUTs the file to `uploadUrl`, stores `publicUrl` |
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` still adds through its "Add" button; text left in the input and
475
- not added is dropped on leave, as any other typed text.
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,
@@ -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` while the number is not yet a possible one. `defaultCountry` is `'FR'` | `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` / `[]` |
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
@@ -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`. The bucket's CORS rules
4694
- do not allow your origin, the `PUT` method, or the headers you send.
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxgt/material",
3
- "version": "3.68.0",
3
+ "version": "3.70.0",
4
4
  "license": "MIT",
5
5
  "bugs": {
6
6
  "email": "stevegires@gmail.com"