@marktiderman/genesis-forms-web 0.6.1 → 0.6.2

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/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ import { ReactNode, ComponentType } from 'react';
3
3
  import { FormDefinition, FormEngine, CreateFormOptions, HostOptions, LinkParams, FormState, FormProgress, FieldBlock, PhotoRef, SignedUpload } 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
+ export { CLASSIC_PROGRESS_DELAY_MS, PROGRESS_DELAY_MS } from '@marktiderman/genesis-forms-react';
6
7
 
7
8
  /**
8
9
  * How `<Form>` sits on the page. `"embedded"` is a block inside your app's own
@@ -257,10 +258,7 @@ interface FormProps {
257
258
  className?: string | undefined;
258
259
  testID?: string | undefined;
259
260
  }
260
- /** How long after a step forward `onProgress` waits, so quick steps save once. */
261
- declare const PROGRESS_DELAY_MS = 400;
262
- /** In classic layout, how long after the last answer changed. */
263
- declare const CLASSIC_PROGRESS_DELAY_MS = 1500;
261
+
264
262
  declare function Form(props: FormProps): react.JSX.Element;
265
263
 
266
264
  interface FieldChromeProps {
@@ -414,4 +412,4 @@ declare function hideQueryOnLoad(win: HiddenQueryWindow, isReload: boolean, now?
414
412
  /** Whether this page load was a reload (Navigation Timing), in the browser; false elsewhere. */
415
413
  declare const wasReload: () => boolean;
416
414
 
417
- export { type BrandSlotProps, CLASSIC_PROGRESS_DELAY_MS, DefaultEnding, type EndingSlotProps, type ErrorSummaryItem, type ErrorSummarySlotProps, FieldChrome, type FieldChromeProps, Form, type FormBrand, type FormPresentation, type FormPresentationValue, type FormProps, type FormSlotMap, type FormTheme, HIDDEN_QUERY_KEEP_MS, type HeaderSlotProps, type HiddenQueryWindow, type MediaTime, MediaTimeContext, type NavigationSlotProps, PROGRESS_DELAY_MS, type ProgressSlotProps, type SignedUploadHost, type SignedUploadHostOptions, SourceTextContext, type ThemeColors, UPLOAD_WAIT_MESSAGE, type UploadClaim, UploadContext, type UploadHost, builtInRenderers, defaultRegistry, describedBy, hideQueryOnLoad, photoAccept, photoFileProblem, photoFileType, signedUploadHost, themeColors, themeCss, useFormPresentation, useMediaTime, useSourceText, useUpload, wasReload };
415
+ export { type BrandSlotProps, DefaultEnding, type EndingSlotProps, type ErrorSummaryItem, type ErrorSummarySlotProps, FieldChrome, type FieldChromeProps, Form, type FormBrand, type FormPresentation, type FormPresentationValue, type FormProps, type FormSlotMap, type FormTheme, HIDDEN_QUERY_KEEP_MS, type HeaderSlotProps, type HiddenQueryWindow, type MediaTime, MediaTimeContext, type NavigationSlotProps, type ProgressSlotProps, type SignedUploadHost, type SignedUploadHostOptions, SourceTextContext, type ThemeColors, UPLOAD_WAIT_MESSAGE, type UploadClaim, UploadContext, type UploadHost, builtInRenderers, defaultRegistry, describedBy, hideQueryOnLoad, photoAccept, photoFileProblem, photoFileType, signedUploadHost, themeColors, themeCss, useFormPresentation, useMediaTime, useSourceText, useUpload, wasReload };
package/dist/index.js CHANGED
@@ -1,10 +1,11 @@
1
1
  "use client";
2
- import { createContext, useState, useEffect, useRef, useId, useContext, useMemo, useLayoutEffect } from 'react';
2
+ import { createContext, useState, useEffect, useRef, useId, useContext, useMemo } from 'react';
3
3
  import { Button } from '@marktiderman/genesis-ui/button';
4
4
  import { Progress } from '@marktiderman/genesis-ui/progress';
5
5
  import { cn } from '@marktiderman/genesis-ui/utils';
6
- import { ratingDisplay, numberLimits, dateLimits, listLimits, tableColumns, tableLimits, parseTimestamp, formatTimestamp, formatCalculated, photoLimits, photoRefs, getFieldType, scaleLabels, isSearchable, otherOption, choiceOptions, searchOptions, OTHER_MAX_LENGTH, photoCountMessage, isPhotoRef, photoTypeMessage, photoSizeMessage, respondentPath, formProgress } from '@marktiderman/genesis-forms';
7
- import { createRendererRegistry, useFormSelector, useChoiceOptions, useForm, FormRuntime, useFormContext, FieldRenderer } from '@marktiderman/genesis-forms-react';
6
+ import { ratingDisplay, numberLimits, dateLimits, listLimits, tableColumns, tableLimits, parseTimestamp, formatTimestamp, formatCalculated, photoLimits, photoRefs, getFieldType, scaleLabels, isSearchable, otherOption, choiceOptions, searchOptions, OTHER_MAX_LENGTH, photoCountMessage, isPhotoRef, photoTypeMessage, photoSizeMessage, contentText } from '@marktiderman/genesis-forms';
7
+ import { createRendererRegistry, useFormSelector, useChoiceOptions, formEngineOptions, useForm, FormRuntime, useFormContext, useFormController, FieldRenderer } from '@marktiderman/genesis-forms-react';
8
+ export { CLASSIC_PROGRESS_DELAY_MS, PROGRESS_DELAY_MS } from '@marktiderman/genesis-forms-react';
8
9
  import { Checkbox } from '@marktiderman/genesis-ui/checkbox';
9
10
  import { Input } from '@marktiderman/genesis-ui/input';
10
11
  import { Label } from '@marktiderman/genesis-ui/label';
@@ -2208,9 +2209,6 @@ function DefaultErrorSummary({ count, errors, className }) {
2208
2209
  )
2209
2210
  );
2210
2211
  }
2211
- var ADVANCE_DELAY_MS = 250;
2212
- var PROGRESS_DELAY_MS = 400;
2213
- var CLASSIC_PROGRESS_DELAY_MS = 1500;
2214
2212
  var engineKeys = /* @__PURE__ */ new WeakMap();
2215
2213
  var nextEngineKey = 0;
2216
2214
  function keyOf(engine) {
@@ -2225,11 +2223,7 @@ function Form(props) {
2225
2223
  return props.engine ? /* @__PURE__ */ jsx(FormWithEngine, { ...props, engine: props.engine }) : /* @__PURE__ */ jsx(OwnedForm, { ...props });
2226
2224
  }
2227
2225
  function OwnedForm(props) {
2228
- const options = props.hostOptions || props.linkParams ? {
2229
- ...props.options,
2230
- ...props.hostOptions ? { hostOptions: props.hostOptions } : {},
2231
- ...props.linkParams ? { linkParams: props.linkParams } : {}
2232
- } : props.options;
2226
+ const options = formEngineOptions(props.options, { hostOptions: props.hostOptions, linkParams: props.linkParams });
2233
2227
  const engine = useForm(props.form, options);
2234
2228
  return /* @__PURE__ */ jsx(FormWithEngine, { ...props, engine });
2235
2229
  }
@@ -2269,157 +2263,58 @@ function FormBody({
2269
2263
  }) {
2270
2264
  const { idPrefix } = useFormContext();
2271
2265
  const def = engine.definition;
2272
- const layout = layoutProp ?? def.layout?.mode ?? "classic";
2273
- const status = useFormSelector(engine, (s) => s.status);
2274
- const current = useFormSelector(engine, (s) => s.current);
2275
- const pathKey = useFormSelector(engine, (s) => JSON.stringify(respondentPath(def, s)));
2276
- const errorsKey = useFormSelector(engine, (s) => JSON.stringify(s.errors));
2277
- const ending = useFormSelector(engine, (s) => s.ending);
2278
- const historyLength = useFormSelector(engine, (s) => s.history.length);
2279
- const description = useFormSelector(engine, () => def.description ? engine.text(def.description) : void 0);
2280
- const path = JSON.parse(pathKey);
2281
- const errors = JSON.parse(errorsKey);
2282
2266
  const formRef = useRef(null);
2283
2267
  const stepRef = useRef(null);
2284
2268
  const endingRef = useRef(null);
2285
- const advanceTimer = useRef(void 0);
2286
- const previousCurrent = useRef(current);
2287
- const [phase, setPhase] = useState("idle");
2288
- const [announcement, setAnnouncement] = useState("");
2289
- const [summaryRequest, setSummaryRequest] = useState(0);
2290
- const sending = useRef(false);
2291
- const direction = useRef("forward");
2292
2269
  const [modKey, setModKey] = useState("Ctrl");
2293
2270
  const page = presentation === "page";
2271
+ const focus = (target) => {
2272
+ if (target.kind === "step") focusWithin(stepRef.current);
2273
+ else if (target.kind === "ending") focusWithin(endingRef.current);
2274
+ else if (target.kind === "errorSummary") {
2275
+ const form = formRef.current;
2276
+ const summary = form?.querySelector("[data-gf-error-summary]");
2277
+ if (summary) summary.focus();
2278
+ else form?.querySelector('[aria-invalid="true"]')?.focus();
2279
+ } else {
2280
+ const form = formRef.current;
2281
+ if (form && !form.contains(document.activeElement)) form.querySelector('[type="submit"]')?.focus();
2282
+ }
2283
+ };
2284
+ const controller = useFormController(engine, {
2285
+ layout: layoutProp,
2286
+ onSubmit,
2287
+ onChange,
2288
+ onProgress,
2289
+ autoAdvance,
2290
+ waitFor: () => pendingUploads.count() === 0 ? null : UPLOAD_WAIT_MESSAGE,
2291
+ focus
2292
+ });
2293
+ const {
2294
+ layout,
2295
+ status,
2296
+ current,
2297
+ path,
2298
+ historyLength,
2299
+ description,
2300
+ phase,
2301
+ announcement,
2302
+ showEnding,
2303
+ locked,
2304
+ saveProgress
2305
+ } = controller;
2294
2306
  useEffect(() => {
2295
2307
  if (/Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent)) setModKey("\u2318");
2296
2308
  }, []);
2297
- const showEnding = status === "completed" && (phase === "sent" || !onSubmit);
2298
- const locked = status === "completed";
2299
- useEffect(() => {
2300
- if (!onChange) return;
2301
- return engine.subscribe(() => onChange(engine.getState(), engine));
2302
- }, [engine, onChange]);
2303
- const saveProgress = def.settings?.saveProgress === true && !!onProgress;
2304
- const onProgressRef = useRef(onProgress);
2305
- onProgressRef.current = onProgress;
2309
+ const { flushProgress } = controller;
2306
2310
  useEffect(() => {
2307
2311
  if (!saveProgress) return;
2308
- let timer;
2309
- let last = engine.getState();
2310
- const fire = () => {
2311
- timer = void 0;
2312
- if (engine.getState().status === "completed") return;
2313
- try {
2314
- void Promise.resolve(onProgressRef.current?.(engine, formProgress(engine))).catch(() => {
2315
- });
2316
- } catch {
2317
- }
2318
- };
2319
- const schedule = (delay) => {
2320
- clearTimeout(timer);
2321
- timer = setTimeout(fire, delay);
2322
- };
2323
- const unsubscribe = engine.subscribe(() => {
2324
- const state = engine.getState();
2325
- const previous = last;
2326
- last = state;
2327
- if (state.status === "completed") {
2328
- clearTimeout(timer);
2329
- timer = void 0;
2330
- return;
2331
- }
2332
- const event = state.events[state.events.length - 1];
2333
- if (layout === "focus") {
2334
- if (event?.type === "next" && state.current !== previous.current) schedule(PROGRESS_DELAY_MS);
2335
- } else if (event?.type === "answer") schedule(CLASSIC_PROGRESS_DELAY_MS);
2336
- });
2337
- const flush = () => {
2338
- if (timer === void 0) return;
2339
- clearTimeout(timer);
2340
- fire();
2341
- };
2342
- window.addEventListener("pagehide", flush);
2343
- return () => {
2344
- unsubscribe();
2345
- window.removeEventListener("pagehide", flush);
2346
- flush();
2347
- };
2348
- }, [engine, saveProgress, layout]);
2349
- useLayoutEffect(() => {
2350
- if (previousCurrent.current === current) return;
2351
- previousCurrent.current = current;
2352
- clearTimeout(advanceTimer.current);
2353
- if (layout === "focus") focusWithin(stepRef.current);
2354
- }, [current, layout]);
2355
- useLayoutEffect(() => {
2356
- if (showEnding) focusWithin(endingRef.current);
2357
- }, [showEnding]);
2358
- useLayoutEffect(() => {
2359
- if (!summaryRequest) return;
2360
- const form = formRef.current;
2361
- const summary = form?.querySelector("[data-gf-error-summary]");
2362
- if (summary) summary.focus();
2363
- else form?.querySelector('[aria-invalid="true"]')?.focus();
2364
- }, [summaryRequest]);
2365
- useLayoutEffect(() => {
2366
- if (phase !== "failed") return;
2367
- const form = formRef.current;
2368
- if (form && !form.contains(document.activeElement)) form.querySelector('[type="submit"]')?.focus();
2369
- }, [phase]);
2370
- useEffect(() => () => clearTimeout(advanceTimer.current), []);
2371
- const announce = (message) => setAnnouncement((previous) => previous === message ? `${message}\xA0` : message);
2372
- const announceStepError = () => {
2373
- const state = engine.getState();
2374
- const message = state.current ? state.errors[state.current] : void 0;
2375
- if (message) announce(message);
2376
- };
2377
- const waitingForUploads = () => {
2378
- if (pendingUploads.count() === 0) return false;
2379
- announce(UPLOAD_WAIT_MESSAGE);
2380
- return true;
2381
- };
2382
- const finish = async () => {
2383
- if (sending.current) return;
2384
- if (engine.getState().status !== "completed" && waitingForUploads()) return;
2385
- if (engine.getState().status !== "completed" && !engine.submit()) {
2386
- if (layout === "classic") setSummaryRequest((n) => n + 1);
2387
- else announceStepError();
2388
- return;
2389
- }
2390
- if (!onSubmit) {
2391
- setPhase("sent");
2392
- return;
2393
- }
2394
- sending.current = true;
2395
- setPhase("sending");
2396
- try {
2397
- await onSubmit(engine);
2398
- setPhase("sent");
2399
- } catch {
2400
- setPhase("failed");
2401
- } finally {
2402
- sending.current = false;
2403
- }
2404
- };
2405
- const isLastStep = (state) => {
2406
- const shown = respondentPath(def, state);
2407
- return state.current === shown[shown.length - 1];
2408
- };
2409
- const advance = () => {
2410
- if (waitingForUploads()) return;
2411
- direction.current = "forward";
2412
- if (isLastStep(engine.getState())) void finish();
2413
- else if (!engine.next()) announceStepError();
2414
- };
2415
- const submit = () => {
2416
- clearTimeout(advanceTimer.current);
2417
- if (layout === "focus") advance();
2418
- else void finish();
2419
- };
2312
+ window.addEventListener("pagehide", flushProgress);
2313
+ return () => window.removeEventListener("pagehide", flushProgress);
2314
+ }, [saveProgress, flushProgress]);
2420
2315
  const handleSubmit = (e) => {
2421
2316
  e.preventDefault();
2422
- submit();
2317
+ controller.submit();
2423
2318
  };
2424
2319
  const handleKeyDown = (e) => {
2425
2320
  if (e.key !== "Enter" || e.nativeEvent.isComposing) return;
@@ -2427,29 +2322,17 @@ function FormBody({
2427
2322
  if (target instanceof HTMLTextAreaElement) {
2428
2323
  if (e.metaKey || e.ctrlKey) {
2429
2324
  e.preventDefault();
2430
- submit();
2325
+ controller.submit();
2431
2326
  }
2432
2327
  return;
2433
2328
  }
2434
2329
  if (layout === "focus" && target.matches('[role="radio"], [role="checkbox"]')) {
2435
2330
  e.preventDefault();
2436
- submit();
2331
+ controller.submit();
2437
2332
  }
2438
2333
  };
2439
- const scheduleAdvance = layout === "focus" && autoAdvance ? () => {
2440
- clearTimeout(advanceTimer.current);
2441
- advanceTimer.current = setTimeout(() => {
2442
- const state = engine.getState();
2443
- if (state.status === "completed" || isLastStep(state)) return;
2444
- direction.current = "forward";
2445
- if (!engine.next()) announceStepError();
2446
- }, ADVANCE_DELAY_MS);
2447
- } : void 0;
2448
- const goBack = () => {
2449
- clearTimeout(advanceTimer.current);
2450
- direction.current = "back";
2451
- engine.back();
2452
- };
2334
+ const scheduleAdvance = controller.onAdvance;
2335
+ const goBack = controller.back;
2453
2336
  const liveRegion = /* @__PURE__ */ jsx("div", { role: "status", "aria-live": "polite", "aria-atomic": "true", className: "sr-only", "data-gf-live": "", children: announcement });
2454
2337
  const presentationValue = { presentation, layout };
2455
2338
  const themeId = `gf-theme-${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
@@ -2458,7 +2341,7 @@ function FormBody({
2458
2341
  const styleNode = themeStyle ? /* @__PURE__ */ jsx("style", { children: themeStyle }) : null;
2459
2342
  const brandNode = brand ? renderSlot(DefaultBrand, slots?.brand, { name: brand.name, logo: brand.logo }, slotProps?.brand) : null;
2460
2343
  if (showEnding) {
2461
- const end = def.endings?.find((x) => x.id === ending) ?? def.endings?.[0];
2344
+ const end = controller.ending;
2462
2345
  const endingNode = renderSlot(
2463
2346
  DefaultEnding,
2464
2347
  slots?.ending,
@@ -2493,11 +2376,11 @@ function FormBody({
2493
2376
  const pageFocus = page && layout === "focus";
2494
2377
  const pageClassic = page && layout === "classic";
2495
2378
  const notice = saveProgress && progressNotice && historyLength === 0 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", "data-gf-progress-notice": "", children: progressNotice }) : null;
2496
- const renderBlock = (id, focus) => {
2379
+ const renderBlock = (id, focus2) => {
2497
2380
  const block = engine.block(id);
2498
2381
  if (!block || block.kind === "page-break") return null;
2499
2382
  if (block.kind === "content") {
2500
- const focusable = focus ? { tabIndex: -1, "data-gf-question": "" } : {};
2383
+ const focusable = focus2 ? { tabIndex: -1, "data-gf-question": "" } : {};
2501
2384
  const content = block.type === "heading" ? /* @__PURE__ */ jsx(
2502
2385
  "h2",
2503
2386
  {
@@ -2507,7 +2390,7 @@ function FormBody({
2507
2390
  focusRing
2508
2391
  ),
2509
2392
  ...focusable,
2510
- children: engine.text(block.text)
2393
+ children: engine.text(contentText(block))
2511
2394
  },
2512
2395
  id
2513
2396
  ) : /* @__PURE__ */ jsx(
@@ -2515,23 +2398,17 @@ function FormBody({
2515
2398
  {
2516
2399
  className: cn("rounded-sm text-muted-foreground", pageFocus && "text-lg sm:text-xl text-pretty", focusRing),
2517
2400
  ...focusable,
2518
- children: engine.text(block.text)
2401
+ children: engine.text(contentText(block))
2519
2402
  },
2520
2403
  id
2521
2404
  );
2522
2405
  return pageClassic ? /* @__PURE__ */ jsx("section", { className: classicSection, children: content }, id) : content;
2523
2406
  }
2524
- const field = /* @__PURE__ */ jsx(FieldRenderer, { id, onAdvance: focus ? scheduleAdvance : void 0 });
2407
+ const field = /* @__PURE__ */ jsx(FieldRenderer, { id, onAdvance: focus2 ? scheduleAdvance : void 0 });
2525
2408
  return /* @__PURE__ */ jsx("div", { "data-block": id, className: pageClassic ? classicSection : void 0, children: field }, id);
2526
2409
  };
2527
- const step = current ? Math.max(1, path.indexOf(current) + 1) : 1;
2528
- const isLast = layout === "classic" || current === path[path.length - 1];
2529
- const errorItems = path.filter((id) => errors[id]).map((id) => ({
2530
- id,
2531
- label: engine.text(engine.field(id)?.label ?? id),
2532
- message: errors[id],
2533
- href: `#${idPrefix}-${id}`
2534
- }));
2410
+ const { step, isLast } = controller;
2411
+ const errorItems = controller.errorList.map((e) => ({ ...e, href: `#${idPrefix}-${e.id}` }));
2535
2412
  const currentField = pageFocus && current ? engine.field(current) : void 0;
2536
2413
  const keyHint = currentField ? currentField.type === "long-text" ? `${modKey} + Enter` : "Enter \u21B5" : void 0;
2537
2414
  const hasRequired = def.blocks.some((b) => b.kind === "field" && b.required === true);
@@ -2570,7 +2447,7 @@ function FormBody({
2570
2447
  className: cn(
2571
2448
  "outline-none",
2572
2449
  pageFocus && "transition-[opacity,translate] duration-300 ease-out starting:opacity-0 motion-reduce:transition-none",
2573
- pageFocus && (direction.current === "back" ? "starting:-translate-y-6" : "starting:translate-y-6")
2450
+ pageFocus && (controller.direction === "back" ? "starting:-translate-y-6" : "starting:translate-y-6")
2574
2451
  ),
2575
2452
  children: current ? renderBlock(current, true) : null
2576
2453
  },
@@ -2586,7 +2463,7 @@ function FormBody({
2586
2463
  DefaultNavigation,
2587
2464
  slots?.navigation,
2588
2465
  {
2589
- canGoBack: layout === "focus" && historyLength > 0 && !locked,
2466
+ canGoBack: controller.canGoBack,
2590
2467
  isLast: isLast || status === "completed",
2591
2468
  busy: phase === "sending",
2592
2469
  failed: phase === "failed",
@@ -2727,6 +2604,6 @@ function nativeReplaceState(history) {
2727
2604
  }
2728
2605
  var wasReload = () => typeof performance !== "undefined" && performance.getEntriesByType?.("navigation")[0]?.type === "reload";
2729
2606
 
2730
- export { CLASSIC_PROGRESS_DELAY_MS, DefaultEnding, FieldChrome, Form, HIDDEN_QUERY_KEEP_MS, MediaTimeContext, PROGRESS_DELAY_MS, SourceTextContext, UPLOAD_WAIT_MESSAGE, UploadContext, builtInRenderers, defaultRegistry, describedBy, hideQueryOnLoad, photoAccept, photoFileProblem, photoFileType, signedUploadHost, themeColors, themeCss, useFormPresentation, useMediaTime, useSourceText, useUpload, wasReload };
2607
+ export { DefaultEnding, FieldChrome, Form, HIDDEN_QUERY_KEEP_MS, MediaTimeContext, SourceTextContext, UPLOAD_WAIT_MESSAGE, UploadContext, builtInRenderers, defaultRegistry, describedBy, hideQueryOnLoad, photoAccept, photoFileProblem, photoFileType, signedUploadHost, themeColors, themeCss, useFormPresentation, useMediaTime, useSourceText, useUpload, wasReload };
2731
2608
  //# sourceMappingURL=index.js.map
2732
2609
  //# sourceMappingURL=index.js.map