@marktiderman/genesis-forms-web 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 CHANGED
@@ -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
@@ -108,6 +121,27 @@ choice, yes/no, and the structured types:
108
121
 
109
122
  `useMediaTime()` and `useSourceText()` read them in a custom renderer.
110
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
+
111
145
  A **`calculated`** field shows the value the engine worked out from other answers, read-only, in an
112
146
  `<output>` named by the question that announces changes politely. It follows the field's `format`
113
147
  and `decimals`, and shows a dash until there is a value.
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
 
@@ -146,6 +146,16 @@ interface FormProps {
146
146
  /** Bring your own engine (from `useForm`) to read state outside the form. `form` and `options` are then unused. */
147
147
  engine?: FormEngine | undefined;
148
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;
149
159
  /** Overrides the definition's layout. */
150
160
  layout?: "focus" | "classic" | undefined;
151
161
  /**
@@ -177,6 +187,22 @@ interface FormProps {
177
187
  * submit attempt (debounce it to save drafts).
178
188
  */
179
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;
180
206
  /**
181
207
  * In focus layout, move on after a single pick (rating, yes/no, one choice).
182
208
  * Arrow keys only move through the options; a click, tap or Space picks.
@@ -194,6 +220,14 @@ interface FormProps {
194
220
  requiredLabel?: string | null | undefined;
195
221
  /** The submit button's label after `onSubmit` failed. Default "Try again". */
196
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;
197
231
  slots?: {
198
232
  [K in keyof FormSlotMap]?: SlotOverride<FormSlotMap[K]>;
199
233
  } | undefined;
@@ -203,6 +237,10 @@ interface FormProps {
203
237
  className?: string | undefined;
204
238
  testID?: string | undefined;
205
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;
206
244
  declare function Form(props: FormProps): react.JSX.Element;
207
245
 
208
246
  interface FieldChromeProps {
@@ -233,6 +271,35 @@ declare const builtInRenderers: RendererEntry[];
233
271
  */
234
272
  declare const defaultRegistry: _marktiderman_genesis_forms_react.RendererRegistry;
235
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
+
236
303
  /**
237
304
  * The video or audio a form is about, as the host app plays it. `media-notes`
238
305
  * fields read it: with `currentTime` the add row starts at the current moment
@@ -258,4 +325,4 @@ declare const SourceTextContext: react.Context<string | null>;
258
325
  /** The enclosing `SourceTextContext` value, or null. */
259
326
  declare function useSourceText(): string | null;
260
327
 
261
- export { type BrandSlotProps, 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, type ProgressSlotProps, SourceTextContext, type ThemeColors, builtInRenderers, defaultRegistry, describedBy, themeColors, themeCss, 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 };