@nxgt/material 2.16.0 → 2.17.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 +76 -0
- package/README.md +9 -4
- package/dist/{_Uint8Array-CjLUgWgf.js → _Uint8Array-C7apDrb7.js} +1 -1
- package/dist/{_overRest-HfSwfmH1.js → _overRest-CCAbV-eT.js} +1 -1
- package/dist/async-slice-Cuekts5f.js +459 -0
- package/dist/{blockDiagram-BEXU5L5S-DSNWJcbS.js → blockDiagram-BEXU5L5S-K2MjXCm9.js} +2 -2
- package/dist/{chunk-3FUC2YCW-CcZvlyvW.js → chunk-3FUC2YCW-6KyD6CAZ.js} +2 -2
- package/dist/{chunk-7M6MHVWA-21OdxoBq.js → chunk-7M6MHVWA-CNWcBB_6.js} +1 -1
- package/dist/{chunk-GNY47TPC-BZdgq2LF.js → chunk-GNY47TPC-DdqgSBxM.js} +4 -4
- package/dist/{chunk-NETBCI7D-CAgvlb0w.js → chunk-NETBCI7D-ZnunhYtw.js} +1 -1
- package/dist/{chunk-Z7XXMR3K-BSXPgLtN.js → chunk-Z7XXMR3K-C8MhuVIi.js} +1 -1
- package/dist/{classDiagram-v2-NBCMYWYE-D_BHO1iT.js → classDiagram-v2-NBCMYWYE-ByUAGkfa.js} +2 -2
- package/dist/components/components.css +1 -0
- package/dist/components/print-only.css +44 -0
- package/dist/{components-Bkdm6jn6.js → components-CIYQrI45.js} +59379 -58878
- package/dist/components.js +6 -6
- package/dist/{dagre-6A5THRUB-CsnftAPx.js → dagre-6A5THRUB-C2vz5UY9.js} +64 -64
- package/dist/{diagram-22UHCM2B-D0yPa8vo.js → diagram-22UHCM2B-DREk9ku4.js} +2 -2
- package/dist/{diagram-3UASUU5V-f8LIX2sj.js → diagram-3UASUU5V-CYkPaos5.js} +1 -1
- package/dist/dnd.js +1 -1
- package/dist/{elk-276RUBZZ-DI8bDQAL.js → elk-276RUBZZ-CK4KoCtK.js} +3 -3
- package/dist/{erDiagram-OPXOYQCR-BGHFtxmB.js → erDiagram-OPXOYQCR-CTfn8QUl.js} +2 -2
- package/dist/{field-blur-C-5K6U1F.js → field-blur-3RW0mJb2.js} +2 -2
- package/dist/{flowDiagram-KWPJA3E3-CnFLTW-3.js → flowDiagram-KWPJA3E3-CDzuzre6.js} +3 -3
- package/dist/{ganttDiagram-FUAMR5RP-B5uyVKE4.js → ganttDiagram-FUAMR5RP-Ci-9ZJz5.js} +1 -1
- package/dist/{graphlib-BqO9Go2u.js → graphlib-DxjKaXeR.js} +42 -42
- package/dist/hooks.js +38 -26
- package/dist/{i18n-provider-C3SlA4J0.js → i18n-provider-Dv9ZSmgO.js} +1 -1
- package/dist/i18n.js +6 -6
- package/dist/{isArrayLikeObject-83P1fvEs.js → isArrayLikeObject-H8k6W51O.js} +3 -3
- package/dist/{isEmpty-C2z3Hypl.js → isEmpty-B0Bavg-e.js} +9 -9
- package/dist/{journeyDiagram-ZHPQQLJL-CKtbGcVT.js → journeyDiagram-ZHPQQLJL-CwFYd51O.js} +1 -1
- package/dist/lib/components/ui/actions/copy-button/use-copy-status.d.ts +13 -0
- package/dist/lib/components/ui/display/enrolment-secret/enrolment-secret.d.ts +30 -0
- package/dist/lib/components/ui/display/enrolment-secret/index.d.ts +2 -0
- package/dist/lib/components/ui/display/enrolment-secret/use-enrolment-secret-labels.d.ts +11 -0
- package/dist/lib/components/ui/display/index.d.ts +2 -0
- package/dist/lib/components/ui/display/recovery-codes/index.d.ts +4 -0
- package/dist/lib/components/ui/display/recovery-codes/partials/recovery-codes-actions.d.ts +14 -0
- package/dist/lib/components/ui/display/recovery-codes/partials/recovery-codes-confirm.d.ts +13 -0
- package/dist/lib/components/ui/display/recovery-codes/partials/recovery-codes-list.d.ts +5 -0
- package/dist/lib/components/ui/display/recovery-codes/recovery-codes.d.ts +8 -0
- package/dist/lib/components/ui/display/recovery-codes/types.d.ts +64 -0
- package/dist/lib/components/ui/display/recovery-codes/use-recovery-codes-labels.d.ts +7 -0
- package/dist/lib/components/ui/fields/form-section/form-section-classes.d.ts +30 -0
- package/dist/lib/components/ui/fields/form-section/form-section.d.ts +25 -0
- package/dist/lib/components/ui/fields/form-section/index.d.ts +1 -0
- package/dist/lib/components/ui/fields/index.d.ts +1 -0
- package/dist/lib/components/ui/forms/index.d.ts +1 -0
- package/dist/lib/components/ui/forms/use-code-resolver.d.ts +42 -0
- package/dist/lib/components/ui/layout/auth-card/auth-card-notice.d.ts +18 -0
- package/dist/lib/components/ui/layout/auth-card/auth-card.d.ts +35 -0
- package/dist/lib/components/ui/layout/auth-card/index.d.ts +1 -0
- package/dist/lib/components/ui/layout/index.d.ts +1 -0
- package/dist/lib/hooks/index.d.ts +2 -0
- package/dist/lib/hooks/use-focus-on-mount.d.ts +24 -0
- package/dist/lib/hooks/use-submit-once.d.ts +32 -0
- package/dist/lib/i18n/resources/en/en.d.ts +21 -0
- package/dist/lib/i18n/resources/en/material.json.d.ts +21 -0
- package/dist/lib/i18n/resources/fr/fr.d.ts +21 -0
- package/dist/lib/i18n/resources/fr/material.json.d.ts +21 -0
- package/dist/lib/i18n/resources/resources.d.ts +42 -0
- package/dist/lib/i18n/translator.d.ts +1 -1
- package/dist/lib/lib/index.d.ts +1 -0
- package/dist/lib.js +58 -102
- package/dist/main.js +16 -16
- package/dist/map.js +22 -22
- package/dist/material.css +1 -1
- package/dist/{merge-CZIEHTTe.js → merge-Js8537dg.js} +4 -4
- package/dist/{mermaid.core-Cp75fWA9.js → mermaid.core-DW7UlwJu.js} +21 -21
- package/dist/{mindmap-definition-NLK3R4M7-CFYIlEfW.js → mindmap-definition-NLK3R4M7-CsLyIyEF.js} +2 -2
- package/dist/{pieDiagram-5QR66LMP-CjvtBYDr.js → pieDiagram-5QR66LMP-C-dfffj5.js} +1 -1
- package/dist/print.js +2 -2
- package/dist/{quadrantDiagram-O4NWA36T-DPj8ndqA.js → quadrantDiagram-O4NWA36T-DZ0TivLD.js} +1 -1
- package/dist/{requirementDiagram-PLB6GJNP-DjVFwRJg.js → requirementDiagram-PLB6GJNP-3brF-h1L.js} +2 -2
- package/dist/{sankeyDiagram-IPEJSGJF-63wsgHUY.js → sankeyDiagram-IPEJSGJF-BY-LLyiZ.js} +1 -1
- package/dist/{sortable-provider-Chbn1x1i.js → sortable-provider-CUT5Wfdl.js} +1 -1
- package/dist/{stateDiagram-v2-GCMORJYK-DNc8m371.js → stateDiagram-v2-GCMORJYK-BGavr7Nm.js} +2 -2
- package/dist/{stored-language-DPqzxRmf.js → stored-language-BKoohv2_.js} +42 -0
- package/dist/{swimlanes-2SLR337P-B7_gOJxx.js → swimlanes-2SLR337P-B6Fkr7-U.js} +3 -3
- package/dist/{swimlanesDiagram-TC7HE7FX-tqlBrGuu.js → swimlanesDiagram-TC7HE7FX-Dt6Tqj4h.js} +3 -3
- package/dist/{timeline-definition-EJHVYXUP-DIA9CBZK.js → timeline-definition-EJHVYXUP-CE-3KwYd.js} +1 -1
- package/dist/{translate-material-CqB-j9WD.js → translate-material-CCJg3yfc.js} +2 -2
- package/dist/{translator-DTl9h__E.js → translator-DEde7BKz.js} +1 -1
- package/dist/{use-heading-spy-C1KA3PrF.js → use-heading-spy-XkSExwoT.js} +67 -59
- package/dist/{use-material-translation-DMa4Xrnd.js → use-material-translation-Cndoq_iK.js} +2 -2
- package/dist/{usecaseDiagram-POWQR4AR-FMjCkoxs.js → usecaseDiagram-POWQR4AR-CPOCe5We.js} +2 -2
- package/dist/{xychartDiagram-PMCCYNJV-B76HOkCh.js → xychartDiagram-PMCCYNJV-Cleaihn8.js} +1 -1
- package/docs/components/README.md +3 -3
- package/docs/components/data-display.md +101 -0
- package/docs/components/form-fields.md +78 -0
- package/docs/components/navigation-and-layout.md +51 -0
- package/docs/guides/forms.md +111 -0
- package/docs/guides/helpers.md +81 -0
- package/docs/guides/i18n.md +17 -0
- package/docs/guides/printing.md +27 -0
- package/docs/previews/data-display-enrolmentsecret.webp +0 -0
- package/docs/previews/data-display-recoverycodes.webp +0 -0
- package/docs/previews/form-fields-formsection.webp +0 -0
- package/docs/previews/navigation-layout-authcard.webp +0 -0
- package/docs/roadmap.md +15 -0
- package/docs/troubleshooting.md +72 -0
- package/lib/components/components.css +1 -0
- package/lib/components/print-only.css +44 -0
- package/package.json +2 -2
- package/dist/async-slice-CsE3hkCD.js +0 -411
package/docs/guides/forms.md
CHANGED
|
@@ -238,6 +238,38 @@ export function SubscribeForm({
|
|
|
238
238
|
- Default values must have the type the field emits. See
|
|
239
239
|
[What each field emits](#what-each-field-emits).
|
|
240
240
|
|
|
241
|
+
## A long form in sections
|
|
242
|
+
|
|
243
|
+
A long form reads better in titled sections. Lay it out in the form grid
|
|
244
|
+
(`.form-grid-layout`, its fields in `[data-slot="form-fields"]`, its buttons
|
|
245
|
+
in `[data-slot="form-actions"]`) and put each group of fields in a
|
|
246
|
+
[`FormSection`](../components/form-fields.md#formsection):
|
|
247
|
+
|
|
248
|
+
```tsx
|
|
249
|
+
<form className="form-grid-layout" onSubmit={form.handleSubmit(onSave)}>
|
|
250
|
+
<div data-slot="form-fields">
|
|
251
|
+
<FormSection title="Profile">
|
|
252
|
+
<TextFormField control={form.control} name="firstName" label="First name" />
|
|
253
|
+
<TextFormField control={form.control} name="lastName" label="Last name" />
|
|
254
|
+
</FormSection>
|
|
255
|
+
<FormSection title="Notifications" description="You can change these at any time.">
|
|
256
|
+
<SwitchFormField control={form.control} name="digest" label="Digest" placeholder="Email me a weekly digest" />
|
|
257
|
+
</FormSection>
|
|
258
|
+
</div>
|
|
259
|
+
<div data-slot="form-actions">
|
|
260
|
+
<Button type="submit" loading={form.formState.isSubmitting}>Save</Button>
|
|
261
|
+
</div>
|
|
262
|
+
</form>
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
- Each section's title is an `h2` (pass `titleAs="h3"` when the form sits
|
|
266
|
+
under an `h2` of your own), so a screen reader moves from section to
|
|
267
|
+
section, and each section is a group named by its title.
|
|
268
|
+
- The sections take a whole row of the grid and lay their fields out in the
|
|
269
|
+
grid's columns, one column on a phone; switches, textareas, chips and file
|
|
270
|
+
fields still take a row of their own. A rule separates each section from the
|
|
271
|
+
one before it (`separated={false}` drops it).
|
|
272
|
+
|
|
241
273
|
## Validation messages
|
|
242
274
|
|
|
243
275
|
`zodLocaleError()` from `@nxgt/material/lib` is a Zod v4 error map that
|
|
@@ -302,6 +334,85 @@ How it picks the language, and what that implies:
|
|
|
302
334
|
`This value must be a number.`); a type with no string of its own says
|
|
303
335
|
`This value has the wrong type.`
|
|
304
336
|
|
|
337
|
+
### Wording errors by code
|
|
338
|
+
|
|
339
|
+
`zodLocaleError` words zod's own issues; a rule of your own — a name already
|
|
340
|
+
taken, a code the API sends back — has words only your app knows.
|
|
341
|
+
`useCodeResolver(schema, options?)` (`@nxgt/material/components`) is a
|
|
342
|
+
react-hook-form resolver that words both in the page's language: a
|
|
343
|
+
refinement carrying a code in `params.code` reads as your translator's words
|
|
344
|
+
for that code, every other issue as `zodLocaleError`'s. The language is
|
|
345
|
+
`I18nProvider`'s (or the `language` option); the resolver is made once per
|
|
346
|
+
schema and language.
|
|
347
|
+
|
|
348
|
+
```tsx
|
|
349
|
+
// i18n.ts — the app's translator, over the package's bundle
|
|
350
|
+
import { createTranslator, mergeResources, resources } from '@nxgt/material/i18n';
|
|
351
|
+
|
|
352
|
+
export const appResources = mergeResources(resources, {
|
|
353
|
+
en: { errors: { taken: 'This name is already taken.', tooShort: 'At least {min} characters.' } },
|
|
354
|
+
fr: { errors: { taken: 'Ce nom est déjà pris.', tooShort: 'Au moins {min} caractères.' } },
|
|
355
|
+
});
|
|
356
|
+
export const translate = createTranslator<string>(appResources);
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
```tsx
|
|
360
|
+
// sign-up-form.tsx
|
|
361
|
+
import { useForm } from 'react-hook-form';
|
|
362
|
+
import { z } from 'zod';
|
|
363
|
+
import { Button, TextFormField, useCodeResolver } from '@nxgt/material/components';
|
|
364
|
+
import { translate } from './i18n';
|
|
365
|
+
|
|
366
|
+
const TAKEN = ['admin', 'root'];
|
|
367
|
+
|
|
368
|
+
const schema = z.object({
|
|
369
|
+
// Worded by material's zod messages: "Invalid email address."
|
|
370
|
+
email: z.email(),
|
|
371
|
+
username: z
|
|
372
|
+
.string()
|
|
373
|
+
.refine((name) => name.length >= 3, { params: { code: 'errors.tooShort', min: 3 } })
|
|
374
|
+
.refine((name) => !TAKEN.includes(name), { params: { code: 'errors.taken' } }),
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
export function SignUpForm({ save }: { save: (values: z.output<typeof schema>) => Promise<void> }) {
|
|
378
|
+
const resolver = useCodeResolver(schema, { translate });
|
|
379
|
+
const { control, handleSubmit } = useForm({ resolver, defaultValues: { email: '', username: '' } });
|
|
380
|
+
return (
|
|
381
|
+
<form noValidate onSubmit={handleSubmit(save)}>
|
|
382
|
+
<TextFormField control={control} name="email" label="Email" />
|
|
383
|
+
<TextFormField control={control} name="username" label="Username" />
|
|
384
|
+
<Button type="submit">Sign up</Button>
|
|
385
|
+
</form>
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
- **The codes.** `translate(code, params, language)`: `params` is the
|
|
391
|
+
refinement's whole `params`, so `{ code, min: 3 }` fills `{min}`. A key
|
|
392
|
+
your translator returns verbatim (what `createTranslator` does with a key
|
|
393
|
+
it lacks) has no words: the issue falls back to `zodLocaleError`.
|
|
394
|
+
- **The rest.** The same `translate` is handed to `zodLocaleError`, so your
|
|
395
|
+
`material.zod.*` overrides apply. Without `translate`, every issue is
|
|
396
|
+
worded by the package's own messages.
|
|
397
|
+
- **Another code.** `codeOf` picks the code an issue carries. Its default is
|
|
398
|
+
`defaultCodeOf` (a custom issue's `params.code`); to word every empty
|
|
399
|
+
required field by one code of yours, use `isEmptyRequired` (both, and
|
|
400
|
+
the type `CodeIssue`, are exported beside `useCodeResolver` from
|
|
401
|
+
`@nxgt/material/components`, and from `@nxgt/material-core/code-errors`):
|
|
402
|
+
`codeOf: (issue) => (isEmptyRequired(issue) ? 'errors.required' : defaultCodeOf(issue))`.
|
|
403
|
+
- **Stable options.** Pass `translate` and `codeOf` from module scope: a new
|
|
404
|
+
function each render makes a new resolver.
|
|
405
|
+
- **After a language switch** an error already on screen keeps its words
|
|
406
|
+
until the field is validated again: call `trigger()` when the language
|
|
407
|
+
changes and the form was submitted (see
|
|
408
|
+
[troubleshooting](../troubleshooting.md#errors-keep-their-old-words-after-a-language-switch)).
|
|
409
|
+
- Outside a hook, `createCodeErrorMap({ translate, codeOf?, fallback })` (also
|
|
410
|
+
exported beside `useCodeResolver`) is the error map itself, for
|
|
411
|
+
`schema.safeParse(values, { error })`.
|
|
412
|
+
|
|
413
|
+
Storybook: `Guides/Forms/useCodeResolver`. Vue: the same name in
|
|
414
|
+
`@nxgt/material-vue/components`, returning a `validationSchema` for vee-validate.
|
|
415
|
+
|
|
305
416
|
## What each field emits
|
|
306
417
|
|
|
307
418
|
The value a wrapper writes into the form, read from each field's `onChange`
|
package/docs/guides/helpers.md
CHANGED
|
@@ -15,6 +15,7 @@ differences are listed under each table.
|
|
|
15
15
|
| `cn(...classes)` | `clsx` + `tailwind-merge`: joins conditional classes, last Tailwind utility wins. |
|
|
16
16
|
| `clientOnly(Component)` | Wraps a component so it renders nothing on the server and mounts only on the client. See [SSR](ssr.md). |
|
|
17
17
|
| `getCspNonce(doc?)`, `setCspNonce(nonce)` | The page's Content Security Policy nonce, from `@nxgt/material-core/csp-nonce`: `getCspNonce` reads what `setCspNonce` kept, else the `csp-nonce` meta tag, a `<script nonce>` or `<style nonce>`, else `__webpack_nonce__`; `undefined` on a server, where `setCspNonce` does nothing. See [CSP](csp.md#nonceprovider). |
|
|
18
|
+
| `printOnly(element, name?)`, `PRINT_ATTRIBUTE`, `PRINT_TARGET_ATTRIBUTE` | Opens the print dialog for one element of the page instead of the whole page (the marks are removed once printing is over), from `@nxgt/material-core/print-only`; the `@media print` rules are in the package's `styles.css`. See [printing](printing.md#printing-one-element-of-the-page). |
|
|
18
19
|
| `renderSlot(slot, context)` | Resolves a `ContentSlot`: a `ReactNode` or a `(context) => ReactNode`. |
|
|
19
20
|
| `DATE_UTILS` | `format(iso, 'PP', locale?)` (`'-'` when empty), `parseFromTimestring('HH:mm')`, `parseFromISOString`, `formatDateRangeFilter`. |
|
|
20
21
|
| `formatDateRangeFilter(range)` | Turns a day-picker range into `{ from, to }` ISO strings at start / end of day; `undefined` when empty. |
|
|
@@ -131,6 +132,9 @@ Vue: the Redux helpers are Pinia store factories: `defineLocalStore`
|
|
|
131
132
|
| `useSearchParam(name)` | One query-string value from the router (`react-router`). |
|
|
132
133
|
| `useNavigationHandler(to, options?)`, `useNavigationHandlers(routes)` | Stable click handlers that navigate (`react-router`). |
|
|
133
134
|
| `useAllowedTransitions(status, transitions, configs)` | Status-machine actions (icon, colour, label, dialog copy) allowed from the current status. |
|
|
135
|
+
| `useFocusOnMount<T>(focus = true)` | A ref whose element takes focus once mounted, and again whenever `focus` turns true: the heading of what replaced the content the user acted on. See [below](#usefocusonmount). |
|
|
136
|
+
| `useSubmitOnce(busy?)` | One submit in flight: wraps a submit handler so a double click or a second Enter is dropped until the request ends (the handler's promise, or `busy` back to false). See [below](#usesubmitonce). |
|
|
137
|
+
| `createSubmitGuard()`, `busySettler(guard)` | The framework-free guard behind `useSubmitOnce` (`@nxgt/material-core/submit-guard`): `{ once(run, { settleSync? }), settle(run?), pendingRun() }`, and the function that settles it from a busy flag. |
|
|
134
138
|
|
|
135
139
|
```tsx
|
|
136
140
|
import { useState } from 'react';
|
|
@@ -168,6 +172,83 @@ the component, or with `useCallback`) when the list is long.
|
|
|
168
172
|
|
|
169
173
|
Vue: `@nxgt/material-vue/composables`, same names.
|
|
170
174
|
|
|
175
|
+
### `useFocusOnMount`
|
|
176
|
+
|
|
177
|
+
When what the user acted on is replaced — a wizard's next step, an end state
|
|
178
|
+
such as "Message sent" — the button that had focus is gone, and focus falls
|
|
179
|
+
back to the top of the page. Put the ref on the new content's heading: it
|
|
180
|
+
takes focus once mounted, a screen reader reads it, and Tab goes on from
|
|
181
|
+
there. A heading is not focusable by itself: give it `tabIndex={-1}` (a
|
|
182
|
+
button, a link or a field needs nothing).
|
|
183
|
+
|
|
184
|
+
```tsx
|
|
185
|
+
import { useFocusOnMount } from '@nxgt/material/hooks';
|
|
186
|
+
|
|
187
|
+
export function Sent({ email }: { email: string }) {
|
|
188
|
+
const title = useFocusOnMount<HTMLHeadingElement>();
|
|
189
|
+
return (
|
|
190
|
+
<section aria-labelledby="sent-title">
|
|
191
|
+
<h2 id="sent-title" ref={title} tabIndex={-1}>
|
|
192
|
+
Message sent
|
|
193
|
+
</h2>
|
|
194
|
+
<p>We replied to {email}.</p>
|
|
195
|
+
</section>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
`useFocusOnMount(open)` focuses again whenever `open` turns true, for content
|
|
201
|
+
that stays mounted and is only shown. Storybook: `Guides/Hooks/useFocusOnMount`.
|
|
202
|
+
|
|
203
|
+
### `useSubmitOnce`
|
|
204
|
+
|
|
205
|
+
A disabled or `loading` button only stops clicks once it has rendered, and
|
|
206
|
+
never stops Enter in a field: a fast double click, or Enter twice, posts the
|
|
207
|
+
form twice — and a second request can abort the first mid-call. The hook
|
|
208
|
+
returns a wrapper that lets one call through until the request ends; a
|
|
209
|
+
dropped call's event has its default prevented, so it never falls back to
|
|
210
|
+
the browser's own submission.
|
|
211
|
+
|
|
212
|
+
```tsx
|
|
213
|
+
import { useForm } from 'react-hook-form';
|
|
214
|
+
import { Button, TextFormField } from '@nxgt/material/components';
|
|
215
|
+
import { useSubmitOnce } from '@nxgt/material/hooks';
|
|
216
|
+
|
|
217
|
+
type Values = { email: string };
|
|
218
|
+
|
|
219
|
+
export function NewsletterForm({ save }: { save: (values: Values) => Promise<void> }) {
|
|
220
|
+
const { control, handleSubmit, formState } = useForm<Values>({ defaultValues: { email: '' } });
|
|
221
|
+
const submitOnce = useSubmitOnce();
|
|
222
|
+
return (
|
|
223
|
+
// handleSubmit returns a promise: the guard opens again when `save` ends.
|
|
224
|
+
<form onSubmit={submitOnce(handleSubmit(save))}>
|
|
225
|
+
<TextFormField control={control} name="email" label="Email" />
|
|
226
|
+
<Button type="submit" loading={formState.isSubmitting}>
|
|
227
|
+
Subscribe
|
|
228
|
+
</Button>
|
|
229
|
+
</form>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
**Return the request's promise, or pass `busy`.** The guard opens again:
|
|
235
|
+
|
|
236
|
+
- when the handler's promise ends, fulfilled or rejected (a rejection still
|
|
237
|
+
reaches the caller);
|
|
238
|
+
- with `busy` passed (a router's form submission, followed by its navigation
|
|
239
|
+
state: `useSubmitOnce(navigation.state !== 'idle')`), once it was true and
|
|
240
|
+
is false again. It settles the submit that was on its way when `busy`
|
|
241
|
+
rose, never a newer one, and a handler that returns no promise waits for
|
|
242
|
+
it;
|
|
243
|
+
- without `busy`, as soon as a handler returns something that is not a
|
|
244
|
+
promise: one that returned early (`if (!valid) return;`) or did its work
|
|
245
|
+
synchronously does not lock the form.
|
|
246
|
+
|
|
247
|
+
Outside React, `createSubmitGuard()` from `@nxgt/material-core/submit-guard`
|
|
248
|
+
(also re-exported here) is the same guard: `once(run)` wraps a handler, `settle()` opens it by hand, and
|
|
249
|
+
`busySettler(guard)` returns the function to call with each value of a busy
|
|
250
|
+
flag. Storybook: `Guides/Hooks/useSubmitOnce`.
|
|
251
|
+
|
|
171
252
|
## `@nxgt/material/i18n`
|
|
172
253
|
|
|
173
254
|
| Export | What it does |
|
package/docs/guides/i18n.md
CHANGED
|
@@ -299,6 +299,19 @@ fichier.") when it cannot show it, `material.file-preview.download`
|
|
|
299
299
|
"Seuls les premiers {size} de ce fichier sont affichés.") under a text cut
|
|
300
300
|
by the size cap; the text or Markdown it shows scrolls in a region named by
|
|
301
301
|
`material.file-preview.content` ("Contents of {name}", "Contenu de {name}").
|
|
302
|
+
`EnrolmentSecret` reads `material.enrolment-secret.qr-label` ("QR code to
|
|
303
|
+
scan with your authenticator app"), `open-app` ("Open in your authenticator
|
|
304
|
+
app", "Ouvrir dans votre application d'authentification"), `manual` ("Or
|
|
305
|
+
type this key in the app:"), `secret-label` ("Setup key", "Clé de
|
|
306
|
+
configuration"), `copy` ("Copy the key") and `copied` ("Key copied", "Clé
|
|
307
|
+
copiée"); each has a prop that overrides it. `RecoveryCodes` reads
|
|
308
|
+
`material.recovery-codes.title` and `list` ("Recovery codes", "Codes de
|
|
309
|
+
récupération"), `warning` ("Each code works once. Keep them somewhere safe:
|
|
310
|
+
they are shown only now."), `copy`, `copied` and `copy-failed` ("Copy codes",
|
|
311
|
+
"Codes copied", "Copy failed"), `download`, `print`, `saved` ("I have saved
|
|
312
|
+
these codes", "J'ai enregistré ces codes"), `saved-required` ("Tick the box
|
|
313
|
+
once your codes are safe") and `done`; `title`, `warning` and `labels`
|
|
314
|
+
override them. `AuthCard` has no copy of its own.
|
|
302
315
|
`material.carousel.roledescription` /
|
|
303
316
|
`slide` and `material.attributes.buttons.add-option` are there for
|
|
304
317
|
`@nxgt/material-vue`'s carousel and attribute options, so the bundles hold
|
|
@@ -407,6 +420,10 @@ z.config({
|
|
|
407
420
|
|
|
408
421
|
Examples and per-field overrides are in [forms](forms.md#validation-messages).
|
|
409
422
|
|
|
423
|
+
Your own rules' messages — a refinement carrying an error code in
|
|
424
|
+
`params.code` — are worded by your translator, in the provider's language,
|
|
425
|
+
with `useCodeResolver`: see [forms](forms.md#wording-errors-by-code).
|
|
426
|
+
|
|
410
427
|
## Server rendering
|
|
411
428
|
|
|
412
429
|
On the server there is no `localStorage`, so without help the server renders
|
package/docs/guides/printing.md
CHANGED
|
@@ -1397,3 +1397,30 @@ Under pnpm the glob is
|
|
|
1397
1397
|
`pdfkitStandardFonts()` is a Vite plugin and does not apply to Next.js.
|
|
1398
1398
|
|
|
1399
1399
|
More on what a server bundle evaluates in [SSR](ssr.md).
|
|
1400
|
+
|
|
1401
|
+
## Printing one element of the page
|
|
1402
|
+
|
|
1403
|
+
`RecoveryCodes`' "Print" button prints its card and nothing else, with
|
|
1404
|
+
`window.print()`, no inline style and no new window. The same helper prints
|
|
1405
|
+
any element of yours: `printOnly(element, name?)` from
|
|
1406
|
+
`@nxgt/material/lib` marks `<html data-nxgt-print="<name>">` and
|
|
1407
|
+
the element (`data-nxgt-print-target`), opens the print dialog, and removes
|
|
1408
|
+
both once printing is over (on `afterprint`, or as soon as `print()` returns
|
|
1409
|
+
where it blocks). The rules that hide the rest of the page are in the
|
|
1410
|
+
package's `styles.css` (`print-only.css`), under `@media print`: everything but the element,
|
|
1411
|
+
what holds it and what it holds is taken out of the flow, and the element
|
|
1412
|
+
starts at the top left of the first sheet.
|
|
1413
|
+
|
|
1414
|
+
```tsx
|
|
1415
|
+
import { printOnly } from '@nxgt/material/lib';
|
|
1416
|
+
|
|
1417
|
+
const stop = printOnly(document.getElementById('ticket') as HTMLElement, 'ticket');
|
|
1418
|
+
// When the element unmounts before the dialog closed (a browser whose
|
|
1419
|
+
// print() returns at once and never fires afterprint):
|
|
1420
|
+
stop();
|
|
1421
|
+
```
|
|
1422
|
+
|
|
1423
|
+
Hide the controls inside the element with Tailwind's `print:hidden`. If the
|
|
1424
|
+
rest of the page still prints, an `!important` print rule of the app's wins
|
|
1425
|
+
over the package's: see
|
|
1426
|
+
[troubleshooting](../troubleshooting.md#recoverycodes-print-button-prints-the-whole-page).
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/docs/roadmap.md
CHANGED
|
@@ -157,6 +157,21 @@ version they shipped in, and planned entries may name the major they target.
|
|
|
157
157
|
|
|
158
158
|
## Shipped
|
|
159
159
|
|
|
160
|
+
- **Signed-out pages and two-step verification** — `AuthCard`, the frame of a
|
|
161
|
+
sign-in page (one `main`, the card's title its `h1`, a status or alert
|
|
162
|
+
notice, safe-area padding); `EnrolmentSecret`, a TOTP enrolment's QR code,
|
|
163
|
+
app link and key grouped by four; `RecoveryCodes`, one-time codes to copy,
|
|
164
|
+
download or print alone, with an "I have saved these codes" confirmation
|
|
165
|
+
(`saved` / `onSavedChange`) an app's router guard can follow. 2.17.0.
|
|
166
|
+
- **Focus after a change, one submit at a time, errors worded by code** —
|
|
167
|
+
`useFocusOnMount` focuses the heading of what replaced the content the user
|
|
168
|
+
acted on; `useSubmitOnce` drops a double click or a second Enter until the
|
|
169
|
+
request ends; `useCodeResolver` words a zod schema's errors in the page's
|
|
170
|
+
language, a refinement's `params.code` in the app's own words. 2.17.0.
|
|
171
|
+
- **Long forms in titled sections** — `FormSection`: a heading (an `h2`, or
|
|
172
|
+
the level `titleAs` names), a description and actions, its fields in the form
|
|
173
|
+
grid's columns and one column on a phone, a rule between sections, and a
|
|
174
|
+
group a screen reader announces by its title. 2.17.0.
|
|
160
175
|
- **A strict Content Security Policy** — a page that sends `script-src` with a
|
|
161
176
|
nonce and no `'unsafe-eval'`, and `style-src-elem` with a nonce and no
|
|
162
177
|
`'unsafe-inline'`, runs every component with no violation report:
|
package/docs/troubleshooting.md
CHANGED
|
@@ -62,6 +62,7 @@ words in your error.
|
|
|
62
62
|
|
|
63
63
|
**Printing**
|
|
64
64
|
|
|
65
|
+
- [`RecoveryCodes`' Print button prints the whole page](#recoverycodes-print-button-prints-the-whole-page)
|
|
65
66
|
- [`Cannot find module '#standard-fonts/Helvetica'`](#cannot-find-module-standard-fontshelvetica)
|
|
66
67
|
- [`still references '#standard-fonts/' after the transform`](#still-references-standard-fonts-after-the-transform)
|
|
67
68
|
- [`TypeError: (0 , t.createContext) is not a function` in a Next.js route handler](#typeerror-0--tcreatecontext-is-not-a-function-in-a-nextjs-route-handler)
|
|
@@ -105,6 +106,8 @@ words in your error.
|
|
|
105
106
|
- [A test asserting the old Zod string length text fails](#a-test-asserting-the-old-zod-string-length-text-fails)
|
|
106
107
|
- [A calendar's weekday row is in English on a French page](#a-calendars-weekday-row-is-in-english-on-a-french-page)
|
|
107
108
|
- [An accessibility audit reports low contrast on a calendar's weekday row or its days](#an-accessibility-audit-reports-low-contrast-on-a-calendars-weekday-row-or-its-days)
|
|
109
|
+
- [Errors keep their old words after a language switch](#errors-keep-their-old-words-after-a-language-switch)
|
|
110
|
+
- [`useCodeResolver` re-validates or resets the form on every render](#usecoderesolver-re-validates-or-resets-the-form-on-every-render)
|
|
108
111
|
|
|
109
112
|
**Runtime**
|
|
110
113
|
|
|
@@ -1105,6 +1108,29 @@ dependencies: `--external @nxgt/material --external @nxgt/material-core`.
|
|
|
1105
1108
|
|
|
1106
1109
|
## Printing
|
|
1107
1110
|
|
|
1111
|
+
### `RecoveryCodes`' Print button prints the whole page
|
|
1112
|
+
|
|
1113
|
+
**When:** "Print" on `RecoveryCodes` prints the rest of the page with the
|
|
1114
|
+
codes, or the codes away from the top of the sheet.
|
|
1115
|
+
**Why:** the button only marks the page (`<html data-nxgt-print>`) and the
|
|
1116
|
+
card (`data-nxgt-print-target`); hiding the rest is a `@media print` rule in
|
|
1117
|
+
the package's stylesheet (`print-only.css`). A page that does not load
|
|
1118
|
+
`@nxgt/material/styles.css` (or `dist/material.css`) has no such rule, and an app's own
|
|
1119
|
+
print rule that is `!important` (or a `position` on an ancestor set with
|
|
1120
|
+
`!important`) wins over it.
|
|
1121
|
+
**Fix:** load the stylesheet the setup guide names, and keep your print
|
|
1122
|
+
rules off the marked page:
|
|
1123
|
+
|
|
1124
|
+
```css
|
|
1125
|
+
/* your app's print rules, everywhere but while one element is printed */
|
|
1126
|
+
@media print {
|
|
1127
|
+
html:not([data-nxgt-print]) .page-header { position: static !important; }
|
|
1128
|
+
}
|
|
1129
|
+
```
|
|
1130
|
+
|
|
1131
|
+
To print an element of your own the same way, call `printOnly(element)`
|
|
1132
|
+
from `@nxgt/material/lib`; the same rules apply to it.
|
|
1133
|
+
|
|
1108
1134
|
### `Cannot find module '#standard-fonts/Helvetica'`
|
|
1109
1135
|
|
|
1110
1136
|
**When:** a built SSR server answers nothing on every route, `/health`
|
|
@@ -1839,6 +1865,52 @@ react-day-picker hides the weekday row from assistive tech
|
|
|
1839
1865
|
(`<thead aria-hidden>`); in a test, find a day by its full date instead
|
|
1840
1866
|
(`getByRole('gridcell', { name: 'jeudi 1 octobre 2026' })`).
|
|
1841
1867
|
|
|
1868
|
+
### Errors keep their old words after a language switch
|
|
1869
|
+
|
|
1870
|
+
**When:** a form validated with `useCodeResolver` shows an error, the user
|
|
1871
|
+
switches the language, and the error stays in the old one (a code's words
|
|
1872
|
+
or a zod message alike).
|
|
1873
|
+
**Why:** the resolver follows the new language, but react-hook-form keeps
|
|
1874
|
+
the message it computed until the field is validated again.
|
|
1875
|
+
**Fix:** validate again when the language changes, once the form was
|
|
1876
|
+
submitted:
|
|
1877
|
+
|
|
1878
|
+
```tsx
|
|
1879
|
+
const resolver = useCodeResolver(schema, { translate });
|
|
1880
|
+
const { trigger, formState } = useForm({ resolver });
|
|
1881
|
+
const { isSubmitted } = formState;
|
|
1882
|
+
useEffect(() => {
|
|
1883
|
+
if (isSubmitted) trigger();
|
|
1884
|
+
}, [resolver, isSubmitted, trigger]); // a new resolver is a new language
|
|
1885
|
+
```
|
|
1886
|
+
|
|
1887
|
+
Storybook: `Guides/Forms/useCodeResolver`. See
|
|
1888
|
+
[forms](guides/forms.md#wording-errors-by-code).
|
|
1889
|
+
|
|
1890
|
+
### `useCodeResolver` re-validates or resets the form on every render
|
|
1891
|
+
|
|
1892
|
+
**When:** `translate` or `codeOf` is written inline in the component
|
|
1893
|
+
(`useCodeResolver(schema, { codeOf: (issue) => … })`), or the schema is built
|
|
1894
|
+
in the component body.
|
|
1895
|
+
**Why:** the resolver is memoised on the schema, the language, `translate`
|
|
1896
|
+
and `codeOf`: a new function or schema each render makes a new resolver
|
|
1897
|
+
each render.
|
|
1898
|
+
**Fix:** define the schema, the translator and `codeOf` at module scope (or
|
|
1899
|
+
with `useMemo` / `useCallback` when they depend on props):
|
|
1900
|
+
|
|
1901
|
+
```tsx
|
|
1902
|
+
import { type CodeIssue, defaultCodeOf, isEmptyRequired, useCodeResolver } from '@nxgt/material/components';
|
|
1903
|
+
|
|
1904
|
+
const schema = z.object({ username: z.string().min(3) });
|
|
1905
|
+
const codeOf = (issue: CodeIssue) =>
|
|
1906
|
+
isEmptyRequired(issue) ? 'errors.required' : defaultCodeOf(issue);
|
|
1907
|
+
|
|
1908
|
+
export function SignUpForm() {
|
|
1909
|
+
const resolver = useCodeResolver(schema, { translate, codeOf });
|
|
1910
|
+
// …
|
|
1911
|
+
}
|
|
1912
|
+
```
|
|
1913
|
+
|
|
1842
1914
|
## Runtime
|
|
1843
1915
|
|
|
1844
1916
|
### A test asserting the old Zod string length text fails
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
@import "./ui/layout/app-bar/bottom-app-bar.css";
|
|
30
30
|
@import "./ui/primitives/label/label.css";
|
|
31
31
|
@import "./ui/feedback/toast/toast.css";
|
|
32
|
+
@import "./print-only.css";
|
|
32
33
|
@import "./ui/fields/contacts/contact-item.css";
|
|
33
34
|
@import "./ui/fields/opening-hours/opening-hours-item.css";
|
|
34
35
|
@import "./ui/fields/attributes/attribute-form/attribute-form.css";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/* Printing one element of the page (`printOnly`, from `/lib` or
|
|
2
|
+
* @nxgt/material-core/print-only; RecoveryCodes' Print button): while
|
|
3
|
+
* <html data-nxgt-print> is set, print media shows the element marked
|
|
4
|
+
* data-nxgt-print-target and nothing else. No inline style: only these rules,
|
|
5
|
+
* the same in @nxgt/material and @nxgt/material-vue.
|
|
6
|
+
*
|
|
7
|
+
* Everything that neither is, holds nor sits inside the target is taken out
|
|
8
|
+
* of the flow, so the page prints no blank sheets after it; where :has() is
|
|
9
|
+
* not supported, that rule is dropped and `visibility` alone hides the rest.
|
|
10
|
+
* The target's ancestors stop positioning it, so it starts at the top left of
|
|
11
|
+
* the first page. Unlayered, these rules beat the components' Tailwind
|
|
12
|
+
* utilities (layered) with no `!important`. */
|
|
13
|
+
@media print {
|
|
14
|
+
html[data-nxgt-print]
|
|
15
|
+
body
|
|
16
|
+
*:not(
|
|
17
|
+
[data-nxgt-print-target],
|
|
18
|
+
[data-nxgt-print-target] *,
|
|
19
|
+
:has([data-nxgt-print-target])
|
|
20
|
+
) {
|
|
21
|
+
display: none;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
html[data-nxgt-print] body * {
|
|
25
|
+
visibility: hidden;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
html[data-nxgt-print] :has([data-nxgt-print-target]) {
|
|
29
|
+
position: static;
|
|
30
|
+
transform: none;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
html[data-nxgt-print] [data-nxgt-print-target],
|
|
34
|
+
html[data-nxgt-print] [data-nxgt-print-target] * {
|
|
35
|
+
visibility: visible;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
html[data-nxgt-print] [data-nxgt-print-target] {
|
|
39
|
+
position: absolute;
|
|
40
|
+
inset: 0 0 auto 0;
|
|
41
|
+
margin: 0;
|
|
42
|
+
box-shadow: none;
|
|
43
|
+
}
|
|
44
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nxgt/material",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.17.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"bugs": {
|
|
6
6
|
"email": "stevegires@gmail.com"
|
|
@@ -119,7 +119,7 @@
|
|
|
119
119
|
"docs:previews": "vitest run --project previews"
|
|
120
120
|
},
|
|
121
121
|
"dependencies": {
|
|
122
|
-
"@nxgt/material-core": "^0.
|
|
122
|
+
"@nxgt/material-core": "^0.10.0",
|
|
123
123
|
"@base-ui/react": "^1.6.0",
|
|
124
124
|
"@dnd-kit/core": "^6.3.1",
|
|
125
125
|
"@dnd-kit/sortable": "^10.0.0",
|