@connextar/house 0.3.0 → 0.4.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/README.md +133 -3
- package/dist/wizard/index.d.ts +26 -0
- package/dist/wizard/index.d.ts.map +1 -0
- package/dist/wizard/index.js +26 -0
- package/dist/wizard/index.js.map +1 -0
- package/dist/wizard/invalid.d.ts +91 -0
- package/dist/wizard/invalid.d.ts.map +1 -0
- package/dist/wizard/invalid.js +121 -0
- package/dist/wizard/invalid.js.map +1 -0
- package/dist/wizard/react-hook-form-index.d.ts +12 -0
- package/dist/wizard/react-hook-form-index.d.ts.map +1 -0
- package/dist/wizard/react-hook-form-index.js +12 -0
- package/dist/wizard/react-hook-form-index.js.map +1 -0
- package/dist/wizard/rhf-adapter.d.ts +32 -0
- package/dist/wizard/rhf-adapter.d.ts.map +1 -0
- package/dist/wizard/rhf-adapter.js +58 -0
- package/dist/wizard/rhf-adapter.js.map +1 -0
- package/dist/wizard/rules.d.ts +92 -0
- package/dist/wizard/rules.d.ts.map +1 -0
- package/dist/wizard/rules.js +105 -0
- package/dist/wizard/rules.js.map +1 -0
- package/dist/wizard/wizard.d.ts +127 -0
- package/dist/wizard/wizard.d.ts.map +1 -0
- package/dist/wizard/wizard.js +195 -0
- package/dist/wizard/wizard.js.map +1 -0
- package/package.json +15 -2
package/README.md
CHANGED
|
@@ -47,6 +47,19 @@ it. It is also the kind of code nobody should write twice: the rules about when
|
|
|
47
47
|
to let go of an optimistic change are small, subtle and easy to get quietly
|
|
48
48
|
wrong.
|
|
49
49
|
|
|
50
|
+
`wizard`, in 0.4.0, is the first module extracted from five copies that had
|
|
51
|
+
already been written. It is here because of what those copies cost: four of the
|
|
52
|
+
five shipped a defect in it, and not one of the four was a mistake about
|
|
53
|
+
wizards. PharmaLine created a booking from step two, because a `type="button"`
|
|
54
|
+
Continue and a `type="submit"` Create shared a slot and React switched the one
|
|
55
|
+
DOM node's type mid-click. Three PM Tool forms refused to save with nothing on
|
|
56
|
+
screen saying why. MAS checked no step at all on submit, so a second press after
|
|
57
|
+
a successful create sent a record whose first step had been cleared. Every one of
|
|
58
|
+
those is a mistake about what a form must do before it agrees to send, and there
|
|
59
|
+
is one right answer to that — which is exactly the shape of thing that belongs
|
|
60
|
+
here and nowhere else. The module is the union of the five, not a copy of the
|
|
61
|
+
best one.
|
|
62
|
+
|
|
50
63
|
## Modules
|
|
51
64
|
|
|
52
65
|
### `@connextar/house/errors`
|
|
@@ -305,6 +318,94 @@ learned the hard way:
|
|
|
305
318
|
exports: anything whose result the person cannot predict, or would be alarmed to
|
|
306
319
|
see undone. Guessing is only honest when the guess is nearly always right.
|
|
307
320
|
|
|
321
|
+
### `@connextar/house/wizard`, `/wizard/react-hook-form`
|
|
322
|
+
|
|
323
|
+
`FormWizard` — one long form split into steps that check themselves. **It renders
|
|
324
|
+
the `<form>`**, because two of its promises cannot be kept from inside somebody
|
|
325
|
+
else's: that Enter has one place to land, and that nothing reaches `onSubmit`
|
|
326
|
+
unchecked.
|
|
327
|
+
|
|
328
|
+
```tsx
|
|
329
|
+
const check = useStepValidator(form); // react-hook-form; or write your own
|
|
330
|
+
|
|
331
|
+
<FormWizard
|
|
332
|
+
steps={[
|
|
333
|
+
{ id: "learner", title: "Learner", fields: ["studentId", "level"], content: <LearnerFields /> },
|
|
334
|
+
{ id: "period", title: "Period", fields: ["startDate", "endDate"], content: <PeriodFields /> },
|
|
335
|
+
{ id: "build", title: "Build", fields: [], content: <ReviewFields /> },
|
|
336
|
+
]}
|
|
337
|
+
onValidateStep={check}
|
|
338
|
+
onSubmit={() => void form.handleSubmit(submit, report)()}
|
|
339
|
+
onCancel={() => router.push(routes.plans)}
|
|
340
|
+
submitLabel="Build my curriculum"
|
|
341
|
+
pending={pending}
|
|
342
|
+
/>;
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
The rules it keeps, each one because a copy of it did not:
|
|
346
|
+
|
|
347
|
+
- **Submitting checks every step, in order, and lands on the first that fails**,
|
|
348
|
+
with its message visible. Checking only the last step lets a marker jump back,
|
|
349
|
+
a cleared field and a press of Save refuse with the problem three screens away.
|
|
350
|
+
Checking none — MAS — sends the record anyway.
|
|
351
|
+
- **Enter before the last step advances.** It validates the step it is on and
|
|
352
|
+
opens the next; only the last step's Enter submits. A booking that emails a
|
|
353
|
+
patient must not be creatable from the step before Confirm.
|
|
354
|
+
- **One submit button, whose `type` never changes.** Never two controls sharing
|
|
355
|
+
the slot. jsdom's timing hid this, and only Chromium showed it, so the test
|
|
356
|
+
that guards it is structural: one `button[type=submit]`, the same node on every
|
|
357
|
+
step.
|
|
358
|
+
- **Continue is never disabled for being incomplete.** A greyed-out button that
|
|
359
|
+
will not say what is missing is a refusal with no words. It is disabled only
|
|
360
|
+
while something is in flight.
|
|
361
|
+
- **Visited markers are reachable, later steps locked** (`allReachable` opens
|
|
362
|
+
them all, for editing values that are already there).
|
|
363
|
+
- **Every step stays mounted, hidden rather than unmounted**, so there is one
|
|
364
|
+
field register and nothing typed is lost stepping back.
|
|
365
|
+
|
|
366
|
+
**Form-library-agnostic, because the five hosts are not the same.** A step is
|
|
367
|
+
complete when the check says so, and a check may answer either way the cohort
|
|
368
|
+
already writes them: `true`/`null` for complete, `false` for "not complete, and I
|
|
369
|
+
am showing why beside the field", or **a sentence** for "not complete, and this is
|
|
370
|
+
what to say" — which the wizard then shows in a `role="alert"` of its own. So
|
|
371
|
+
react-hook-form, plain `useState` and MAS's state forms all host it unchanged. A
|
|
372
|
+
check may live on the form (`onValidateStep`) or on the step (`step.validate`);
|
|
373
|
+
both are asked, the step's first.
|
|
374
|
+
|
|
375
|
+
`stepValidator(form, report)` and `useStepValidator(form)` are in
|
|
376
|
+
`@connextar/house/wizard/react-hook-form` — its own entry point because
|
|
377
|
+
`react-hook-form` is an **optional** peer dependency. Every import of it there is
|
|
378
|
+
`import type`, so the emitted JavaScript never mentions it; what would break an
|
|
379
|
+
app without it is a declaration file naming a package it has not installed, which
|
|
380
|
+
is why the names sit behind a subpath MAS never reaches for.
|
|
381
|
+
`src/wizard/entry-points.test.ts` keeps that honest against the built output.
|
|
382
|
+
|
|
383
|
+
**When the error is somewhere nobody can look.** A wizard's fields live on steps
|
|
384
|
+
that are mounted but hidden, and a form library will not call a submit handler
|
|
385
|
+
while any field is invalid — so a message beside a field the person cannot see
|
|
386
|
+
makes the button look broken. `useInvalidReporter()` returns the function to pass
|
|
387
|
+
as the invalid handler (`form.handleSubmit(onSubmit, report)`); it waits for the
|
|
388
|
+
form to re-render and the wizard to move, then says the message out loud **only
|
|
389
|
+
if it is not already on screen**. It goes to the `ActionFeedbackProvider` from
|
|
390
|
+
[`ux`](#connextarhouseux), so this module imports no toast library. A field's
|
|
391
|
+
message opts in by carrying `data-error-for="<name>"` — `FieldError` does it, and
|
|
392
|
+
an app's own message component adds one attribute.
|
|
393
|
+
|
|
394
|
+
The look is theme-token classes, replaced part by part through `classNames`, the
|
|
395
|
+
same way the guide renderer allows — there is still no `Button` in this package,
|
|
396
|
+
so the wizard draws plain elements and wears the app's classes. The busy state is
|
|
397
|
+
`data-busy`, which [`house.css`](#styles--connextarhousehousecss) already turns
|
|
398
|
+
into a spinner.
|
|
399
|
+
|
|
400
|
+
**What not to make a wizard.** A form of four fields is shorter than a wizard
|
|
401
|
+
around it, and a stepper over one screen of content is decoration that costs a
|
|
402
|
+
click. This is for a form long enough that a person would otherwise lose their
|
|
403
|
+
place — five steps in the curriculum builder, a booking that has to reach Confirm
|
|
404
|
+
— and specifically for one where **later steps depend on earlier answers**. Three
|
|
405
|
+
independent sections are a form with three headings. A flow whose steps are
|
|
406
|
+
separate saves, each landing before the next opens, is not one form and must not
|
|
407
|
+
pretend to be: this wizard keeps one register and submits once.
|
|
408
|
+
|
|
308
409
|
### Styles — `@connextar/house/house.css`
|
|
309
410
|
|
|
310
411
|
What the components need that utility classes cannot say: rich text, the editor's
|
|
@@ -407,11 +508,40 @@ Traps found adopting it the first time:
|
|
|
407
508
|
module"; delete `.next/dev` when no dev server is running.
|
|
408
509
|
- If the app also depends on `@tiptap/*` directly, keep the range aligned or drop
|
|
409
510
|
the direct dependency — two copies of `@tiptap/pm` break the editor.
|
|
511
|
+
- **`FormWizard` renders the `<form>`, so the app's wrapper one has to go.** Every
|
|
512
|
+
local copy but two sat inside a host `<form onSubmit={preventDefault}>`; nested
|
|
513
|
+
forms are invalid HTML and the outer one is where the Enter bug lives. Keep the
|
|
514
|
+
app's `<Form {...form}>` provider if it has one — that is context, not an
|
|
515
|
+
element — and delete the `<form>` inside it. MAS also loses its
|
|
516
|
+
`finishControl`: the submit button is the wizard's now, and passing one back in
|
|
517
|
+
is the defect the wizard exists to prevent.
|
|
410
518
|
- **Jest** (with `next/jest`) does not transform `node_modules`, and this package is
|
|
411
519
|
ESM, so the first import fails with `Unexpected token 'export'`. `next/jest` only
|
|
412
520
|
appends to `transformIgnorePatterns`, so export an async config that rewrites each
|
|
413
|
-
`/node_modules/(?!` rule to `/node_modules/(?!@connextar/house/)(?!`.
|
|
414
|
-
|
|
521
|
+
`/node_modules/(?!` rule to `/node_modules/(?!@connextar/house/)(?!`.
|
|
522
|
+
- **Vitest needs one line to mock anything the package imports.** It loads
|
|
523
|
+
`node_modules` through native Node ESM, where `vi.mock` does not reach: a test
|
|
524
|
+
that mocks `next/navigation` keeps passing while the package's components call
|
|
525
|
+
the real router, until one of them throws `invariant expected app router to be
|
|
526
|
+
mounted`. Inline the package so vite transforms it:
|
|
527
|
+
|
|
528
|
+
```ts
|
|
529
|
+
test: {
|
|
530
|
+
server: {
|
|
531
|
+
deps: {
|
|
532
|
+
inline: [/@connextar\/house/];
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
- **Mock `next/navigation.js`, not just `next/navigation`.** This package is built
|
|
539
|
+
with `moduleResolution: nodenext`, and `next` ships no `exports` map — so an
|
|
540
|
+
extensionless subpath does not resolve and the source has to say
|
|
541
|
+
`next/navigation.js`. Vitest keys mocks by specifier, so an app's existing mock
|
|
542
|
+
of `next/navigation` does not cover the package. Register both, with the factory
|
|
543
|
+
written out twice: `vi.mock` is hoisted above every `const`, so a shared factory
|
|
544
|
+
variable throws `Cannot access ... before initialization`.
|
|
415
545
|
|
|
416
546
|
**A note on local development.** Installing this with `npm install ../path` makes
|
|
417
547
|
a symlink, and Next's Turbopack will not resolve a package whose real path is
|
|
@@ -421,7 +551,7 @@ behaves exactly like a registry install:
|
|
|
421
551
|
|
|
422
552
|
```bash
|
|
423
553
|
npm pack --pack-destination /tmp # in this repo
|
|
424
|
-
npm install /tmp/connextar-house-0.
|
|
554
|
+
npm install /tmp/connextar-house-0.4.0.tgz # in the app
|
|
425
555
|
```
|
|
426
556
|
|
|
427
557
|
## Adding to it
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@connextar/house/wizard` — one long form, split into steps that check
|
|
3
|
+
* themselves.
|
|
4
|
+
*
|
|
5
|
+
* Five apps wrote this and four of them shipped a defect in it: a booking
|
|
6
|
+
* created from step two because two buttons shared a slot and React switched the
|
|
7
|
+
* type mid-click; three forms that refused to save with nothing on screen saying
|
|
8
|
+
* why; a stepper that checked no step at all on submit and sent a record whose
|
|
9
|
+
* first step had been cleared. None of those are mistakes about wizards. They are
|
|
10
|
+
* mistakes about what a form must do before it agrees to send, and there is one
|
|
11
|
+
* right answer to that, which is why this is here rather than in five places.
|
|
12
|
+
*
|
|
13
|
+
* Form-library-agnostic on purpose: the host supplies the check
|
|
14
|
+
* (`onValidateStep`, or a `validate` on the step), so react-hook-form, plain
|
|
15
|
+
* state and a form held in `useState` can all host it. The react-hook-form
|
|
16
|
+
* adapter is `@connextar/house/wizard/react-hook-form`, kept apart so that
|
|
17
|
+
* `react-hook-form` stays the optional peer dependency it is declared to be.
|
|
18
|
+
*
|
|
19
|
+
* What reaches the person, when a refusal has nowhere to show itself, goes
|
|
20
|
+
* through the `ActionFeedbackProvider` from `@connextar/house/ux` — so this
|
|
21
|
+
* module imports no toast library and no design system.
|
|
22
|
+
*/
|
|
23
|
+
export { ERROR_ATTRIBUTE, fieldProblems, INVALID_MESSAGES, isOnScreen, REPORT_DELAY, reportInvalid, shownErrorFields, unseenProblems, useInvalidReporter, type FieldProblem, type InvalidMessages, type InvalidReporter, } from "./invalid.js";
|
|
24
|
+
export { checkStep, firstFailure, isReachable, markerState, nextIndex, percentComplete, previousIndex, stepResult, type MarkerState, type StepCheck, type StepCheckable, type StepFailure, type StepOutcome, type StepResult, } from "./rules.js";
|
|
25
|
+
export { FieldError, FormWizard, ReviewRow, WIZARD_CLASS_NAMES, type FormWizardProps, type WizardClassNames, type WizardStep, } from "./wizard.js";
|
|
26
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/wizard/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,EACL,eAAe,EACf,aAAa,EACb,gBAAgB,EAChB,UAAU,EACV,YAAY,EACZ,aAAa,EACb,gBAAgB,EAChB,cAAc,EACd,kBAAkB,EAClB,KAAK,YAAY,EACjB,KAAK,eAAe,EACpB,KAAK,eAAe,GACrB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,SAAS,EACT,YAAY,EACZ,WAAW,EACX,WAAW,EACX,SAAS,EACT,eAAe,EACf,aAAa,EACb,UAAU,EACV,KAAK,WAAW,EAChB,KAAK,SAAS,EACd,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,KAAK,UAAU,GAChB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,UAAU,EACV,UAAU,EACV,SAAS,EACT,kBAAkB,EAClB,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,UAAU,GAChB,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@connextar/house/wizard` — one long form, split into steps that check
|
|
3
|
+
* themselves.
|
|
4
|
+
*
|
|
5
|
+
* Five apps wrote this and four of them shipped a defect in it: a booking
|
|
6
|
+
* created from step two because two buttons shared a slot and React switched the
|
|
7
|
+
* type mid-click; three forms that refused to save with nothing on screen saying
|
|
8
|
+
* why; a stepper that checked no step at all on submit and sent a record whose
|
|
9
|
+
* first step had been cleared. None of those are mistakes about wizards. They are
|
|
10
|
+
* mistakes about what a form must do before it agrees to send, and there is one
|
|
11
|
+
* right answer to that, which is why this is here rather than in five places.
|
|
12
|
+
*
|
|
13
|
+
* Form-library-agnostic on purpose: the host supplies the check
|
|
14
|
+
* (`onValidateStep`, or a `validate` on the step), so react-hook-form, plain
|
|
15
|
+
* state and a form held in `useState` can all host it. The react-hook-form
|
|
16
|
+
* adapter is `@connextar/house/wizard/react-hook-form`, kept apart so that
|
|
17
|
+
* `react-hook-form` stays the optional peer dependency it is declared to be.
|
|
18
|
+
*
|
|
19
|
+
* What reaches the person, when a refusal has nowhere to show itself, goes
|
|
20
|
+
* through the `ActionFeedbackProvider` from `@connextar/house/ux` — so this
|
|
21
|
+
* module imports no toast library and no design system.
|
|
22
|
+
*/
|
|
23
|
+
export { ERROR_ATTRIBUTE, fieldProblems, INVALID_MESSAGES, isOnScreen, REPORT_DELAY, reportInvalid, shownErrorFields, unseenProblems, useInvalidReporter, } from "./invalid.js";
|
|
24
|
+
export { checkStep, firstFailure, isReachable, markerState, nextIndex, percentComplete, previousIndex, stepResult, } from "./rules.js";
|
|
25
|
+
export { FieldError, FormWizard, ReviewRow, WIZARD_CLASS_NAMES, } from "./wizard.js";
|
|
26
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/wizard/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,EACL,eAAe,EACf,aAAa,EACb,gBAAgB,EAChB,UAAU,EACV,YAAY,EACZ,aAAa,EACb,gBAAgB,EAChB,cAAc,EACd,kBAAkB,GAInB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,SAAS,EACT,YAAY,EACZ,WAAW,EACX,WAAW,EACX,SAAS,EACT,eAAe,EACf,aAAa,EACb,UAAU,GAOX,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,UAAU,EACV,UAAU,EACV,SAAS,EACT,kBAAkB,GAInB,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { type ActionReporter } from "../ux/feedback.js";
|
|
2
|
+
/**
|
|
3
|
+
* Saying why a form refused, when the reason is somewhere nobody can look.
|
|
4
|
+
*
|
|
5
|
+
* A wizard keeps fields on steps that are mounted but hidden, and a form library
|
|
6
|
+
* will not call a submit handler while any field is invalid. An error shown
|
|
7
|
+
* beside its field speaks for itself. One on a field nobody can see — another
|
|
8
|
+
* step, a field the form hides because it does not apply, a rule that belongs to
|
|
9
|
+
* no field at all — makes the button look broken, and that one has to be said
|
|
10
|
+
* out loud. This is the half of the wizard that has to read the DOM, so it is
|
|
11
|
+
* kept apart from the rules.
|
|
12
|
+
*
|
|
13
|
+
* Where it is said is the app's decision, not this package's: the message goes to
|
|
14
|
+
* the `ActionFeedbackProvider` that `@connextar/house/ux` already defines, so an
|
|
15
|
+
* app wires one reporter to whatever it uses to tell people things and this
|
|
16
|
+
* module imports no toast library.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* The attribute a field's error message carries, so a visible one can be told
|
|
20
|
+
* from a hidden one. `FieldError` sets it; an app's own message component sets
|
|
21
|
+
* it too (`<p data-error-for="title">`), which is the whole integration.
|
|
22
|
+
*/
|
|
23
|
+
export declare const ERROR_ATTRIBUTE = "data-error-for";
|
|
24
|
+
/** How long to wait before looking. */
|
|
25
|
+
export declare const REPORT_DELAY = 50;
|
|
26
|
+
export interface InvalidMessages {
|
|
27
|
+
/** The heading. */
|
|
28
|
+
title: string;
|
|
29
|
+
/** Used when no problem carried a message of its own. */
|
|
30
|
+
fallback: string;
|
|
31
|
+
}
|
|
32
|
+
export declare const INVALID_MESSAGES: InvalidMessages;
|
|
33
|
+
export interface FieldProblem {
|
|
34
|
+
name: string;
|
|
35
|
+
message: string | null;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The problems in an error object, whatever shape the form library gave them.
|
|
39
|
+
* Only the entries that are objects count: a form library's error map holds one
|
|
40
|
+
* per field, and anything else in there is not a field error.
|
|
41
|
+
*/
|
|
42
|
+
export declare function fieldProblems(errors: object): FieldProblem[];
|
|
43
|
+
/**
|
|
44
|
+
* Rendered, outside a hidden step, and — where the page has layout at all —
|
|
45
|
+
* taking up space.
|
|
46
|
+
*
|
|
47
|
+
* A DOM without layout, like a test's, has no boxes to measure, so `hidden`
|
|
48
|
+
* decides there on its own. Measuring alone was AltEd's version, and under jsdom
|
|
49
|
+
* it called every message invisible and said everything twice.
|
|
50
|
+
*/
|
|
51
|
+
export declare function isOnScreen(element: HTMLElement): boolean;
|
|
52
|
+
/** The field names whose message is on screen right now. */
|
|
53
|
+
export declare function shownErrorFields(root: ParentNode): string[];
|
|
54
|
+
/**
|
|
55
|
+
* The problems with nothing on screen to show them.
|
|
56
|
+
*
|
|
57
|
+
* A name matches a shown message exactly, or as its prefix: an error on
|
|
58
|
+
* `times` is answered by a message on `times.0.start`, because that is where a
|
|
59
|
+
* form library puts the message for an array field's row.
|
|
60
|
+
*/
|
|
61
|
+
export declare function unseenProblems(problems: readonly FieldProblem[], shown: readonly string[]): FieldProblem[];
|
|
62
|
+
export type InvalidReporter = (errors: object) => void;
|
|
63
|
+
/**
|
|
64
|
+
* Report the errors an invalid form refused on, if any of them has nowhere to
|
|
65
|
+
* show itself.
|
|
66
|
+
*
|
|
67
|
+
* Deferred, because it has to run after the form has re-rendered with its errors
|
|
68
|
+
* and after a wizard has moved to the failing step — before that, every message
|
|
69
|
+
* is still hidden and it would say everything twice. Returns a way to cancel,
|
|
70
|
+
* for a component that unmounts in between.
|
|
71
|
+
*
|
|
72
|
+
* Two silences and one noise:
|
|
73
|
+
*
|
|
74
|
+
* - Problems, all of them showing → quiet. The fields say it better.
|
|
75
|
+
* - Problems, at least one unseen → the first unseen message, or the fallback.
|
|
76
|
+
* - **No problems at all → the fallback**, not quiet. A form that refuses with
|
|
77
|
+
* an empty error map is the worst of the cases, not the mildest: something
|
|
78
|
+
* said no and named nothing, and the person is left pressing a dead button.
|
|
79
|
+
*/
|
|
80
|
+
export declare function reportInvalid(errors: object, report: ActionReporter, options?: {
|
|
81
|
+
delay?: number;
|
|
82
|
+
root?: ParentNode;
|
|
83
|
+
messages?: Partial<InvalidMessages>;
|
|
84
|
+
}): () => void;
|
|
85
|
+
/**
|
|
86
|
+
* `reportInvalid` bound to the app's reporter — pass it straight to a form
|
|
87
|
+
* library's invalid handler (`handleSubmit(onSubmit, report)`), or let
|
|
88
|
+
* `stepValidator` from `@connextar/house/wizard/react-hook-form` call it.
|
|
89
|
+
*/
|
|
90
|
+
export declare function useInvalidReporter(messages?: Partial<InvalidMessages>): InvalidReporter;
|
|
91
|
+
//# sourceMappingURL=invalid.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invalid.d.ts","sourceRoot":"","sources":["../../src/wizard/invalid.ts"],"names":[],"mappings":"AAIA,OAAO,EAAqB,KAAK,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAE3E;;;;;;;;;;;;;;;GAeG;AAEH;;;;GAIG;AACH,eAAO,MAAM,eAAe,mBAAmB,CAAC;AAEhD,uCAAuC;AACvC,eAAO,MAAM,YAAY,KAAK,CAAC;AAE/B,MAAM,WAAW,eAAe;IAC9B,mBAAmB;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,yDAAyD;IACzD,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,eAAO,MAAM,gBAAgB,EAAE,eAG9B,CAAC;AAEF,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,EAAE,CAM5D;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAIxD;AAED,4DAA4D;AAC5D,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,EAAE,CAI3D;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,SAAS,YAAY,EAAE,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,YAAY,EAAE,CAE1G;AAED,MAAM,MAAM,eAAe,GAAG,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;AAEvD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,cAAc,EACtB,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,CAAA;CAAO,GACvF,MAAM,IAAI,CAcZ;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,GAAG,eAAe,CAYvF"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { useActionReporter } from "../ux/feedback.js";
|
|
4
|
+
/**
|
|
5
|
+
* Saying why a form refused, when the reason is somewhere nobody can look.
|
|
6
|
+
*
|
|
7
|
+
* A wizard keeps fields on steps that are mounted but hidden, and a form library
|
|
8
|
+
* will not call a submit handler while any field is invalid. An error shown
|
|
9
|
+
* beside its field speaks for itself. One on a field nobody can see — another
|
|
10
|
+
* step, a field the form hides because it does not apply, a rule that belongs to
|
|
11
|
+
* no field at all — makes the button look broken, and that one has to be said
|
|
12
|
+
* out loud. This is the half of the wizard that has to read the DOM, so it is
|
|
13
|
+
* kept apart from the rules.
|
|
14
|
+
*
|
|
15
|
+
* Where it is said is the app's decision, not this package's: the message goes to
|
|
16
|
+
* the `ActionFeedbackProvider` that `@connextar/house/ux` already defines, so an
|
|
17
|
+
* app wires one reporter to whatever it uses to tell people things and this
|
|
18
|
+
* module imports no toast library.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* The attribute a field's error message carries, so a visible one can be told
|
|
22
|
+
* from a hidden one. `FieldError` sets it; an app's own message component sets
|
|
23
|
+
* it too (`<p data-error-for="title">`), which is the whole integration.
|
|
24
|
+
*/
|
|
25
|
+
export const ERROR_ATTRIBUTE = "data-error-for";
|
|
26
|
+
/** How long to wait before looking. */
|
|
27
|
+
export const REPORT_DELAY = 50;
|
|
28
|
+
export const INVALID_MESSAGES = {
|
|
29
|
+
title: "That can't be saved yet",
|
|
30
|
+
fallback: "Something on the form needs changing — check the highlighted fields.",
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* The problems in an error object, whatever shape the form library gave them.
|
|
34
|
+
* Only the entries that are objects count: a form library's error map holds one
|
|
35
|
+
* per field, and anything else in there is not a field error.
|
|
36
|
+
*/
|
|
37
|
+
export function fieldProblems(errors) {
|
|
38
|
+
return Object.entries(errors).flatMap(([name, value]) => {
|
|
39
|
+
if (typeof value !== "object" || value === null)
|
|
40
|
+
return [];
|
|
41
|
+
const message = value.message;
|
|
42
|
+
return [{ name, message: typeof message === "string" && message !== "" ? message : null }];
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Rendered, outside a hidden step, and — where the page has layout at all —
|
|
47
|
+
* taking up space.
|
|
48
|
+
*
|
|
49
|
+
* A DOM without layout, like a test's, has no boxes to measure, so `hidden`
|
|
50
|
+
* decides there on its own. Measuring alone was AltEd's version, and under jsdom
|
|
51
|
+
* it called every message invisible and said everything twice.
|
|
52
|
+
*/
|
|
53
|
+
export function isOnScreen(element) {
|
|
54
|
+
if (element.closest("[hidden]"))
|
|
55
|
+
return false;
|
|
56
|
+
const hasLayout = element.ownerDocument.documentElement.getClientRects().length > 0;
|
|
57
|
+
return !hasLayout || element.getClientRects().length > 0;
|
|
58
|
+
}
|
|
59
|
+
/** The field names whose message is on screen right now. */
|
|
60
|
+
export function shownErrorFields(root) {
|
|
61
|
+
return [...root.querySelectorAll(`[${ERROR_ATTRIBUTE}]`)]
|
|
62
|
+
.filter(isOnScreen)
|
|
63
|
+
.map((element) => element.getAttribute(ERROR_ATTRIBUTE) ?? "");
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The problems with nothing on screen to show them.
|
|
67
|
+
*
|
|
68
|
+
* A name matches a shown message exactly, or as its prefix: an error on
|
|
69
|
+
* `times` is answered by a message on `times.0.start`, because that is where a
|
|
70
|
+
* form library puts the message for an array field's row.
|
|
71
|
+
*/
|
|
72
|
+
export function unseenProblems(problems, shown) {
|
|
73
|
+
return problems.filter(({ name }) => !shown.some((on) => on === name || on.startsWith(`${name}.`)));
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Report the errors an invalid form refused on, if any of them has nowhere to
|
|
77
|
+
* show itself.
|
|
78
|
+
*
|
|
79
|
+
* Deferred, because it has to run after the form has re-rendered with its errors
|
|
80
|
+
* and after a wizard has moved to the failing step — before that, every message
|
|
81
|
+
* is still hidden and it would say everything twice. Returns a way to cancel,
|
|
82
|
+
* for a component that unmounts in between.
|
|
83
|
+
*
|
|
84
|
+
* Two silences and one noise:
|
|
85
|
+
*
|
|
86
|
+
* - Problems, all of them showing → quiet. The fields say it better.
|
|
87
|
+
* - Problems, at least one unseen → the first unseen message, or the fallback.
|
|
88
|
+
* - **No problems at all → the fallback**, not quiet. A form that refuses with
|
|
89
|
+
* an empty error map is the worst of the cases, not the mildest: something
|
|
90
|
+
* said no and named nothing, and the person is left pressing a dead button.
|
|
91
|
+
*/
|
|
92
|
+
export function reportInvalid(errors, report, options = {}) {
|
|
93
|
+
const { delay = REPORT_DELAY, messages } = options;
|
|
94
|
+
const words = { ...INVALID_MESSAGES, ...messages };
|
|
95
|
+
const problems = fieldProblems(errors);
|
|
96
|
+
const timer = setTimeout(() => {
|
|
97
|
+
const root = options.root ?? globalThis.document;
|
|
98
|
+
const unseen = unseenProblems(problems, shownErrorFields(root));
|
|
99
|
+
if (problems.length > 0 && unseen.length === 0)
|
|
100
|
+
return;
|
|
101
|
+
const message = unseen.map((problem) => problem.message).find((text) => text !== null);
|
|
102
|
+
report({ tone: "error", title: words.title, description: message ?? words.fallback });
|
|
103
|
+
}, delay);
|
|
104
|
+
return () => clearTimeout(timer);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* `reportInvalid` bound to the app's reporter — pass it straight to a form
|
|
108
|
+
* library's invalid handler (`handleSubmit(onSubmit, report)`), or let
|
|
109
|
+
* `stepValidator` from `@connextar/house/wizard/react-hook-form` call it.
|
|
110
|
+
*/
|
|
111
|
+
export function useInvalidReporter(messages) {
|
|
112
|
+
const report = useActionReporter();
|
|
113
|
+
const title = messages?.title;
|
|
114
|
+
const fallback = messages?.fallback;
|
|
115
|
+
return React.useCallback((errors) => {
|
|
116
|
+
reportInvalid(errors, report, {
|
|
117
|
+
messages: { ...(title === undefined ? {} : { title }), ...(fallback === undefined ? {} : { fallback }) },
|
|
118
|
+
});
|
|
119
|
+
}, [report, title, fallback]);
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=invalid.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invalid.js","sourceRoot":"","sources":["../../src/wizard/invalid.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@connextar/house/wizard/react-hook-form` — the step check for a
|
|
3
|
+
* react-hook-form wizard.
|
|
4
|
+
*
|
|
5
|
+
* Its own entry point so that `react-hook-form` stays an optional peer
|
|
6
|
+
* dependency: every import of it in here is `import type`, which erases, but a
|
|
7
|
+
* declaration file that names a package an app has not installed still fails
|
|
8
|
+
* that app's typecheck. An app on plain state imports `@connextar/house/wizard`
|
|
9
|
+
* and never comes near this.
|
|
10
|
+
*/
|
|
11
|
+
export { stepValidator, useStepValidator } from "./rhf-adapter.js";
|
|
12
|
+
//# sourceMappingURL=react-hook-form-index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"react-hook-form-index.d.ts","sourceRoot":"","sources":["../../src/wizard/react-hook-form-index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@connextar/house/wizard/react-hook-form` — the step check for a
|
|
3
|
+
* react-hook-form wizard.
|
|
4
|
+
*
|
|
5
|
+
* Its own entry point so that `react-hook-form` stays an optional peer
|
|
6
|
+
* dependency: every import of it in here is `import type`, which erases, but a
|
|
7
|
+
* declaration file that names a package an app has not installed still fails
|
|
8
|
+
* that app's typecheck. An app on plain state imports `@connextar/house/wizard`
|
|
9
|
+
* and never comes near this.
|
|
10
|
+
*/
|
|
11
|
+
export { stepValidator, useStepValidator } from "./rhf-adapter.js";
|
|
12
|
+
//# sourceMappingURL=react-hook-form-index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"react-hook-form-index.js","sourceRoot":"","sources":["../../src/wizard/react-hook-form-index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { FieldValues, UseFormReturn } from "react-hook-form";
|
|
2
|
+
import { type InvalidReporter } from "./invalid.js";
|
|
3
|
+
import type { StepCheck } from "./rules.js";
|
|
4
|
+
/**
|
|
5
|
+
* `@connextar/house/wizard/react-hook-form` — the step check most of the cohort
|
|
6
|
+
* wants, for the library most of the cohort uses.
|
|
7
|
+
*
|
|
8
|
+
* Its own entry point, and the reason is types rather than code: every import
|
|
9
|
+
* from `react-hook-form` here is `import type`, so the emitted JavaScript
|
|
10
|
+
* mentions it nowhere and an app without it can still run every part of
|
|
11
|
+
* `@connextar/house/wizard`. What an app without it *cannot* do is typecheck a
|
|
12
|
+
* declaration file that names the package — so the names live behind a subpath
|
|
13
|
+
* nobody has to reach for. MAS has no `react-hook-form` and never will;
|
|
14
|
+
* `entry-points.test.ts` keeps this split honest.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* The usual step check for a react-hook-form wizard: validate just this step's
|
|
18
|
+
* fields, and when one fails, say why if its message is not on screen.
|
|
19
|
+
*
|
|
20
|
+
* Returns `false` rather than a sentence, because a form library puts its
|
|
21
|
+
* messages beside the fields they belong to and a second copy in a banner above
|
|
22
|
+
* them is noise. The exception is the message with nowhere to go, which is what
|
|
23
|
+
* the reporter is for.
|
|
24
|
+
*/
|
|
25
|
+
export declare function stepValidator<T extends FieldValues>(form: UseFormReturn<T>, report: InvalidReporter): StepCheck;
|
|
26
|
+
/**
|
|
27
|
+
* `stepValidator` wired to the app's `ActionFeedbackProvider`, with an identity
|
|
28
|
+
* that does not change between renders — a check that is a new function on every
|
|
29
|
+
* render makes the wizard re-render every step's content for nothing.
|
|
30
|
+
*/
|
|
31
|
+
export declare function useStepValidator<T extends FieldValues>(form: UseFormReturn<T>): StepCheck;
|
|
32
|
+
//# sourceMappingURL=rhf-adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rhf-adapter.d.ts","sourceRoot":"","sources":["../../src/wizard/rhf-adapter.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAa,WAAW,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAE7E,OAAO,EAAsB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AACxE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C;;;;;;;;;;;GAWG;AAEH;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,WAAW,EAAE,IAAI,EAAE,aAAa,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,eAAe,GAAG,SAAS,CAgB/G;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,WAAW,EAAE,IAAI,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,SAAS,CAazF"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { useInvalidReporter } from "./invalid.js";
|
|
4
|
+
/**
|
|
5
|
+
* `@connextar/house/wizard/react-hook-form` — the step check most of the cohort
|
|
6
|
+
* wants, for the library most of the cohort uses.
|
|
7
|
+
*
|
|
8
|
+
* Its own entry point, and the reason is types rather than code: every import
|
|
9
|
+
* from `react-hook-form` here is `import type`, so the emitted JavaScript
|
|
10
|
+
* mentions it nowhere and an app without it can still run every part of
|
|
11
|
+
* `@connextar/house/wizard`. What an app without it *cannot* do is typecheck a
|
|
12
|
+
* declaration file that names the package — so the names live behind a subpath
|
|
13
|
+
* nobody has to reach for. MAS has no `react-hook-form` and never will;
|
|
14
|
+
* `entry-points.test.ts` keeps this split honest.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* The usual step check for a react-hook-form wizard: validate just this step's
|
|
18
|
+
* fields, and when one fails, say why if its message is not on screen.
|
|
19
|
+
*
|
|
20
|
+
* Returns `false` rather than a sentence, because a form library puts its
|
|
21
|
+
* messages beside the fields they belong to and a second copy in a banner above
|
|
22
|
+
* them is noise. The exception is the message with nowhere to go, which is what
|
|
23
|
+
* the reporter is for.
|
|
24
|
+
*/
|
|
25
|
+
export function stepValidator(form, report) {
|
|
26
|
+
return async (fields) => {
|
|
27
|
+
if (fields.length === 0)
|
|
28
|
+
return true;
|
|
29
|
+
const names = fields;
|
|
30
|
+
const ok = await form.trigger(names);
|
|
31
|
+
if (ok)
|
|
32
|
+
return true;
|
|
33
|
+
report(Object.fromEntries(names.flatMap((name) => {
|
|
34
|
+
const error = form.getFieldState(name).error;
|
|
35
|
+
return error ? [[name, error]] : [];
|
|
36
|
+
})));
|
|
37
|
+
return false;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* `stepValidator` wired to the app's `ActionFeedbackProvider`, with an identity
|
|
42
|
+
* that does not change between renders — a check that is a new function on every
|
|
43
|
+
* render makes the wizard re-render every step's content for nothing.
|
|
44
|
+
*/
|
|
45
|
+
export function useStepValidator(form) {
|
|
46
|
+
const report = useInvalidReporter();
|
|
47
|
+
// Written after the render, so the check always reads the current form and
|
|
48
|
+
// reporter without itself having to change.
|
|
49
|
+
const latest = React.useRef({ form, report });
|
|
50
|
+
React.useEffect(() => {
|
|
51
|
+
latest.current = { form, report };
|
|
52
|
+
});
|
|
53
|
+
return React.useCallback((fields, step) => {
|
|
54
|
+
const { form: current, report: tell } = latest.current;
|
|
55
|
+
return stepValidator(current, tell)(fields, step);
|
|
56
|
+
}, []);
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=rhf-adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rhf-adapter.js","sourceRoot":"","sources":["../../src/wizard/rhf-adapter.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B,OAAO,EAAE,kBAAkB,EAAwB,MAAM,cAAc,CAAC;AAGxE;;;;;;;;;;;GAWG;AAEH;;;;;;;;GAQG;AACH,MAAM,UAAU,aAAa,CAAwB,IAAsB,EAAE,MAAuB;IAClG,OAAO,KAAK,EAAE,MAAM,EAAE,EAAE;QACtB,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QACrC,MAAM,KAAK,GAAG,MAAwB,CAAC;QACvC,MAAM,EAAE,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QACrC,IAAI,EAAE;YAAE,OAAO,IAAI,CAAC;QACpB,MAAM,CACJ,MAAM,CAAC,WAAW,CAChB,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE;YACrB,MAAM,KAAK,GAAG,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC;YAC7C,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,CAAU,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/C,CAAC,CAAC,CACH,CACF,CAAC;QACF,OAAO,KAAK,CAAC;IACf,CAAC,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAwB,IAAsB;IAC5E,MAAM,MAAM,GAAG,kBAAkB,EAAE,CAAC;IACpC,2EAA2E;IAC3E,4CAA4C;IAC5C,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;IAC9C,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE;QACnB,MAAM,CAAC,OAAO,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IACpC,CAAC,CAAC,CAAC;IAEH,OAAO,KAAK,CAAC,WAAW,CAAY,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE;QACnD,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,CAAC,OAAO,CAAC;QACvD,OAAO,aAAa,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IACpD,CAAC,EAAE,EAAE,CAAC,CAAC;AACT,CAAC"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rules a stepped form follows — where you are, where you may go, and what
|
|
3
|
+
* has to pass before you get there. No React and no DOM, so they are tested
|
|
4
|
+
* without a browser and hold whatever draws them.
|
|
5
|
+
*
|
|
6
|
+
* The one decision worth spelling out is what a *check* is. Five apps wrote a
|
|
7
|
+
* wizard and arrived at two different answers: a react-hook-form app asks the
|
|
8
|
+
* form to validate some field names and gets a boolean back, while an app
|
|
9
|
+
* holding its values in plain state knows the sentence it wants to show and
|
|
10
|
+
* returns that. Neither is wrong, and a shared wizard that picked one of them
|
|
11
|
+
* would shut the other out — so a check may answer either way:
|
|
12
|
+
*
|
|
13
|
+
* | Returned | Means |
|
|
14
|
+
* | ---------------------------- | ------------------------------------------------ |
|
|
15
|
+
* | `true`, `null`, `undefined` | The step is complete |
|
|
16
|
+
* | `false` | Not complete; the host is showing why, in place |
|
|
17
|
+
* | a sentence | Not complete, and this is what to say |
|
|
18
|
+
*
|
|
19
|
+
* An empty string counts as complete. It is not part of either contract, and a
|
|
20
|
+
* check that returns one by accident must not be able to wedge a form shut with
|
|
21
|
+
* nothing on screen — which is the whole failure this module exists to prevent.
|
|
22
|
+
*/
|
|
23
|
+
/** What a check answers. See the table above. */
|
|
24
|
+
export type StepOutcome = boolean | string | null | undefined | void;
|
|
25
|
+
/** The part of a step these rules need: what it owns, and its own check. */
|
|
26
|
+
export interface StepCheckable {
|
|
27
|
+
/**
|
|
28
|
+
* Field names this step owns. Passed to the form-level check, which is how a
|
|
29
|
+
* react-hook-form host validates one step's worth of fields and no more.
|
|
30
|
+
*/
|
|
31
|
+
fields?: readonly string[];
|
|
32
|
+
/**
|
|
33
|
+
* This step's own check, for a host that keeps validation beside the step
|
|
34
|
+
* rather than in one function. Asked before the form-level check.
|
|
35
|
+
*/
|
|
36
|
+
validate?: StepCheck;
|
|
37
|
+
}
|
|
38
|
+
/** Is this step complete? Asked before leaving it, and again for every step on submit. */
|
|
39
|
+
export type StepCheck = (fields: string[], step: StepCheckable) => StepOutcome | Promise<StepOutcome>;
|
|
40
|
+
export type StepResult = {
|
|
41
|
+
ok: true;
|
|
42
|
+
} | {
|
|
43
|
+
ok: false;
|
|
44
|
+
message: string | null;
|
|
45
|
+
};
|
|
46
|
+
/** Normalise whatever a check answered. */
|
|
47
|
+
export declare function stepResult(outcome: StepOutcome): StepResult;
|
|
48
|
+
/**
|
|
49
|
+
* Ask everything that has a say about one step. The step's own check first, then
|
|
50
|
+
* the form-level one; the first refusal is the answer, so its message is the one
|
|
51
|
+
* shown rather than the last one computed.
|
|
52
|
+
*/
|
|
53
|
+
export declare function checkStep(step: StepCheckable, onValidateStep?: StepCheck): Promise<StepResult>;
|
|
54
|
+
export interface StepFailure {
|
|
55
|
+
index: number;
|
|
56
|
+
message: string | null;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The first step that will not pass, checked in order from the beginning.
|
|
60
|
+
*
|
|
61
|
+
* This is what a submit does, and checking only the last step is the defect
|
|
62
|
+
* every one of the five copies had at some point. Step back by a marker (which
|
|
63
|
+
* does not validate), clear a required field, jump to the end and press Save:
|
|
64
|
+
* the form refuses with the problem on a step nobody is looking at, and the
|
|
65
|
+
* button looks broken. In MAS it was worse — nothing was checked on submit at
|
|
66
|
+
* all, so a second press after a successful create sent a record whose first
|
|
67
|
+
* step had been cleared.
|
|
68
|
+
*
|
|
69
|
+
* In order matters as much as every: the person is sent to the earliest thing
|
|
70
|
+
* that needs fixing, which is the one they will understand.
|
|
71
|
+
*/
|
|
72
|
+
export declare function firstFailure<S extends StepCheckable>(steps: readonly S[], onValidateStep?: StepCheck): Promise<StepFailure | null>;
|
|
73
|
+
export type MarkerState = "done" | "current" | "upcoming";
|
|
74
|
+
/** How a marker reads: behind you, where you are, or ahead. */
|
|
75
|
+
export declare function markerState(index: number, current: number): MarkerState;
|
|
76
|
+
/**
|
|
77
|
+
* May this marker be clicked?
|
|
78
|
+
*
|
|
79
|
+
* Anything reached before now, including the step you are on — clicking that one
|
|
80
|
+
* does nothing, and disabling it takes the tracker's "you are here" out of the
|
|
81
|
+
* tab order for no gain. Steps never reached stay locked, because skipping
|
|
82
|
+
* forward past a step that has never been checked is how people arrive at a
|
|
83
|
+
* submit button that will not work with nothing saying why.
|
|
84
|
+
*/
|
|
85
|
+
export declare function isReachable(index: number, furthest: number): boolean;
|
|
86
|
+
/** The next index, not past the end. */
|
|
87
|
+
export declare function nextIndex(current: number, total: number): number;
|
|
88
|
+
/** The previous index, not past the beginning. */
|
|
89
|
+
export declare function previousIndex(current: number): number;
|
|
90
|
+
/** How far through, for the apps that show a bar rather than a count. */
|
|
91
|
+
export declare function percentComplete(current: number, total: number): number;
|
|
92
|
+
//# sourceMappingURL=rules.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../src/wizard/rules.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,iDAAiD;AACjD,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI,CAAC;AAErE,4EAA4E;AAC5E,MAAM,WAAW,aAAa;IAC5B;;;OAGG;IACH,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED,0FAA0F;AAC1F,MAAM,MAAM,SAAS,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,aAAa,KAAK,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;AAEtG,MAAM,MAAM,UAAU,GAAG;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC;AAI9E,2CAA2C;AAC3C,wBAAgB,UAAU,CAAC,OAAO,EAAE,WAAW,GAAG,UAAU,CAI3D;AAED;;;;GAIG;AACH,wBAAsB,SAAS,CAAC,IAAI,EAAE,aAAa,EAAE,cAAc,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CAQpG;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,YAAY,CAAC,CAAC,SAAS,aAAa,EACxD,KAAK,EAAE,SAAS,CAAC,EAAE,EACnB,cAAc,CAAC,EAAE,SAAS,GACzB,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAQ7B;AAED,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,SAAS,GAAG,UAAU,CAAC;AAE1D,+DAA+D;AAC/D,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,WAAW,CAGvE;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAEpE;AAED,wCAAwC;AACxC,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAEhE;AAED,kDAAkD;AAClD,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAErD;AAED,yEAAyE;AACzE,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAGtE"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rules a stepped form follows — where you are, where you may go, and what
|
|
3
|
+
* has to pass before you get there. No React and no DOM, so they are tested
|
|
4
|
+
* without a browser and hold whatever draws them.
|
|
5
|
+
*
|
|
6
|
+
* The one decision worth spelling out is what a *check* is. Five apps wrote a
|
|
7
|
+
* wizard and arrived at two different answers: a react-hook-form app asks the
|
|
8
|
+
* form to validate some field names and gets a boolean back, while an app
|
|
9
|
+
* holding its values in plain state knows the sentence it wants to show and
|
|
10
|
+
* returns that. Neither is wrong, and a shared wizard that picked one of them
|
|
11
|
+
* would shut the other out — so a check may answer either way:
|
|
12
|
+
*
|
|
13
|
+
* | Returned | Means |
|
|
14
|
+
* | ---------------------------- | ------------------------------------------------ |
|
|
15
|
+
* | `true`, `null`, `undefined` | The step is complete |
|
|
16
|
+
* | `false` | Not complete; the host is showing why, in place |
|
|
17
|
+
* | a sentence | Not complete, and this is what to say |
|
|
18
|
+
*
|
|
19
|
+
* An empty string counts as complete. It is not part of either contract, and a
|
|
20
|
+
* check that returns one by accident must not be able to wedge a form shut with
|
|
21
|
+
* nothing on screen — which is the whole failure this module exists to prevent.
|
|
22
|
+
*/
|
|
23
|
+
const PASSED = { ok: true };
|
|
24
|
+
/** Normalise whatever a check answered. */
|
|
25
|
+
export function stepResult(outcome) {
|
|
26
|
+
if (outcome === false)
|
|
27
|
+
return { ok: false, message: null };
|
|
28
|
+
if (typeof outcome === "string" && outcome !== "")
|
|
29
|
+
return { ok: false, message: outcome };
|
|
30
|
+
return PASSED;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Ask everything that has a say about one step. The step's own check first, then
|
|
34
|
+
* the form-level one; the first refusal is the answer, so its message is the one
|
|
35
|
+
* shown rather than the last one computed.
|
|
36
|
+
*/
|
|
37
|
+
export async function checkStep(step, onValidateStep) {
|
|
38
|
+
const fields = [...(step.fields ?? [])];
|
|
39
|
+
for (const check of [step.validate, onValidateStep]) {
|
|
40
|
+
if (!check)
|
|
41
|
+
continue;
|
|
42
|
+
const result = stepResult(await check(fields, step));
|
|
43
|
+
if (!result.ok)
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
return PASSED;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The first step that will not pass, checked in order from the beginning.
|
|
50
|
+
*
|
|
51
|
+
* This is what a submit does, and checking only the last step is the defect
|
|
52
|
+
* every one of the five copies had at some point. Step back by a marker (which
|
|
53
|
+
* does not validate), clear a required field, jump to the end and press Save:
|
|
54
|
+
* the form refuses with the problem on a step nobody is looking at, and the
|
|
55
|
+
* button looks broken. In MAS it was worse — nothing was checked on submit at
|
|
56
|
+
* all, so a second press after a successful create sent a record whose first
|
|
57
|
+
* step had been cleared.
|
|
58
|
+
*
|
|
59
|
+
* In order matters as much as every: the person is sent to the earliest thing
|
|
60
|
+
* that needs fixing, which is the one they will understand.
|
|
61
|
+
*/
|
|
62
|
+
export async function firstFailure(steps, onValidateStep) {
|
|
63
|
+
for (let index = 0; index < steps.length; index++) {
|
|
64
|
+
const step = steps[index];
|
|
65
|
+
if (!step)
|
|
66
|
+
continue;
|
|
67
|
+
const result = await checkStep(step, onValidateStep);
|
|
68
|
+
if (!result.ok)
|
|
69
|
+
return { index, message: result.message };
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
/** How a marker reads: behind you, where you are, or ahead. */
|
|
74
|
+
export function markerState(index, current) {
|
|
75
|
+
if (index < current)
|
|
76
|
+
return "done";
|
|
77
|
+
return index === current ? "current" : "upcoming";
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* May this marker be clicked?
|
|
81
|
+
*
|
|
82
|
+
* Anything reached before now, including the step you are on — clicking that one
|
|
83
|
+
* does nothing, and disabling it takes the tracker's "you are here" out of the
|
|
84
|
+
* tab order for no gain. Steps never reached stay locked, because skipping
|
|
85
|
+
* forward past a step that has never been checked is how people arrive at a
|
|
86
|
+
* submit button that will not work with nothing saying why.
|
|
87
|
+
*/
|
|
88
|
+
export function isReachable(index, furthest) {
|
|
89
|
+
return index <= furthest;
|
|
90
|
+
}
|
|
91
|
+
/** The next index, not past the end. */
|
|
92
|
+
export function nextIndex(current, total) {
|
|
93
|
+
return Math.min(current + 1, Math.max(total - 1, 0));
|
|
94
|
+
}
|
|
95
|
+
/** The previous index, not past the beginning. */
|
|
96
|
+
export function previousIndex(current) {
|
|
97
|
+
return Math.max(current - 1, 0);
|
|
98
|
+
}
|
|
99
|
+
/** How far through, for the apps that show a bar rather than a count. */
|
|
100
|
+
export function percentComplete(current, total) {
|
|
101
|
+
if (total <= 0)
|
|
102
|
+
return 0;
|
|
103
|
+
return Math.round((Math.min(current + 1, total) / total) * 100);
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=rules.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rules.js","sourceRoot":"","sources":["../../src/wizard/rules.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { type StepCheck, type StepCheckable } from "./rules.js";
|
|
3
|
+
export interface WizardStep extends StepCheckable {
|
|
4
|
+
id: string;
|
|
5
|
+
title: string;
|
|
6
|
+
/** One line saying what this step is for. */
|
|
7
|
+
description?: string;
|
|
8
|
+
content: React.ReactNode;
|
|
9
|
+
}
|
|
10
|
+
/** Every element the wizard draws. Each value replaces the default for that part. */
|
|
11
|
+
export interface WizardClassNames {
|
|
12
|
+
form: string;
|
|
13
|
+
markers: string;
|
|
14
|
+
markerItem: string;
|
|
15
|
+
marker: string;
|
|
16
|
+
markerLocked: string;
|
|
17
|
+
circle: string;
|
|
18
|
+
circleDone: string;
|
|
19
|
+
circleCurrent: string;
|
|
20
|
+
circleUpcoming: string;
|
|
21
|
+
markerTitle: string;
|
|
22
|
+
markerTitleCurrent: string;
|
|
23
|
+
connector: string;
|
|
24
|
+
connectorDone: string;
|
|
25
|
+
bar: string;
|
|
26
|
+
barFill: string;
|
|
27
|
+
heading: string;
|
|
28
|
+
description: string;
|
|
29
|
+
alert: string;
|
|
30
|
+
panel: string;
|
|
31
|
+
footer: string;
|
|
32
|
+
counter: string;
|
|
33
|
+
button: string;
|
|
34
|
+
backButton: string;
|
|
35
|
+
forwardButton: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The house look, on the shared theme tokens. An app without them — MAS keeps
|
|
39
|
+
* its palette in literal classes — replaces the parts it needs through
|
|
40
|
+
* `classNames`, exactly as the guide renderer allows.
|
|
41
|
+
*/
|
|
42
|
+
export declare const WIZARD_CLASS_NAMES: WizardClassNames;
|
|
43
|
+
export interface FormWizardProps {
|
|
44
|
+
steps: WizardStep[];
|
|
45
|
+
/**
|
|
46
|
+
* Is the step being left complete? Asked before each advance, and again for
|
|
47
|
+
* every step on submit. See `StepCheck` for the ways it may answer.
|
|
48
|
+
*/
|
|
49
|
+
onValidateStep?: StepCheck;
|
|
50
|
+
/** Called once every step has passed, and never before. */
|
|
51
|
+
onSubmit: () => void | Promise<void>;
|
|
52
|
+
/** Shown in place of Back on the first step; without it, Back is disabled there. */
|
|
53
|
+
onCancel?: () => void;
|
|
54
|
+
submitLabel?: React.ReactNode;
|
|
55
|
+
nextLabel?: React.ReactNode;
|
|
56
|
+
cancelLabel?: React.ReactNode;
|
|
57
|
+
backLabel?: React.ReactNode;
|
|
58
|
+
/** The submission is in flight: navigation locks and the button says so. */
|
|
59
|
+
pending?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Every step open from the start — for editing, where the values are all there
|
|
62
|
+
* already and the change is usually to one field on a later step.
|
|
63
|
+
*/
|
|
64
|
+
allReachable?: boolean;
|
|
65
|
+
/** Show how far through as a bar as well as a count. */
|
|
66
|
+
showProgressBar?: boolean;
|
|
67
|
+
/** A heading or introduction above the tracker, inside the form. */
|
|
68
|
+
header?: React.ReactNode;
|
|
69
|
+
className?: string;
|
|
70
|
+
footerClassName?: string;
|
|
71
|
+
classNames?: Partial<WizardClassNames>;
|
|
72
|
+
"aria-label"?: string;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* A long form split into steps. The one stepper for every multi-step form in
|
|
76
|
+
* every app — the union of what five of them each learnt the hard way.
|
|
77
|
+
*
|
|
78
|
+
* - **It renders the `<form>` itself,** so nothing reaches `onSubmit`
|
|
79
|
+
* unchecked and so Enter has one place to land. A wizard sitting inside
|
|
80
|
+
* somebody else's form cannot promise either.
|
|
81
|
+
* - **Enter never skips ahead.** Pressing Enter in a one-line field submits the
|
|
82
|
+
* form the browser's way, and before the last step that submission means
|
|
83
|
+
* Continue: this step is checked and, if it passes, the next one opens. Only
|
|
84
|
+
* the last step's Enter submits.
|
|
85
|
+
* - **There is one submit button and its `type` never changes.** PharmaLine put
|
|
86
|
+
* a `type="button"` Continue and a `type="submit"` Create in the same slot;
|
|
87
|
+
* React reused the one DOM node and switched its type mid-click, and Chromium
|
|
88
|
+
* submitted — every booking made from the dashboard skipped Confirm and sent a
|
|
89
|
+
* confirmation nobody chose. jsdom's timing hid it, so the guard here is
|
|
90
|
+
* structural rather than behavioural: one button, always `type="submit"`.
|
|
91
|
+
* - **Steps are checked as you leave them.** `onValidateStep` (or the step's own
|
|
92
|
+
* `validate`) is asked before each advance, and the step stays put if it
|
|
93
|
+
* refuses, saying why when the check gave a message.
|
|
94
|
+
* - **Submitting checks every step, in order, and lands on the first that
|
|
95
|
+
* fails.** Checking only the last one, or none at all, is how a form refuses
|
|
96
|
+
* with the problem on a screen nobody is looking at.
|
|
97
|
+
* - **Continue is never disabled for being incomplete.** A greyed-out button
|
|
98
|
+
* that will not say what is missing is a refusal with no words; it is only
|
|
99
|
+
* disabled while something is actually in flight.
|
|
100
|
+
* - **Visited markers are clickable; steps ahead stay locked** (unless
|
|
101
|
+
* `allReachable`), so the tracker is navigation rather than a way past a step
|
|
102
|
+
* that has never been checked.
|
|
103
|
+
* - **All steps stay mounted,** hidden rather than unmounted: one register, and
|
|
104
|
+
* nothing typed is lost by stepping back to check something.
|
|
105
|
+
*
|
|
106
|
+
* What it deliberately does not do is validate anything itself, or know what a
|
|
107
|
+
* button looks like. The check is a function the host supplies, so
|
|
108
|
+
* react-hook-form, plain state and a form built out of `useState` can all host
|
|
109
|
+
* it; the look is theme-token classes an app replaces through `classNames`.
|
|
110
|
+
*/
|
|
111
|
+
export declare function FormWizard({ steps, onValidateStep, onSubmit, onCancel, submitLabel, nextLabel, cancelLabel, backLabel, pending, allReachable, showProgressBar, header, className, footerClassName, classNames, "aria-label": ariaLabel, }: FormWizardProps): React.JSX.Element | null;
|
|
112
|
+
/**
|
|
113
|
+
* A field's error message, marked so the reporter can tell a visible error from
|
|
114
|
+
* a hidden one. An app whose form library draws its own message adds
|
|
115
|
+
* `data-error-for` to that component instead of using this.
|
|
116
|
+
*/
|
|
117
|
+
export declare function FieldError({ name, message, className, }: {
|
|
118
|
+
name: string;
|
|
119
|
+
message?: string | null;
|
|
120
|
+
className?: string;
|
|
121
|
+
}): React.JSX.Element | null;
|
|
122
|
+
/** A read-only label/value row, for the review step at the end of a wizard. */
|
|
123
|
+
export declare function ReviewRow({ label, value }: {
|
|
124
|
+
label: React.ReactNode;
|
|
125
|
+
value: React.ReactNode;
|
|
126
|
+
}): React.JSX.Element;
|
|
127
|
+
//# sourceMappingURL=wizard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wizard.d.ts","sourceRoot":"","sources":["../../src/wizard/wizard.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { Check, ChevronLeft, ChevronRight } from "lucide-react";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { cx } from "../internal/cx.js";
|
|
6
|
+
import { ERROR_ATTRIBUTE } from "./invalid.js";
|
|
7
|
+
import { checkStep, firstFailure, isReachable, markerState, nextIndex, percentComplete, previousIndex, } from "./rules.js";
|
|
8
|
+
/** Inputs where Enter is not "I'm done with this field". */
|
|
9
|
+
const NOT_ONE_LINE_FIELDS = new Set([
|
|
10
|
+
"button",
|
|
11
|
+
"checkbox",
|
|
12
|
+
"color",
|
|
13
|
+
"file",
|
|
14
|
+
"hidden",
|
|
15
|
+
"image",
|
|
16
|
+
"radio",
|
|
17
|
+
"range",
|
|
18
|
+
"reset",
|
|
19
|
+
"submit",
|
|
20
|
+
]);
|
|
21
|
+
/**
|
|
22
|
+
* The house look, on the shared theme tokens. An app without them — MAS keeps
|
|
23
|
+
* its palette in literal classes — replaces the parts it needs through
|
|
24
|
+
* `classNames`, exactly as the guide renderer allows.
|
|
25
|
+
*/
|
|
26
|
+
export const WIZARD_CLASS_NAMES = {
|
|
27
|
+
form: "flex flex-col gap-5",
|
|
28
|
+
markers: "flex items-center gap-2",
|
|
29
|
+
markerItem: "flex min-w-0 flex-1 items-center gap-2 last:flex-none sm:last:flex-1",
|
|
30
|
+
marker: "flex min-w-0 items-center gap-2 rounded-lg px-1.5 py-1 text-left transition-colors focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none sm:flex-1",
|
|
31
|
+
markerLocked: "cursor-not-allowed opacity-60",
|
|
32
|
+
circle: "flex size-6 shrink-0 items-center justify-center rounded-full border text-xs font-semibold",
|
|
33
|
+
circleDone: "border-primary bg-primary text-primary-foreground",
|
|
34
|
+
circleCurrent: "border-2 border-primary text-primary",
|
|
35
|
+
circleUpcoming: "border-border text-muted-foreground",
|
|
36
|
+
markerTitle: "hidden truncate text-sm sm:block",
|
|
37
|
+
markerTitleCurrent: "font-medium text-foreground",
|
|
38
|
+
connector: "h-px flex-1 bg-border sm:w-3 sm:flex-none",
|
|
39
|
+
connectorDone: "bg-primary",
|
|
40
|
+
bar: "h-1.5 w-full overflow-hidden rounded-full bg-muted",
|
|
41
|
+
barFill: "h-full rounded-full bg-primary transition-[width]",
|
|
42
|
+
heading: "text-sm font-medium",
|
|
43
|
+
description: "text-xs text-muted-foreground",
|
|
44
|
+
alert: "rounded-lg border border-destructive/40 bg-destructive/10 px-4 py-3 text-sm text-destructive",
|
|
45
|
+
panel: "space-y-4",
|
|
46
|
+
footer: "flex items-center justify-between gap-2 border-t border-border pt-4",
|
|
47
|
+
counter: "text-xs tabular-nums text-muted-foreground",
|
|
48
|
+
button: "inline-flex items-center gap-1.5 rounded-md px-3 py-2 text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none disabled:pointer-events-none disabled:opacity-60",
|
|
49
|
+
backButton: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
50
|
+
forwardButton: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* A long form split into steps. The one stepper for every multi-step form in
|
|
54
|
+
* every app — the union of what five of them each learnt the hard way.
|
|
55
|
+
*
|
|
56
|
+
* - **It renders the `<form>` itself,** so nothing reaches `onSubmit`
|
|
57
|
+
* unchecked and so Enter has one place to land. A wizard sitting inside
|
|
58
|
+
* somebody else's form cannot promise either.
|
|
59
|
+
* - **Enter never skips ahead.** Pressing Enter in a one-line field submits the
|
|
60
|
+
* form the browser's way, and before the last step that submission means
|
|
61
|
+
* Continue: this step is checked and, if it passes, the next one opens. Only
|
|
62
|
+
* the last step's Enter submits.
|
|
63
|
+
* - **There is one submit button and its `type` never changes.** PharmaLine put
|
|
64
|
+
* a `type="button"` Continue and a `type="submit"` Create in the same slot;
|
|
65
|
+
* React reused the one DOM node and switched its type mid-click, and Chromium
|
|
66
|
+
* submitted — every booking made from the dashboard skipped Confirm and sent a
|
|
67
|
+
* confirmation nobody chose. jsdom's timing hid it, so the guard here is
|
|
68
|
+
* structural rather than behavioural: one button, always `type="submit"`.
|
|
69
|
+
* - **Steps are checked as you leave them.** `onValidateStep` (or the step's own
|
|
70
|
+
* `validate`) is asked before each advance, and the step stays put if it
|
|
71
|
+
* refuses, saying why when the check gave a message.
|
|
72
|
+
* - **Submitting checks every step, in order, and lands on the first that
|
|
73
|
+
* fails.** Checking only the last one, or none at all, is how a form refuses
|
|
74
|
+
* with the problem on a screen nobody is looking at.
|
|
75
|
+
* - **Continue is never disabled for being incomplete.** A greyed-out button
|
|
76
|
+
* that will not say what is missing is a refusal with no words; it is only
|
|
77
|
+
* disabled while something is actually in flight.
|
|
78
|
+
* - **Visited markers are clickable; steps ahead stay locked** (unless
|
|
79
|
+
* `allReachable`), so the tracker is navigation rather than a way past a step
|
|
80
|
+
* that has never been checked.
|
|
81
|
+
* - **All steps stay mounted,** hidden rather than unmounted: one register, and
|
|
82
|
+
* nothing typed is lost by stepping back to check something.
|
|
83
|
+
*
|
|
84
|
+
* What it deliberately does not do is validate anything itself, or know what a
|
|
85
|
+
* button looks like. The check is a function the host supplies, so
|
|
86
|
+
* react-hook-form, plain state and a form built out of `useState` can all host
|
|
87
|
+
* it; the look is theme-token classes an app replaces through `classNames`.
|
|
88
|
+
*/
|
|
89
|
+
export function FormWizard({ steps, onValidateStep, onSubmit, onCancel, submitLabel = "Save", nextLabel = "Continue", cancelLabel = "Cancel", backLabel = "Back", pending = false, allReachable = false, showProgressBar = false, header, className, footerClassName, classNames, "aria-label": ariaLabel, }) {
|
|
90
|
+
const c = { ...WIZARD_CLASS_NAMES, ...classNames };
|
|
91
|
+
const [current, setCurrent] = React.useState(0);
|
|
92
|
+
const [furthest, setFurthest] = React.useState(allReachable ? steps.length - 1 : 0);
|
|
93
|
+
const [checking, setChecking] = React.useState(false);
|
|
94
|
+
const [problem, setProblem] = React.useState(null);
|
|
95
|
+
const inFlight = React.useRef(false);
|
|
96
|
+
const formId = React.useId();
|
|
97
|
+
const step = steps[current];
|
|
98
|
+
const isFirst = current === 0;
|
|
99
|
+
const isLast = current === steps.length - 1;
|
|
100
|
+
const busy = pending || checking;
|
|
101
|
+
function goTo(index) {
|
|
102
|
+
setProblem(null);
|
|
103
|
+
setCurrent(index);
|
|
104
|
+
}
|
|
105
|
+
async function goNext() {
|
|
106
|
+
if (!step)
|
|
107
|
+
return;
|
|
108
|
+
setChecking(true);
|
|
109
|
+
try {
|
|
110
|
+
const result = await checkStep(step, onValidateStep);
|
|
111
|
+
if (!result.ok) {
|
|
112
|
+
setProblem(result.message);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
setChecking(false);
|
|
118
|
+
}
|
|
119
|
+
const next = nextIndex(current, steps.length);
|
|
120
|
+
setProblem(null);
|
|
121
|
+
setCurrent(next);
|
|
122
|
+
setFurthest((seen) => Math.max(seen, next));
|
|
123
|
+
}
|
|
124
|
+
async function submit() {
|
|
125
|
+
setChecking(true);
|
|
126
|
+
try {
|
|
127
|
+
const failure = await firstFailure(steps, onValidateStep);
|
|
128
|
+
if (failure) {
|
|
129
|
+
// Show the step that holds the problem, so its message is on screen.
|
|
130
|
+
setCurrent(failure.index);
|
|
131
|
+
setProblem(failure.message);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
setProblem(null);
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
setChecking(false);
|
|
138
|
+
}
|
|
139
|
+
await onSubmit();
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Every way of submitting — the footer button, or Enter in a field — comes
|
|
143
|
+
* through here, and before the last step it means Continue. Re-entry is
|
|
144
|
+
* refused rather than queued: a second press is the person saying the first
|
|
145
|
+
* one gave them nothing to look at.
|
|
146
|
+
*/
|
|
147
|
+
function handleSubmit(event) {
|
|
148
|
+
event.preventDefault();
|
|
149
|
+
if (busy || inFlight.current)
|
|
150
|
+
return;
|
|
151
|
+
inFlight.current = true;
|
|
152
|
+
void (isLast ? submit() : goNext()).finally(() => {
|
|
153
|
+
inFlight.current = false;
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Enter in a one-line field means "go on", whatever the field. Browsers
|
|
158
|
+
* disagree about Enter in some of them — Chromium does nothing on Enter in a
|
|
159
|
+
* date-and-time field — so the form asks for submission itself. A field that
|
|
160
|
+
* handles Enter on its own (and cancels it) is left alone, and so is a
|
|
161
|
+
* textarea, where Enter is a new line.
|
|
162
|
+
*/
|
|
163
|
+
function handleKeyDown(event) {
|
|
164
|
+
if (event.key !== "Enter" || event.defaultPrevented || event.nativeEvent.isComposing)
|
|
165
|
+
return;
|
|
166
|
+
const target = event.target;
|
|
167
|
+
if (!(target instanceof HTMLInputElement) || NOT_ONE_LINE_FIELDS.has(target.type))
|
|
168
|
+
return;
|
|
169
|
+
event.preventDefault();
|
|
170
|
+
event.currentTarget.requestSubmit();
|
|
171
|
+
}
|
|
172
|
+
if (!step)
|
|
173
|
+
return null;
|
|
174
|
+
const percent = percentComplete(current, steps.length);
|
|
175
|
+
return (_jsxs("form", { onSubmit: handleSubmit, onKeyDown: handleKeyDown, noValidate: true, "aria-label": ariaLabel, className: cx(c.form, className), children: [header, _jsx("ol", { className: c.markers, "aria-label": "Progress", children: steps.map((s, index) => {
|
|
176
|
+
const state = markerState(index, current);
|
|
177
|
+
const reachable = isReachable(index, furthest);
|
|
178
|
+
return (_jsxs("li", { className: c.markerItem, children: [_jsxs("button", { type: "button", disabled: !reachable || busy, onClick: () => goTo(index), "aria-current": state === "current" ? "step" : undefined, "aria-controls": `${formId}-${s.id}`, "aria-label": `Step ${index + 1}: ${s.title}`, className: cx(c.marker, !reachable && c.markerLocked), children: [_jsx("span", { "aria-hidden": true, className: cx(c.circle, state === "done" && c.circleDone, state === "current" && c.circleCurrent, state === "upcoming" && c.circleUpcoming), children: state === "done" ? _jsx(Check, { className: "size-3.5" }) : index + 1 }), _jsx("span", { "aria-hidden": true, className: cx(c.markerTitle, state === "current" && c.markerTitleCurrent), children: s.title })] }), index < steps.length - 1 ? (_jsx("span", { "aria-hidden": true, className: cx(c.connector, state === "done" && c.connectorDone) })) : null] }, s.id));
|
|
179
|
+
}) }), showProgressBar ? (_jsx("div", { className: c.bar, role: "progressbar", "aria-label": "Form progress", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": percent, children: _jsx("div", { className: c.barFill, style: { width: `${percent}%` } }) })) : null, _jsxs("div", { children: [_jsx("h3", { className: cx(c.heading, "sm:hidden"), children: step.title }), step.description ? _jsx("p", { className: c.description, children: step.description }) : null] }), problem ? (_jsx("p", { className: c.alert, role: "alert", children: problem })) : null, steps.map((s, index) => (_jsx("section", { id: `${formId}-${s.id}`, hidden: index !== current, "aria-label": s.title, className: c.panel, children: s.content }, s.id))), _jsxs("div", { className: cx(c.footer, footerClassName), children: [isFirst && onCancel ? (_jsx("button", { type: "button", onClick: onCancel, disabled: pending, className: cx(c.button, c.backButton), children: cancelLabel })) : (_jsxs("button", { type: "button", onClick: () => goTo(previousIndex(current)), disabled: isFirst || busy, className: cx(c.button, c.backButton), children: [_jsx(ChevronLeft, { className: "size-4", "aria-hidden": true }), backLabel] })), _jsxs("span", { className: c.counter, "aria-live": "polite", children: ["Step ", current + 1, " of ", steps.length] }), _jsx("button", { type: "submit", disabled: busy, "data-busy": busy ? "true" : undefined, className: cx(c.button, c.forwardButton), children: isLast ? (submitLabel) : (_jsxs(_Fragment, { children: [nextLabel, _jsx(ChevronRight, { className: "size-4", "aria-hidden": true })] })) })] })] }));
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* A field's error message, marked so the reporter can tell a visible error from
|
|
183
|
+
* a hidden one. An app whose form library draws its own message adds
|
|
184
|
+
* `data-error-for` to that component instead of using this.
|
|
185
|
+
*/
|
|
186
|
+
export function FieldError({ name, message, className, }) {
|
|
187
|
+
if (!message)
|
|
188
|
+
return null;
|
|
189
|
+
return (_jsx("p", { [ERROR_ATTRIBUTE]: name, className: className ?? "text-xs text-destructive", children: message }));
|
|
190
|
+
}
|
|
191
|
+
/** A read-only label/value row, for the review step at the end of a wizard. */
|
|
192
|
+
export function ReviewRow({ label, value }) {
|
|
193
|
+
return (_jsxs("div", { className: "flex items-start justify-between gap-4 border-b border-border py-2 last:border-0", children: [_jsx("span", { className: "text-sm text-muted-foreground", children: label }), _jsx("span", { className: "text-right text-sm font-medium", children: value || _jsx("span", { className: "text-muted-foreground", children: "\u2014" }) })] }));
|
|
194
|
+
}
|
|
195
|
+
//# sourceMappingURL=wizard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wizard.js","sourceRoot":"","sources":["../../src/wizard/wizard.tsx"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@connextar/house",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "The pieces every app we build needs and none of them should own a copy of.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -50,6 +50,14 @@
|
|
|
50
50
|
"types": "./dist/ux/navigation-index.d.ts",
|
|
51
51
|
"default": "./dist/ux/navigation-index.js"
|
|
52
52
|
},
|
|
53
|
+
"./wizard": {
|
|
54
|
+
"types": "./dist/wizard/index.d.ts",
|
|
55
|
+
"default": "./dist/wizard/index.js"
|
|
56
|
+
},
|
|
57
|
+
"./wizard/react-hook-form": {
|
|
58
|
+
"types": "./dist/wizard/react-hook-form-index.d.ts",
|
|
59
|
+
"default": "./dist/wizard/react-hook-form-index.js"
|
|
60
|
+
},
|
|
53
61
|
"./editor": {
|
|
54
62
|
"types": "./dist/editor/index.d.ts",
|
|
55
63
|
"default": "./dist/editor/index.js"
|
|
@@ -85,11 +93,15 @@
|
|
|
85
93
|
"peerDependencies": {
|
|
86
94
|
"next": "^16.0.0",
|
|
87
95
|
"react": "^19.0.0",
|
|
88
|
-
"react-dom": "^19.0.0"
|
|
96
|
+
"react-dom": "^19.0.0",
|
|
97
|
+
"react-hook-form": "^7.60.0"
|
|
89
98
|
},
|
|
90
99
|
"peerDependenciesMeta": {
|
|
91
100
|
"next": {
|
|
92
101
|
"optional": true
|
|
102
|
+
},
|
|
103
|
+
"react-hook-form": {
|
|
104
|
+
"optional": true
|
|
93
105
|
}
|
|
94
106
|
},
|
|
95
107
|
"devDependencies": {
|
|
@@ -106,6 +118,7 @@
|
|
|
106
118
|
"prettier": "^3.0.0",
|
|
107
119
|
"react": "^19.2.8",
|
|
108
120
|
"react-dom": "^19.2.8",
|
|
121
|
+
"react-hook-form": "^7.68.0",
|
|
109
122
|
"typescript": "^5.7.0",
|
|
110
123
|
"typescript-eslint": "^8.69.0",
|
|
111
124
|
"vitest": "^4.0.0"
|