@marktiderman/genesis-forms-web 0.2.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 +52 -2
- package/dist/index.d.ts +129 -2
- package/dist/index.js +814 -64
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ import { Form } from "@marktiderman/genesis-forms-web";
|
|
|
24
24
|
progress bar and Enter to continue (Ctrl/Cmd+Enter in long text; Enter on a focused radio or
|
|
25
25
|
checkbox too). After a single pick (rating, NPS, yes/no, one choice) it moves on by itself
|
|
26
26
|
(`autoAdvance={false}` turns it off): only a click, tap or Space counts as a pick, arrow keys just
|
|
27
|
-
move through the options, and it never moves on from the last question, so
|
|
27
|
+
move through the options, and it never moves on from the last question, so Send is always a
|
|
28
28
|
deliberate press. `classic` shows every visible question; a failed submit focuses an error summary
|
|
29
29
|
above the fields that links to each invalid one.
|
|
30
30
|
- **Presentation:** `presentation="embedded"` (the default) is a block inside your own page.
|
|
@@ -40,7 +40,7 @@ import { Form } from "@marktiderman/genesis-forms-web";
|
|
|
40
40
|
button at the top-left. Each new question fades and slides in (CSS `@starting-style`, none with
|
|
41
41
|
`prefers-reduced-motion`). The ending is centred and large. In classic layout, page mode is one
|
|
42
42
|
scrolling page, Google Forms style: a header card with the title and description, each question
|
|
43
|
-
in its own card (red-edged while invalid),
|
|
43
|
+
in its own card (red-edged while invalid), Send at the end.
|
|
44
44
|
`brand={{ name: "Acme", logo }}` shows an organisation's or business's logo (an image URL or an
|
|
45
45
|
element; its initials without one) and name small at the top of every step and on the ending.
|
|
46
46
|
Custom slots and renderers can match with `useFormPresentation()`, which returns
|
|
@@ -50,6 +50,19 @@ import { Form } from "@marktiderman/genesis-forms-web";
|
|
|
50
50
|
<Form form={eventFeedback} presentation="page" brand={{ name: "Acme" }} onSubmit={send} />
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
+
- **Save answers as they go:** with the definition's `settings.saveProgress` on, `onProgress(engine,
|
|
54
|
+
{ step, total, fieldId })` is called a moment after each step forward (`PROGRESS_DELAY_MS`; in
|
|
55
|
+
classic layout, `CLASSIC_PROGRESS_DELAY_MS` after the answers change), with `fieldId` the
|
|
56
|
+
furthest question reached (`formProgress` from genesis-forms) and `engine.answers()` the answers
|
|
57
|
+
so far. It never holds up Next: a rejection is ignored (the next call carries every answer
|
|
58
|
+
again), a pending call goes at once on `pagehide`, and none is made once the form is submitted.
|
|
59
|
+
Under the first question a note says "Your answers save as you go." (`progressNotice`; `null`
|
|
60
|
+
hides it). Without `onProgress`, or with the setting off, nothing is saved and nothing is said.
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
<Form form={feedback} presentation="page" onProgress={(engine, p) => saveDraft(engine.answers(), p.fieldId)} onSubmit={send} />
|
|
64
|
+
```
|
|
65
|
+
|
|
53
66
|
- **Accessible by default:** group questions are a `fieldset` with a `legend`, and rating, NPS and
|
|
54
67
|
yes/no are real radio groups (arrows select). Every control carries `aria-invalid`,
|
|
55
68
|
errors are linked with `aria-describedby`, and the NPS end labels are read with 0 and 10. In
|
|
@@ -57,6 +70,18 @@ import { Form } from "@marktiderman/genesis-forms-web";
|
|
|
57
70
|
question text), a failed Next is announced once through a live region, and the ending heading
|
|
58
71
|
takes focus. Choice rows are full-width labels at least 44px tall; motion respects
|
|
59
72
|
`prefers-reduced-motion`. Ids are unique per form, so two forms can share a page.
|
|
73
|
+
- **Send word and required note:** the send button says `submitLabel` (default **Send**) in every
|
|
74
|
+
layout. When any question is required, the title shows "\* Required" (`requiredLabel`, default
|
|
75
|
+
"Required"; `null` hides it).
|
|
76
|
+
- **Theme:** `theme={{ accent: "#f97316", headerImage: url, headerImageAlt }}` sets the form's own
|
|
77
|
+
primary colour (buttons, selected options, stars, progress, focus rings) and a banner over the
|
|
78
|
+
title. The accent is adjusted only as far as contrast needs: at least 3:1 against the page in light
|
|
79
|
+
and dark, and text on it at least 4.5:1. It takes `#rgb`, `#rrggbb` or `rgb()`; anything else is
|
|
80
|
+
ignored. `themeColors(accent)` returns the colours it would use.
|
|
81
|
+
- **Question types:** besides text, email, phone, rating, NPS, choices and yes/no, there are
|
|
82
|
+
`dropdown` (a native select), `number` (min, max, step; the decimal keypad on phones), `date` (the
|
|
83
|
+
browser's date picker) and `consent` (one checkbox). A rating can show end labels (`lowLabel`,
|
|
84
|
+
`highLabel`) and `display: "stars"`; NPS takes the same labels.
|
|
60
85
|
- **Submitting:** the ending shows once `onSubmit` resolves. If it rejects, the completed response
|
|
61
86
|
stays on screen read-only (a completed response is final), Back is hidden, and "Try again"
|
|
62
87
|
(`retryLabel`) resends the same answers.
|
|
@@ -96,6 +121,31 @@ choice, yes/no, and the structured types:
|
|
|
96
121
|
|
|
97
122
|
`useMediaTime()` and `useSourceText()` read them in a custom renderer.
|
|
98
123
|
|
|
124
|
+
**`photo`** (a picture of broken equipment) has "Choose a photo" and, on a phone or tablet, "Take a
|
|
125
|
+
photo" (the camera), checks each file's type, size and the count before uploading, shows a thumbnail
|
|
126
|
+
(or the file's name when the browser can't draw it, such as HEIC) and removes one. The file goes to
|
|
127
|
+
the host, which stores it and resolves to its reference; the answer keeps only that:
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
<Form
|
|
131
|
+
form={feedback}
|
|
132
|
+
onUpload={async (file, field) => {
|
|
133
|
+
const { ref } = await uploadSomewhere(file, field.id); // your storage
|
|
134
|
+
return ref; // { id, name, type, size }
|
|
135
|
+
}}
|
|
136
|
+
onSubmit={send}
|
|
137
|
+
/>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
A rejected upload shows under the field. While a photo is uploading the form neither moves on nor
|
|
141
|
+
sends (it says "Wait for the photo to finish uploading."). Without `onUpload`, or an
|
|
142
|
+
`UploadContext` around the form, a photo field says photos can't be added here. `photoFileProblem`,
|
|
143
|
+
`photoFileType` and `photoAccept` are the renderer's own checks, for a custom one.
|
|
144
|
+
|
|
145
|
+
A **`calculated`** field shows the value the engine worked out from other answers, read-only, in an
|
|
146
|
+
`<output>` named by the question that announces changes politely. It follows the field's `format`
|
|
147
|
+
and `decimals`, and shows a dash until there is a value.
|
|
148
|
+
|
|
99
149
|
## License
|
|
100
150
|
|
|
101
151
|
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ReactNode, ComponentType } from 'react';
|
|
3
|
-
import { FormDefinition, FormEngine, CreateFormOptions, FormState } from '@marktiderman/genesis-forms';
|
|
3
|
+
import { FormDefinition, FormEngine, CreateFormOptions, HostOptions, FormState, FormProgress, FieldBlock, PhotoRef } from '@marktiderman/genesis-forms';
|
|
4
4
|
import * as _marktiderman_genesis_forms_react from '@marktiderman/genesis-forms-react';
|
|
5
5
|
import { RendererRegistry, FieldRendererProps, RendererEntry } from '@marktiderman/genesis-forms-react';
|
|
6
6
|
|
|
@@ -21,10 +21,56 @@ interface FormPresentationValue {
|
|
|
21
21
|
*/
|
|
22
22
|
declare function useFormPresentation(): FormPresentationValue;
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* A form's own look: an accent colour and a header image. The accent becomes
|
|
26
|
+
* the Genesis `--primary` (and `--ring`) inside the form, so every primary
|
|
27
|
+
* surface follows it: buttons, selected options, stars, progress, focus rings.
|
|
28
|
+
*/
|
|
29
|
+
interface FormTheme {
|
|
30
|
+
/**
|
|
31
|
+
* The accent colour, as `#rgb`, `#rrggbb` or `rgb(r, g, b)`, such as a
|
|
32
|
+
* brand's colour. It is adjusted only as far as needed for contrast: at
|
|
33
|
+
* least 3:1 against the page (light and dark), with text on it at least
|
|
34
|
+
* 4.5:1 (white or near-black, whichever reads better). Anything else is ignored.
|
|
35
|
+
*/
|
|
36
|
+
accent?: string | undefined;
|
|
37
|
+
/** A banner image URL shown over the form's title. */
|
|
38
|
+
headerImage?: string | undefined;
|
|
39
|
+
/** The banner's text alternative. Default "" (decorative). */
|
|
40
|
+
headerImageAlt?: string | undefined;
|
|
41
|
+
}
|
|
42
|
+
/** Shadcn HSL components (`"221 83% 53%"`), as Genesis tokens hold them. */
|
|
43
|
+
interface ThemeColors {
|
|
44
|
+
primary: string;
|
|
45
|
+
primaryForeground: string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The accent as Genesis primary colours for a light and a dark page, each
|
|
49
|
+
* adjusted only as far as contrast needs. Null when `accent` is not a colour
|
|
50
|
+
* this understands.
|
|
51
|
+
*/
|
|
52
|
+
declare function themeColors(accent: string): {
|
|
53
|
+
light: ThemeColors;
|
|
54
|
+
dark: ThemeColors;
|
|
55
|
+
} | null;
|
|
56
|
+
/** The CSS that applies `theme` to the elements matching `selector`, or "" when it changes no colour. */
|
|
57
|
+
declare function themeCss(theme: FormTheme | undefined, selector: string): string;
|
|
58
|
+
|
|
24
59
|
type SlotOverride<P> = ComponentType<P> | null;
|
|
25
60
|
interface HeaderSlotProps {
|
|
26
61
|
title: string;
|
|
27
62
|
description?: string;
|
|
63
|
+
/** `theme.headerImage`: a banner over the title. */
|
|
64
|
+
image?: {
|
|
65
|
+
src: string;
|
|
66
|
+
alt: string;
|
|
67
|
+
} | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* The note that marks required questions ("Required", drawn as "* Required"),
|
|
70
|
+
* set when the form has at least one required question. Empty when it has none
|
|
71
|
+
* or `requiredLabel` is null.
|
|
72
|
+
*/
|
|
73
|
+
requiredNote?: string | undefined;
|
|
28
74
|
className?: string;
|
|
29
75
|
}
|
|
30
76
|
interface BrandSlotProps {
|
|
@@ -100,6 +146,16 @@ interface FormProps {
|
|
|
100
146
|
/** Bring your own engine (from `useForm`) to read state outside the form. `form` and `options` are then unused. */
|
|
101
147
|
engine?: FormEngine | undefined;
|
|
102
148
|
options?: CreateFormOptions | undefined;
|
|
149
|
+
/**
|
|
150
|
+
* Option lists this app supplies, by key, for choice questions whose
|
|
151
|
+
* options come from the app (`props.optionsFrom: { source: "host", key }`),
|
|
152
|
+
* such as `{ coaches: [{ value: "8c1…", label: "Ada Lovelace" }] }`. Shown
|
|
153
|
+
* after the question's own options. Like `options`, read when the response
|
|
154
|
+
* starts; unused with `engine` (pass `hostOptions` to `useForm` then). Check
|
|
155
|
+
* answers on the server with the same lists (`validateSubmission`'s
|
|
156
|
+
* `hostOptions`).
|
|
157
|
+
*/
|
|
158
|
+
hostOptions?: HostOptions | undefined;
|
|
103
159
|
/** Overrides the definition's layout. */
|
|
104
160
|
layout?: "focus" | "classic" | undefined;
|
|
105
161
|
/**
|
|
@@ -112,6 +168,13 @@ interface FormProps {
|
|
|
112
168
|
presentation?: FormPresentation | undefined;
|
|
113
169
|
/** An organisation's or business's name and logo, shown small at the top of every step and on the ending. */
|
|
114
170
|
brand?: FormBrand | undefined;
|
|
171
|
+
/**
|
|
172
|
+
* Colours and a header image: `accent` replaces the primary colour (buttons,
|
|
173
|
+
* selected options, progress, focus ring), adjusted where needed so it keeps
|
|
174
|
+
* at least 3:1 contrast with the page and its text 4.5:1; `headerImage` is a
|
|
175
|
+
* banner over the title.
|
|
176
|
+
*/
|
|
177
|
+
theme?: FormTheme | undefined;
|
|
115
178
|
registry?: RendererRegistry | undefined;
|
|
116
179
|
/**
|
|
117
180
|
* Called once the form completes. The ending shows only after it resolves.
|
|
@@ -124,16 +187,47 @@ interface FormProps {
|
|
|
124
187
|
* submit attempt (debounce it to save drafts).
|
|
125
188
|
*/
|
|
126
189
|
onChange?: ((state: FormState, engine: FormEngine) => void) | undefined;
|
|
190
|
+
/**
|
|
191
|
+
* Save answers as they go: with the definition's `settings.saveProgress` on,
|
|
192
|
+
* called a moment after each step forward (in classic layout, a moment after
|
|
193
|
+
* the answers change) with how far the respondent got (`formProgress`); read
|
|
194
|
+
* the answers so far with `engine.answers()`. It never holds them up: it runs
|
|
195
|
+
* after the step, and a rejection is ignored (the next call carries every
|
|
196
|
+
* answer again). A pending call goes at once when the page is hidden, and none
|
|
197
|
+
* is made once the form is submitted.
|
|
198
|
+
*/
|
|
199
|
+
onProgress?: ((engine: FormEngine, progress: FormProgress) => void | Promise<void>) | undefined;
|
|
200
|
+
/**
|
|
201
|
+
* The note under the first question while answers save as they go (only with
|
|
202
|
+
* `onProgress` and `settings.saveProgress`). `null` hides it. Default
|
|
203
|
+
* "Your answers save as you go."
|
|
204
|
+
*/
|
|
205
|
+
progressNotice?: string | null | undefined;
|
|
127
206
|
/**
|
|
128
207
|
* In focus layout, move on after a single pick (rating, yes/no, one choice).
|
|
129
208
|
* Arrow keys only move through the options; a click, tap or Space picks.
|
|
130
209
|
* Never moves on from the last question. Default true.
|
|
131
210
|
*/
|
|
132
211
|
autoAdvance?: boolean | undefined;
|
|
212
|
+
/** The send button's word, the same in every layout. Default "Send". */
|
|
133
213
|
submitLabel?: string | undefined;
|
|
214
|
+
/** The button that moves to the next question in focus layout. Default "OK". */
|
|
134
215
|
nextLabel?: string | undefined;
|
|
216
|
+
/**
|
|
217
|
+
* The note shown with the title when any question is required, after an
|
|
218
|
+
* asterisk ("* Required"). `null` hides it. Default "Required".
|
|
219
|
+
*/
|
|
220
|
+
requiredLabel?: string | null | undefined;
|
|
135
221
|
/** The submit button's label after `onSubmit` failed. Default "Try again". */
|
|
136
222
|
retryLabel?: string | undefined;
|
|
223
|
+
/**
|
|
224
|
+
* Stores a file a respondent adds to a `photo` field and resolves to its
|
|
225
|
+
* reference (`{ id, name, type, size }`), which is all the answer keeps; a
|
|
226
|
+
* rejection shows under the field. While any upload is in flight the form
|
|
227
|
+
* neither moves on nor sends. Without it (and without an `UploadContext`
|
|
228
|
+
* above the form), photo fields say photos can't be added.
|
|
229
|
+
*/
|
|
230
|
+
onUpload?: ((file: File, field: FieldBlock) => Promise<PhotoRef>) | undefined;
|
|
137
231
|
slots?: {
|
|
138
232
|
[K in keyof FormSlotMap]?: SlotOverride<FormSlotMap[K]>;
|
|
139
233
|
} | undefined;
|
|
@@ -143,6 +237,10 @@ interface FormProps {
|
|
|
143
237
|
className?: string | undefined;
|
|
144
238
|
testID?: string | undefined;
|
|
145
239
|
}
|
|
240
|
+
/** How long after a step forward `onProgress` waits, so quick steps save once. */
|
|
241
|
+
declare const PROGRESS_DELAY_MS = 400;
|
|
242
|
+
/** In classic layout, how long after the last answer changed. */
|
|
243
|
+
declare const CLASSIC_PROGRESS_DELAY_MS = 1500;
|
|
146
244
|
declare function Form(props: FormProps): react.JSX.Element;
|
|
147
245
|
|
|
148
246
|
interface FieldChromeProps {
|
|
@@ -173,6 +271,35 @@ declare const builtInRenderers: RendererEntry[];
|
|
|
173
271
|
*/
|
|
174
272
|
declare const defaultRegistry: _marktiderman_genesis_forms_react.RendererRegistry;
|
|
175
273
|
|
|
274
|
+
/**
|
|
275
|
+
* Where a form's files go. The host stores each file (its own bucket, its own
|
|
276
|
+
* rules) and resolves to a reference, which is all the answer keeps; a
|
|
277
|
+
* rejection is shown under the field. `photo` fields read it.
|
|
278
|
+
*/
|
|
279
|
+
interface UploadHost {
|
|
280
|
+
upload(file: File, field: FieldBlock): Promise<PhotoRef>;
|
|
281
|
+
}
|
|
282
|
+
/** Provide it around `<Form>` (or pass `onUpload`) so `photo` fields can store files. */
|
|
283
|
+
declare const UploadContext: react.Context<UploadHost | null>;
|
|
284
|
+
/** The enclosing `UploadContext` value, or null when no host stores files. */
|
|
285
|
+
declare function useUpload(): UploadHost | null;
|
|
286
|
+
/** Said when the respondent tries to move on or send while a photo is still uploading. */
|
|
287
|
+
declare const UPLOAD_WAIT_MESSAGE = "Wait for the photo to finish uploading.";
|
|
288
|
+
|
|
289
|
+
/** The file's type as the field judges it: some browsers give a HEIC photo no type, so its name decides. */
|
|
290
|
+
declare function photoFileType(file: {
|
|
291
|
+
name: string;
|
|
292
|
+
type: string;
|
|
293
|
+
}): string;
|
|
294
|
+
/** Why a picked file can't be added, or null when it can. */
|
|
295
|
+
declare function photoFileProblem(field: FieldBlock, file: {
|
|
296
|
+
name: string;
|
|
297
|
+
type: string;
|
|
298
|
+
size: number;
|
|
299
|
+
}): string | null;
|
|
300
|
+
/** The `accept` attribute for a field: its MIME types, plus HEIC's file names for browsers that give it no type. */
|
|
301
|
+
declare function photoAccept(field: FieldBlock): string;
|
|
302
|
+
|
|
176
303
|
/**
|
|
177
304
|
* The video or audio a form is about, as the host app plays it. `media-notes`
|
|
178
305
|
* fields read it: with `currentTime` the add row starts at the current moment
|
|
@@ -198,4 +325,4 @@ declare const SourceTextContext: react.Context<string | null>;
|
|
|
198
325
|
/** The enclosing `SourceTextContext` value, or null. */
|
|
199
326
|
declare function useSourceText(): string | null;
|
|
200
327
|
|
|
201
|
-
export { type BrandSlotProps, type EndingSlotProps, type ErrorSummaryItem, type ErrorSummarySlotProps, FieldChrome, type FieldChromeProps, Form, type FormBrand, type FormPresentation, type FormPresentationValue, type FormProps, type FormSlotMap, type HeaderSlotProps, type MediaTime, MediaTimeContext, type NavigationSlotProps, type ProgressSlotProps, SourceTextContext, builtInRenderers, defaultRegistry, describedBy, useFormPresentation, useMediaTime, useSourceText };
|
|
328
|
+
export { type BrandSlotProps, CLASSIC_PROGRESS_DELAY_MS, type EndingSlotProps, type ErrorSummaryItem, type ErrorSummarySlotProps, FieldChrome, type FieldChromeProps, Form, type FormBrand, type FormPresentation, type FormPresentationValue, type FormProps, type FormSlotMap, type FormTheme, type HeaderSlotProps, type MediaTime, MediaTimeContext, type NavigationSlotProps, PROGRESS_DELAY_MS, type ProgressSlotProps, SourceTextContext, type ThemeColors, UPLOAD_WAIT_MESSAGE, UploadContext, type UploadHost, builtInRenderers, defaultRegistry, describedBy, photoAccept, photoFileProblem, photoFileType, themeColors, themeCss, useFormPresentation, useMediaTime, useSourceText, useUpload };
|