@10x-media/form-builder 0.1.0-beta.13 → 0.1.0-beta.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/exports/react.d.ts +2 -1
- package/dist/react/Form.d.ts +9 -1
- package/dist/react/Form.js +3 -2
- package/dist/react/Form.js.map +1 -1
- package/dist/react/Poll.d.ts +5 -1
- package/dist/react/Poll.js +28 -13
- package/dist/react/Poll.js.map +1 -1
- package/dist/react/adapters.d.ts +35 -0
- package/dist/react/adapters.js +11 -0
- package/dist/react/adapters.js.map +1 -0
- package/dist/react/captcha/HcaptchaCaptcha.d.ts +4 -1
- package/dist/react/captcha/HcaptchaCaptcha.js +2 -2
- package/dist/react/captcha/HcaptchaCaptcha.js.map +1 -1
- package/dist/react/captcha/RecaptchaCaptcha.d.ts +4 -1
- package/dist/react/captcha/RecaptchaCaptcha.js +2 -2
- package/dist/react/captcha/RecaptchaCaptcha.js.map +1 -1
- package/dist/react/captcha/TurnstileCaptcha.d.ts +4 -1
- package/dist/react/captcha/TurnstileCaptcha.js +2 -2
- package/dist/react/captcha/TurnstileCaptcha.js.map +1 -1
- package/dist/react/captcha/useCaptchaScript.js +23 -5
- package/dist/react/captcha/useCaptchaScript.js.map +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @10x-media/form-builder
|
|
2
2
|
|
|
3
|
+
## 0.1.0-beta.14
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Add an `adapters` prop for host-owned effects on `<Form>`, `<Poll>`, and the captcha widgets. `adapters.navigate` replaces the post-submit `window.location` redirect so router-based hosts (Next.js App Router, TanStack Router, React Router) can transition in place; the plugin passes its intent (`{ replace: false }`) and never falls back to a hard navigation when the adapter is set. `adapters.voteStorage` overrides the poll voted-flag persistence (or disables it with `false` for hosts on the server-side `poll.votedCookie`). `adapters.loadScript` replaces the captcha vendor script injection for consent-gated or CSP-nonced loading, keeping the per-src cache and evict-on-failure retry. Every member is optional and defaults to the current behavior, so existing consumers are unaffected.
|
|
8
|
+
|
|
3
9
|
## 0.1.0-beta.13
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/dist/exports/react.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { firstStepId, isTerminalStepId, resolveNextStepId, stepFieldNames } from
|
|
|
20
20
|
import { FormButtonSettings, FormDocument, FormPollSettings, FormResponseSettings } from "../form/types.js";
|
|
21
21
|
import { ToFormDocumentOptions, toFormDocument } from "../form/toFormDocument.js";
|
|
22
22
|
import { PrefillOptions, valuesFromSearchParams } from "../prefill/valuesFromSearchParams.js";
|
|
23
|
+
import { FormAdapters, LoadScript, NavigateOptions, VoteStorage } from "../react/adapters.js";
|
|
23
24
|
import { CaptchaWidgetHandle } from "../react/captcha/types.js";
|
|
24
25
|
import { HCAPTCHA_SCRIPT_URL, HcaptchaCaptcha, HcaptchaCaptchaProps } from "../react/captcha/HcaptchaCaptcha.js";
|
|
25
26
|
import { RECAPTCHA_SCRIPT_URL, RecaptchaCaptcha, RecaptchaCaptchaHandle, RecaptchaCaptchaProps } from "../react/captcha/RecaptchaCaptcha.js";
|
|
@@ -64,4 +65,4 @@ import { interpolate } from "../recall/interpolate.js";
|
|
|
64
65
|
import { CAPTCHA_TOKEN_KEY, DEFAULT_HONEYPOT_FIELD } from "../spam/constants.js";
|
|
65
66
|
import { makeTranslate } from "../translations/makeTranslate.js";
|
|
66
67
|
import { formatBytes } from "../uploads/formatBytes.js";
|
|
67
|
-
export { type AggregationBucket, type BackButtonRenderProps, Backdrop, type BackdropProps, type BodyContext, type BodyConverter, type BodyConverterArgs, type BodyRender, CAPTCHA_TOKEN_KEY, COUNTRIES, type CalcDisplayConfig, type CalcExpression, type CalcResolved, type CaptchaWidgetHandle, Checkbox, type CheckboxProps, ChoiceGroup, type ChoiceGroupProps, type ChoiceOption, type ConsentStatement, type ConsentStatements, DEFAULT_HONEYPOT_FIELD, DialogSurface, type DialogSurfaceProps, type FetchResultsInput, type FetchResultsResult, type FieldAggregation, type FieldErrors, type FieldRenderer, type FieldRendererProps, FieldShell, type FieldShellProps, type FieldWidth, Form, type FormAction, type FormButtonSettings, type FormContextValue, type FormControlLabels, FormControls, type FormControlsProps, type FormDocument, type FormFieldInstance, FormFields, type FormFieldsProps, FormLayout, type FormLayoutProps, type FormPollSettings, type FormPresentation, type FormProps, type FormResponseSettings, FormResults, type FormResultsProps, type FormState, type FormStepInfo, FormSteps, type FormStepsProps, type FormSuccessResponse, type FormSuccessResult, HCAPTCHA_SCRIPT_URL, HcaptchaCaptcha, type HcaptchaCaptchaProps, Honeypot, type HoneypotProps, Input, type InputProps, type NextButtonRenderProps, Poll, type PollProps, type PrefillOptions, type PresentationOption, type PresentationRegistry, type PresentationWrapperProps, type PresentationsConfig, RECAPTCHA_SCRIPT_URL, type RecallResolver, RecaptchaCaptcha, type RecaptchaCaptchaHandle, type RecaptchaCaptchaProps, type RendererOption, type RendererRegistry, type RendererTranslate, type RenderersConfig, Select, type SelectOption, type SelectProps, type SubmissionValue, type SubmitButtonRenderProps, type SubmitFormResult, type SubmitHandler, TURNSTILE_SCRIPT_URL, Textarea, type TextareaProps, type ToFormDocumentOptions, type TranslationBundle, TurnstileCaptcha, type TurnstileCaptchaProps, US_STATES, type UploadFileInput, type UploadFileResult, type UseDismissOptions, type UseFieldResult, buildRecallResolver, bundles, clientRuntimeKeys, cn, computeCalcFields, de, defaultPresentations, defaultRenderers, defineFieldRenderer, en, evaluateCalc, evaluateCondition, fetchFormResults, fieldKey, firstStepId, formatBytes, formatCalc, formatCalcValue, interpolate, isTerminalStepId, makeTranslate, resolveNextStepId, resolvePresentations, resolveRenderers, sanitizeUrl, serializeBody, stepFieldNames, submitForm, textOfBody, toFormDocument, uploadFile, useCalcValues, useDismiss, useField, useFocusTrap, useFormContext, useFormState, useFormStep, useScrollLock, valuesFromSearchParams, widthProps };
|
|
68
|
+
export { type AggregationBucket, type BackButtonRenderProps, Backdrop, type BackdropProps, type BodyContext, type BodyConverter, type BodyConverterArgs, type BodyRender, CAPTCHA_TOKEN_KEY, COUNTRIES, type CalcDisplayConfig, type CalcExpression, type CalcResolved, type CaptchaWidgetHandle, Checkbox, type CheckboxProps, ChoiceGroup, type ChoiceGroupProps, type ChoiceOption, type ConsentStatement, type ConsentStatements, DEFAULT_HONEYPOT_FIELD, DialogSurface, type DialogSurfaceProps, type FetchResultsInput, type FetchResultsResult, type FieldAggregation, type FieldErrors, type FieldRenderer, type FieldRendererProps, FieldShell, type FieldShellProps, type FieldWidth, Form, type FormAction, type FormAdapters, type FormButtonSettings, type FormContextValue, type FormControlLabels, FormControls, type FormControlsProps, type FormDocument, type FormFieldInstance, FormFields, type FormFieldsProps, FormLayout, type FormLayoutProps, type FormPollSettings, type FormPresentation, type FormProps, type FormResponseSettings, FormResults, type FormResultsProps, type FormState, type FormStepInfo, FormSteps, type FormStepsProps, type FormSuccessResponse, type FormSuccessResult, HCAPTCHA_SCRIPT_URL, HcaptchaCaptcha, type HcaptchaCaptchaProps, Honeypot, type HoneypotProps, Input, type InputProps, type LoadScript, type NavigateOptions, type NextButtonRenderProps, Poll, type PollProps, type PrefillOptions, type PresentationOption, type PresentationRegistry, type PresentationWrapperProps, type PresentationsConfig, RECAPTCHA_SCRIPT_URL, type RecallResolver, RecaptchaCaptcha, type RecaptchaCaptchaHandle, type RecaptchaCaptchaProps, type RendererOption, type RendererRegistry, type RendererTranslate, type RenderersConfig, Select, type SelectOption, type SelectProps, type SubmissionValue, type SubmitButtonRenderProps, type SubmitFormResult, type SubmitHandler, TURNSTILE_SCRIPT_URL, Textarea, type TextareaProps, type ToFormDocumentOptions, type TranslationBundle, TurnstileCaptcha, type TurnstileCaptchaProps, US_STATES, type UploadFileInput, type UploadFileResult, type UseDismissOptions, type UseFieldResult, type VoteStorage, buildRecallResolver, bundles, clientRuntimeKeys, cn, computeCalcFields, de, defaultPresentations, defaultRenderers, defineFieldRenderer, en, evaluateCalc, evaluateCondition, fetchFormResults, fieldKey, firstStepId, formatBytes, formatCalc, formatCalcValue, interpolate, isTerminalStepId, makeTranslate, resolveNextStepId, resolvePresentations, resolveRenderers, sanitizeUrl, serializeBody, stepFieldNames, submitForm, textOfBody, toFormDocument, uploadFile, useCalcValues, useDismiss, useField, useFocusTrap, useFormContext, useFormState, useFormStep, useScrollLock, valuesFromSearchParams, widthProps };
|
package/dist/react/Form.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AnyFormFieldDefinition } from "../fields/types.js";
|
|
2
2
|
import { BodyConverter } from "../actions/body/converters.js";
|
|
3
3
|
import { FormButtonSettings, FormDocument, FormPollSettings, FormResponseSettings } from "../form/types.js";
|
|
4
|
+
import { FormAdapters } from "./adapters.js";
|
|
4
5
|
import { RendererTranslate } from "./contract.js";
|
|
5
6
|
import { FormEventSink } from "../events/types.js";
|
|
6
7
|
import { AnyValidationRuleDefinition } from "../validation/types.js";
|
|
@@ -96,6 +97,12 @@ type FormProps = {
|
|
|
96
97
|
submitButtonClassName?: string; /** CSS class forwarded to the default "Next" `<button>`. Ignored when `renderNext` is provided. */
|
|
97
98
|
nextButtonClassName?: string; /** CSS class forwarded to the default "Back" `<button>`. Ignored when `renderBack` is provided. */
|
|
98
99
|
backButtonClassName?: string;
|
|
100
|
+
/**
|
|
101
|
+
* Host-owned effects (navigation, vote persistence, script loading). Each member defaults to
|
|
102
|
+
* the built-in DOM behavior; see {@link FormAdapters}. Pass a stable reference (module scope or
|
|
103
|
+
* `useMemo`), matching the other injectable props.
|
|
104
|
+
*/
|
|
105
|
+
adapters?: FormAdapters;
|
|
99
106
|
};
|
|
100
107
|
/** The headless form controller: state, progressive client validation, conditional visibility, submission, events. */
|
|
101
108
|
declare const Form: ({
|
|
@@ -135,7 +142,8 @@ declare const Form: ({
|
|
|
135
142
|
renderBack,
|
|
136
143
|
submitButtonClassName,
|
|
137
144
|
nextButtonClassName,
|
|
138
|
-
backButtonClassName
|
|
145
|
+
backButtonClassName,
|
|
146
|
+
adapters
|
|
139
147
|
}: FormProps) => import("react/jsx-runtime").JSX.Element;
|
|
140
148
|
//#endregion
|
|
141
149
|
export { Form, FormProps, FormSuccessResponse, FormSuccessResult };
|
package/dist/react/Form.js
CHANGED
|
@@ -14,6 +14,7 @@ import { buildRecallResolver, descriptorsFor } from "../recall/resolver.js";
|
|
|
14
14
|
import { firstStepId, isTerminalStepId, resolveNextStepId, stepFieldNames } from "../flow/engine.js";
|
|
15
15
|
import { cn } from "./cn.js";
|
|
16
16
|
import { makeTranslate } from "../translations/makeTranslate.js";
|
|
17
|
+
import { defaultNavigate } from "./adapters.js";
|
|
17
18
|
import { emitFormEvent } from "./events.js";
|
|
18
19
|
import { FormContext } from "./FormContext.js";
|
|
19
20
|
import { FormControls } from "./FormControls.js";
|
|
@@ -60,7 +61,7 @@ const focusFirstIn = (root, selector) => {
|
|
|
60
61
|
/** A stored button label counts only when it is a non-empty string; anything else falls through. */
|
|
61
62
|
const storedLabel = (value) => typeof value === "string" && value.length > 0 ? value : void 0;
|
|
62
63
|
/** The headless form controller: state, progressive client validation, conditional visibility, submission, events. */
|
|
63
|
-
const Form = ({ form, fieldTypes, rules, renderers, apiRoute, onSubmit, onSuccess, onError, converters, successBehavior = "replace", calcFunctions, events, t, locale = "en", layout, submitLabel, nextLabel, prevLabel, closeLabel, successMessage, presentation, presentations, onClose, title, initialValues, honeypot, captchaToken, context, children, header, className, renderSubmit, renderNext, renderBack, submitButtonClassName, nextButtonClassName, backButtonClassName }) => {
|
|
64
|
+
const Form = ({ form, fieldTypes, rules, renderers, apiRoute, onSubmit, onSuccess, onError, converters, successBehavior = "replace", calcFunctions, events, t, locale = "en", layout, submitLabel, nextLabel, prevLabel, closeLabel, successMessage, presentation, presentations, onClose, title, initialValues, honeypot, captchaToken, context, children, header, className, renderSubmit, renderNext, renderBack, submitButtonClassName, nextButtonClassName, backButtonClassName, adapters }) => {
|
|
64
65
|
const honeypotName = honeypot === false ? null : honeypot?.name ?? "website";
|
|
65
66
|
const honeypotRef = useRef(null);
|
|
66
67
|
const registry = useMemo(() => buildFieldTypeRegistry(fieldTypes), [fieldTypes]);
|
|
@@ -470,7 +471,7 @@ const Form = ({ form, fieldTypes, rules, renderers, apiRoute, onSubmit, onSucces
|
|
|
470
471
|
if (activePresentation.dismissOnSuccess) handleClose();
|
|
471
472
|
}
|
|
472
473
|
const redirectUrl = form.response?.type === "redirect" ? form.response.redirect?.url : void 0;
|
|
473
|
-
if (typeof redirectUrl === "string" && redirectUrl.length > 0
|
|
474
|
+
if (typeof redirectUrl === "string" && redirectUrl.length > 0) (adapters?.navigate ?? defaultNavigate)(redirectUrl, { replace: false });
|
|
474
475
|
} else {
|
|
475
476
|
if (result.fieldErrors) rawDispatch({
|
|
476
477
|
type: "SET_ALL_ISSUES",
|
package/dist/react/Form.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Form.js","names":[],"sources":["../../src/react/Form.tsx"],"sourcesContent":["'use client'\n\nimport {\n\ttype CSSProperties,\n\ttype FormEvent as ReactFormEvent,\n\ttype KeyboardEvent as ReactKeyboardEvent,\n\ttype ReactNode,\n\tuseCallback,\n\tuseEffect,\n\tuseMemo,\n\tuseReducer,\n\tuseRef,\n\tuseState,\n} from 'react'\nimport type { BodyConverter } from '../actions/body/converters'\nimport { serializeBody } from '../actions/body/serializeBody'\nimport { calcExpressionOf, computeCalcFields } from '../calc/computeCalcFields'\nimport { evaluateCondition } from '../conditions/evaluate'\nimport { noopEventSink } from '../events/noopSink'\nimport type { FormEventSink } from '../events/types'\nimport { fieldKey, isNamedField, type NamedFormFieldInstance } from '../fields/fieldKey'\nimport type { AnyFormFieldDefinition } from '../fields/types'\nimport { firstStepId, isTerminalStepId, resolveNextStepId, stepFieldNames } from '../flow/engine'\nimport type { FormFlow } from '../flow/types'\nimport type {\n\tFormButtonSettings,\n\tFormDocument,\n\tFormPollSettings,\n\tFormResponseSettings,\n} from '../form/types'\nimport {\n\tDEFAULT_PRESENTATION_NAME,\n\tdefaultPresentationDescriptors,\n} from '../presentations/defaults'\nimport { interpolate } from '../recall/interpolate'\nimport { buildRecallResolver, descriptorsFor } from '../recall/resolver'\nimport {\n\tCAPTCHA_TOKEN_KEY,\n\tCONTEXT_KEY,\n\tDEFAULT_HONEYPOT_FIELD,\n\tHONEYPOT_VALUE_KEY,\n} from '../spam/constants'\nimport type { FormFieldInstance, SubmissionValue } from '../submissions/types'\nimport { en } from '../translations/en'\nimport { keys } from '../translations/keys'\nimport { makeTranslate } from '../translations/makeTranslate'\nimport { resolveMessage } from '../validation/message'\nimport type { AnyValidationRuleDefinition } from '../validation/types'\nimport { cn } from './cn'\nimport type { RendererTranslate } from './contract'\nimport { emitFormEvent } from './events'\nimport { FormContext, type FormContextValue, type FormStepInfo } from './FormContext'\nimport {\n\ttype BackButtonRenderProps,\n\tFormControls,\n\ttype NextButtonRenderProps,\n\ttype SubmitButtonRenderProps,\n} from './FormControls'\nimport { FormFields } from './FormFields'\nimport { Honeypot } from './Honeypot'\nimport { defaultPresentations } from './presentation/presentations'\nimport { type PresentationsConfig, resolvePresentations } from './presentation/registry'\nimport type { FormPresentation } from './presentation/types'\nimport { type RenderersConfig, resolveRenderers } from './registry'\nimport { defaultRenderers } from './renderers'\nimport { buildFieldTypeRegistry, buildValidationRuleRegistry, visibleFields } from './resolveForm'\nimport {\n\tDEFAULT_STEP_ID,\n\ttype FieldErrors,\n\ttype FormAction,\n\tformReducer,\n\tinitialFormState,\n\tseedFieldValues,\n} from './state'\nimport { type SubmitFormResult, type SubmitHandler, submitForm } from './submitForm'\nimport { validateFieldValue } from './validateField'\n\nexport type {\n\tBackButtonRenderProps,\n\tNextButtonRenderProps,\n\tSubmitButtonRenderProps,\n} from './FormControls'\n// FormResponseSettings, FormButtonSettings, FormPollSettings, and FormDocument live in\n// `../form/types` (no 'use client') so server code (e.g. `toFormDocument` in a Server Component)\n// can use them without pulling in this client module. Re-exported here so `./react` and existing\n// `from './Form'` imports keep working unchanged.\nexport type { FormButtonSettings, FormDocument, FormPollSettings, FormResponseSettings }\n\n/**\n * The success response passed to `onSuccess`, recall-resolved and (for a message) serialized with the\n * form's active converters, so a host can render or toast the resolved response without re-deriving it.\n */\nexport type FormSuccessResponse =\n\t| { type: 'message'; html?: string }\n\t| { type: 'redirect'; url?: string }\n\n/** The second argument to `onSuccess`: the resolved success response (an object, so it can grow). */\nexport type FormSuccessResult = { response?: FormSuccessResponse }\n\nexport type FormProps = {\n\tform: FormDocument\n\tfieldTypes?: AnyFormFieldDefinition[]\n\trules?: AnyValidationRuleDefinition[]\n\trenderers?: RenderersConfig\n\tapiRoute?: string\n\tonSubmit?: SubmitHandler\n\t/**\n\t * Called after a successful submission with the submission id and the resolved success response\n\t * (recall-applied, serialized with `converters`), so a host can toast or render it. Fires in both\n\t * `successBehavior` modes and on the custom-`children` path.\n\t */\n\tonSuccess?: (submissionId?: string, result?: FormSuccessResult) => void\n\tonError?: (message: string) => void\n\t/**\n\t * Custom Lexical block converters (e.g. host `icon`/`badge` blocks) spread over the defaults for the\n\t * client serializer, so those blocks survive in the success message and in the `onSuccess` response.\n\t */\n\tconverters?: Record<string, BodyConverter>\n\t/**\n\t * What happens on a successful submit. `'replace'` (default) swaps the form for the success screen;\n\t * `'reset'` clears the fields in place and shows no success screen, so a host can toast via `onSuccess`\n\t * and keep the form usable.\n\t */\n\tsuccessBehavior?: 'replace' | 'reset'\n\t/**\n\t * Client halves of the plugin's registered `calc.functions` (same keys), used for the live calc\n\t * preview only; the server always recomputes authoritatively at submit with the registry's own\n\t * functions.\n\t */\n\tcalcFunctions?: Record<string, (args: number[]) => number>\n\tevents?: FormEventSink\n\tt?: RendererTranslate\n\tlocale?: string\n\tlayout?: boolean\n\t/** Submit button label. Precedence: this prop, then the form's `buttons.submitLabel`, then the translated default. */\n\tsubmitLabel?: string\n\t/** \"Next\" button label for multi-step forms. Precedence: this prop, then the form's `buttons.nextLabel`, then the translated default. */\n\tnextLabel?: string\n\t/** \"Back\" button label for multi-step forms. Precedence: this prop, then the form's `buttons.prevLabel`, then the translated default. */\n\tprevLabel?: string\n\t/** Label for the overlay close control (modal/drawer). */\n\tcloseLabel?: string\n\tsuccessMessage?: string\n\t/** Active presentation: a name into the registry or an inline presentation. Defaults to `'page'` when omitted. */\n\tpresentation?: string | FormPresentation\n\t/** Per-render presentation overrides merged onto the defaults (add, replace, or `false` to remove). */\n\tpresentations?: PresentationsConfig\n\t/** Invoked when an overlay presentation dismisses (close button, Escape, outside click, or `dismissOnSuccess`). */\n\tonClose?: () => void\n\t/**\n\t * Accessible name for an overlay surface (modal/drawer). Hosts choosing between a trigger\n\t * label and the form's own admin title should prefer `form.title` when set, falling back to\n\t * their own label otherwise.\n\t */\n\ttitle?: string\n\t/** Seed initial field values (e.g. from `valuesFromSearchParams`). Still validated on submit. */\n\tinitialValues?: Record<string, unknown>\n\t/** Honeypot decoy (on by default). `false` removes it; `{ name }` matches a customized server `spam.honeypot.fieldName`. */\n\thoneypot?: false | { name?: string }\n\t/** A token from your captcha widget; verified server-side when a captcha provider is configured. */\n\tcaptchaToken?: string\n\t/**\n\t * A signed form-context token from `signFormContext` (server/RSC), tying this render to a document\n\t * (e.g. the profile page it is on). Carried alongside the answers, verified server-side, and\n\t * available to `email.recipientSources`; it is never stored as an answer.\n\t */\n\tcontext?: string\n\t/** Custom layout: render fields with `useField`/`useFormState` instead of the auto-rendered field loop. */\n\tchildren?: ReactNode\n\t/** Chrome rendered inside the form, above the fields, in default mode (e.g. `<FormSteps />`). */\n\theader?: ReactNode\n\t/** Additional CSS class names applied to the root `<form>` element (and the success node). */\n\tclassName?: string\n\t/** Replace the default submit button entirely. Receives the resolved label and submitting state. */\n\trenderSubmit?: (props: SubmitButtonRenderProps) => ReactNode\n\t/** Replace the default \"Next\" button in multi-step forms. */\n\trenderNext?: (props: NextButtonRenderProps) => ReactNode\n\t/** Replace the default \"Back\" button in multi-step forms. */\n\trenderBack?: (props: BackButtonRenderProps) => ReactNode\n\t/** CSS class forwarded to the default submit `<button>`. Ignored when `renderSubmit` is provided. */\n\tsubmitButtonClassName?: string\n\t/** CSS class forwarded to the default \"Next\" `<button>`. Ignored when `renderNext` is provided. */\n\tnextButtonClassName?: string\n\t/** CSS class forwarded to the default \"Back\" `<button>`. Ignored when `renderBack` is provided. */\n\tbackButtonClassName?: string\n}\n\nconst isEmpty = (value: unknown): boolean =>\n\tvalue == null || value === '' || (Array.isArray(value) && value.length === 0)\n\n/** Visually hidden but screen-reader-announced, for the \"Step X of Y\" live region (no host CSS required). */\nconst SR_ONLY: CSSProperties = {\n\tposition: 'absolute',\n\twidth: 1,\n\theight: 1,\n\tpadding: 0,\n\tmargin: -1,\n\toverflow: 'hidden',\n\tclip: 'rect(0, 0, 0, 0)',\n\twhiteSpace: 'nowrap',\n\tborder: 0,\n}\n\n/** The base field name of an error key: the part before a repeater composite suffix (`name[0].sub`). */\nconst baseFieldKey = (key: string): string => {\n\tconst bracket = key.indexOf('[')\n\treturn bracket === -1 ? key : key.slice(0, bracket)\n}\n\n/** The id of the first flow step (in order) that owns an errored field, or undefined when none does. */\nconst firstStepWithError = (flow: FormFlow, errors: FieldErrors): string | undefined => {\n\tconst errored = new Set(Object.keys(errors).map(baseFieldKey))\n\treturn flow.steps.find((flowStep) => flowStep.fields.some((key) => errored.has(key)))?.id\n}\n\n/** The first focusable element under `root` that a step transition should land on (skips hidden/honeypot). */\nconst focusFirstIn = (root: HTMLElement | null, selector: string): void => {\n\tconst target = root?.querySelector<HTMLElement>(selector)\n\ttarget?.focus()\n}\n\n/** A stored button label counts only when it is a non-empty string; anything else falls through. */\nconst storedLabel = (value: unknown): string | undefined =>\n\ttypeof value === 'string' && value.length > 0 ? value : undefined\n\n/** The headless form controller: state, progressive client validation, conditional visibility, submission, events. */\nexport const Form = ({\n\tform,\n\tfieldTypes,\n\trules,\n\trenderers,\n\tapiRoute,\n\tonSubmit,\n\tonSuccess,\n\tonError,\n\tconverters,\n\tsuccessBehavior = 'replace',\n\tcalcFunctions,\n\tevents,\n\tt,\n\tlocale = 'en',\n\tlayout,\n\tsubmitLabel,\n\tnextLabel,\n\tprevLabel,\n\tcloseLabel,\n\tsuccessMessage,\n\tpresentation,\n\tpresentations,\n\tonClose,\n\ttitle,\n\tinitialValues,\n\thoneypot,\n\tcaptchaToken,\n\tcontext,\n\tchildren,\n\theader,\n\tclassName,\n\trenderSubmit,\n\trenderNext,\n\trenderBack,\n\tsubmitButtonClassName,\n\tnextButtonClassName,\n\tbackButtonClassName,\n}: FormProps) => {\n\tconst honeypotName = honeypot === false ? null : (honeypot?.name ?? DEFAULT_HONEYPOT_FIELD)\n\tconst honeypotRef = useRef<HTMLInputElement>(null)\n\tconst registry = useMemo(() => buildFieldTypeRegistry(fieldTypes), [fieldTypes])\n\tconst ruleRegistry = useMemo(() => buildValidationRuleRegistry(rules), [rules])\n\tconst rendererRegistry = useMemo(() => resolveRenderers(defaultRenderers, renderers), [renderers])\n\tconst presentationRegistry = useMemo(\n\t\t() => resolvePresentations(defaultPresentations, presentations),\n\t\t[presentations]\n\t)\n\tconst activePresentation: FormPresentation =\n\t\ttypeof presentation === 'object'\n\t\t\t? presentation\n\t\t\t: (presentationRegistry.get(presentation ?? DEFAULT_PRESENTATION_NAME) ??\n\t\t\t\tpresentationRegistry.get(DEFAULT_PRESENTATION_NAME) ??\n\t\t\t\tdefaultPresentationDescriptors.page)\n\tconst fieldsByName = useMemo(\n\t\t() => new Map(form.fields.filter(isNamedField).map((field) => [field.name, field])),\n\t\t[form.fields]\n\t)\n\tconst translate = useMemo<RendererTranslate>(() => t ?? makeTranslate(en), [t])\n\tconst resolvedCloseLabel = closeLabel ?? translate(keys.formClose)\n\tconst resolvedSuccessMessage = successMessage ?? translate(keys.formSuccess)\n\tconst docButtons: FormButtonSettings | undefined = form.buttons\n\tconst labels = useMemo(\n\t\t() => ({\n\t\t\tprev: prevLabel ?? storedLabel(docButtons?.prevLabel) ?? translate(keys.formBack),\n\t\t\tnext: nextLabel ?? storedLabel(docButtons?.nextLabel) ?? translate(keys.formNext),\n\t\t\tsubmit: submitLabel ?? storedLabel(docButtons?.submitLabel) ?? translate(keys.formSubmit),\n\t\t}),\n\t\t[prevLabel, nextLabel, submitLabel, docButtons, translate]\n\t)\n\n\t// Latest-value refs so event emission and the mount/unmount effect tolerate an inline `events` prop or a changing form id.\n\tconst sinkRef = useRef<FormEventSink>(noopEventSink)\n\tsinkRef.current = events ?? noopEventSink\n\tconst formIdRef = useRef('')\n\tformIdRef.current = String(form.id)\n\n\tconst [state, rawDispatch] = useReducer(formReducer, form.fields, (fields) =>\n\t\tinitialFormState({\n\t\t\t...seedFieldValues(fields),\n\t\t\t...(initialValues ?? {}),\n\t\t})\n\t)\n\n\t// Authoritative values for derived (calc) fields, recomputed from user answers on every change. The\n\t// server recomputes these too at submit; the client copy drives the live calc renderer, recall, and\n\t// submit. Server-resolved source values ride the document (`calcResolved`); custom function halves\n\t// come from the `calcFunctions` prop (functions never serialize).\n\tconst effectiveValues = useMemo(\n\t\t() =>\n\t\t\tcomputeCalcFields(form.fields, state.values, {\n\t\t\t\t...(form.calcResolved ?? {}),\n\t\t\t\t...(calcFunctions ? { functions: calcFunctions } : {}),\n\t\t\t}),\n\t\t[form.fields, state.values, form.calcResolved, calcFunctions]\n\t)\n\n\t// The calc fields' slice of effectiveValues, exposed on context for composed frontends.\n\tconst calcValues = useMemo(() => {\n\t\tconst values: Record<string, number> = {}\n\t\tfor (const field of form.fields) {\n\t\t\tif (calcExpressionOf(field) && isNamedField(field)) {\n\t\t\t\tconst computed = effectiveValues[field.name]\n\t\t\t\tvalues[field.name] = typeof computed === 'number' ? computed : 0\n\t\t\t}\n\t\t}\n\t\treturn values\n\t}, [form.fields, effectiveValues])\n\n\tconst recall = useMemo(\n\t\t() =>\n\t\t\tbuildRecallResolver({\n\t\t\t\tfields: form.fields,\n\t\t\t\tvalues: effectiveValues,\n\t\t\t\tregistry,\n\t\t\t\tlocale,\n\t\t\t\tt: translate,\n\t\t\t}),\n\t\t[form.fields, effectiveValues, registry, locale, translate]\n\t)\n\n\t// Multi-step is active only when the form is flagged `multistep` and its flow declares two or more\n\t// steps. With the flag off the form renders as a single page even if flow data is still stored.\n\tconst flow =\n\t\tform.multistep === true && form.flow && form.flow.steps.length >= 2 ? form.flow : undefined\n\tconst [currentStepId, setCurrentStepId] = useState<string | undefined>(() =>\n\t\tflow ? firstStepId(flow) : undefined\n\t)\n\tconst [history, setHistory] = useState<string[]>([])\n\n\tconst startedRef = useRef(false)\n\tconst submittedRef = useRef(false)\n\tconst submittingRef = useRef(false)\n\tconst advancingRef = useRef(false)\n\tconst flowRef = useRef(flow)\n\tflowRef.current = flow\n\n\t// Per-step error reveal: a field maps to the id of the flow step whose `fields` include it, else the\n\t// default step (single-step forms, or a field assigned to no step), so a single-step form collapses to\n\t// one step and today's behavior.\n\tconst stepIdOfField = useMemo(() => {\n\t\tconst map = new Map<string, string>()\n\t\tif (flow) {\n\t\t\tfor (const flowStep of flow.steps) {\n\t\t\t\tfor (const key of flowStep.fields) {\n\t\t\t\t\tmap.set(key, flowStep.id)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn (fieldKey: string): string => map.get(fieldKey) ?? DEFAULT_STEP_ID\n\t}, [flow])\n\tconst allStepIds = useMemo(\n\t\t() => (flow ? flow.steps.map((flowStep) => flowStep.id) : [DEFAULT_STEP_ID]),\n\t\t[flow]\n\t)\n\n\t// Focus management for step transitions and blocked advances/submits. A pending request is performed\n\t// by an effect after the render it triggers, so focus lands on the DOM that reflects the new state.\n\tconst formRef = useRef<HTMLFormElement>(null)\n\tconst pendingFocusRef = useRef<'stepStart' | 'firstInvalid' | null>(null)\n\tconst [focusNonce, setFocusNonce] = useState(0)\n\tconst requestFocus = (intent: 'stepStart' | 'firstInvalid') => {\n\t\tpendingFocusRef.current = intent\n\t\tsetFocusNonce((nonce) => nonce + 1)\n\t}\n\n\tconst dispatch = useCallback((action: FormAction) => {\n\t\tif (action.type === 'SET_VALUE' && !startedRef.current) {\n\t\t\tstartedRef.current = true\n\t\t\temitFormEvent(sinkRef.current, formIdRef.current, { type: 'form.started' })\n\t\t}\n\t\trawDispatch(action)\n\t}, [])\n\n\tconst validateField = useCallback(\n\t\t(name: string, value: unknown) => {\n\t\t\tconst field = fieldsByName.get(name)\n\t\t\tif (!field) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst answers = { ...effectiveValues, [name]: value }\n\t\t\t// Mirror the server: a field whose `validateWhen` is unmet is not validated; clear any stale error.\n\t\t\tif (!evaluateCondition(field.validateWhen, answers)) {\n\t\t\t\trawDispatch({ type: 'SET_FIELD_ISSUES', name, errors: [] })\n\t\t\t\treturn\n\t\t\t}\n\t\t\tvoid validateFieldValue({\n\t\t\t\tfield,\n\t\t\t\tvalue,\n\t\t\t\tregistry,\n\t\t\t\truleRegistry,\n\t\t\t\tanswers,\n\t\t\t\tlocale,\n\t\t\t\tt: translate,\n\t\t\t}).then(({ errors }) => {\n\t\t\t\trawDispatch({ type: 'SET_FIELD_ISSUES', name, errors })\n\t\t\t\tconst [firstError] = errors\n\t\t\t\tif (firstError !== undefined) {\n\t\t\t\t\temitFormEvent(sinkRef.current, formIdRef.current, {\n\t\t\t\t\t\ttype: 'field.errored',\n\t\t\t\t\t\tfield: name,\n\t\t\t\t\t\tmessage: firstError,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\t\t[fieldsByName, effectiveValues, registry, ruleRegistry, locale, translate]\n\t)\n\n\tconst visible = visibleFields(form.fields, effectiveValues)\n\n\t/** Visible, answered named fields. Display-only ('none' kind) and nameless (bare) fields never contribute. */\n\tconst answerableFields = (): NamedFormFieldInstance[] =>\n\t\tvisibleFields(form.fields, effectiveValues)\n\t\t\t.filter((field) => registry.get(field.blockType)?.value !== 'none')\n\t\t\t.filter(isNamedField)\n\t\t\t.filter((field) => !isEmpty(effectiveValues[field.name]))\n\n\t/** Answered visible fields as raw submission values, sent to the server on submit. */\n\tconst answeredValues = (): SubmissionValue[] =>\n\t\tanswerableFields().map((field) => ({ field: field.name, value: effectiveValues[field.name] }))\n\n\t/**\n\t * Same fields, formatted via `recall` (option labels, Yes/No, localized dates): recall-fidelity\n\t * values for client-rendered templates, matching what the server gives email-team's body.\n\t */\n\tconst formattedValues = (): SubmissionValue[] =>\n\t\tanswerableFields().map((field) => ({ field: field.name, value: recall(field.name) }))\n\n\t/**\n\t * The recall-resolved success response: a redirect's url, or the response message serialized with\n\t * the active `converters` (so host blocks survive). Shared by the success screen and the value\n\t * handed to `onSuccess`, so both stay identical.\n\t */\n\tconst resolveSuccessResponse = (): FormSuccessResponse | undefined => {\n\t\tconst response = form.response\n\t\tif (response?.type === 'redirect') {\n\t\t\treturn { type: 'redirect', url: response.redirect?.url ?? undefined }\n\t\t}\n\t\tconst message =\n\t\t\tresponse?.type === 'message' || response?.type == null ? response?.message : undefined\n\t\tif (!message) {\n\t\t\treturn undefined\n\t\t}\n\t\treturn {\n\t\t\ttype: 'message',\n\t\t\thtml: serializeBody(message, {\n\t\t\t\tvalues: formattedValues(),\n\t\t\t\tdescriptors: descriptorsFor(answerableFields()),\n\t\t\t\tconverters,\n\t\t\t}),\n\t\t}\n\t}\n\n\t// Steps address fields by key: machine names for named fields, block row ids for bare blocks.\n\t// `step.fields` is membership only; render order follows `form.fields` (the order of `visible`),\n\t// so a step shows its fields in the form's field order, not the flow-builder entry order.\n\tconst stepKeys = flow && currentStepId ? stepFieldNames(flow, currentStepId) : []\n\tconst stepKeySet = new Set(stepKeys)\n\tconst stepVisible: FormFieldInstance[] = visible.filter((field) =>\n\t\tstepKeySet.has(fieldKey(field))\n\t)\n\n\t// Validate the sub-fields of the given repeaters, one pass per visible row, returning composite-key\n\t// errors (`fieldName[rowIndex].subFieldName`). Shared by submit and step navigation so both mirror\n\t// the server's per-row pass and neither lets an invalid required sub-field slip through.\n\tconst validateRepeaterSubFields = async (\n\t\trepeaters: FormFieldInstance[]\n\t): Promise<FieldErrors> => {\n\t\tconst errors: FieldErrors = {}\n\t\tfor (const field of repeaters.filter(\n\t\t\t(f): f is NamedFormFieldInstance => f.blockType === 'repeater' && isNamedField(f)\n\t\t)) {\n\t\t\tconst rows = Array.isArray(effectiveValues[field.name])\n\t\t\t\t? (effectiveValues[field.name] as Array<Record<string, unknown>>)\n\t\t\t\t: []\n\t\t\tconst subFields = (\n\t\t\t\tArray.isArray(field.subFields) ? (field.subFields as FormFieldInstance[]) : []\n\t\t\t).filter(isNamedField)\n\t\t\tfor (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {\n\t\t\t\tconst row = rows[rowIndex] ?? {}\n\t\t\t\tfor (const subField of subFields) {\n\t\t\t\t\tif (!evaluateCondition(subField.visibleWhen, row)) continue\n\t\t\t\t\tif (!evaluateCondition(subField.validateWhen, row)) continue\n\t\t\t\t\tconst subResult = await validateFieldValue({\n\t\t\t\t\t\tfield: subField,\n\t\t\t\t\t\tvalue: row[subField.name],\n\t\t\t\t\t\tregistry,\n\t\t\t\t\t\truleRegistry,\n\t\t\t\t\t\tanswers: row,\n\t\t\t\t\t\tlocale,\n\t\t\t\t\t\tt: translate,\n\t\t\t\t\t})\n\t\t\t\t\tconst compositeKey = `${field.name}[${rowIndex}].${subField.name}`\n\t\t\t\t\tif (subResult.errors.length > 0) errors[compositeKey] = subResult.errors\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn errors\n\t}\n\n\tconst goNext = async () => {\n\t\t// Re-entrancy guard: a double-click during async validation must not push the same step onto\n\t\t// history twice (which would need two Back presses to undo).\n\t\tif (!flow || !currentStepId || advancingRef.current) {\n\t\t\treturn\n\t\t}\n\t\tadvancingRef.current = true\n\t\ttry {\n\t\t\tconst results = await Promise.all(\n\t\t\t\tstepVisible\n\t\t\t\t\t.filter((field) => !calcExpressionOf(field))\n\t\t\t\t\t.filter((field) => registry.get(field.blockType)?.value !== 'none')\n\t\t\t\t\t.filter(isNamedField)\n\t\t\t\t\t.filter((field) => evaluateCondition(field.validateWhen, effectiveValues))\n\t\t\t\t\t.map(async (field) => ({\n\t\t\t\t\t\tfield,\n\t\t\t\t\t\t...(await validateFieldValue({\n\t\t\t\t\t\t\tfield,\n\t\t\t\t\t\t\tvalue: effectiveValues[field.name],\n\t\t\t\t\t\t\tregistry,\n\t\t\t\t\t\t\truleRegistry,\n\t\t\t\t\t\t\tanswers: effectiveValues,\n\t\t\t\t\t\t\tlocale,\n\t\t\t\t\t\t\tt: translate,\n\t\t\t\t\t\t})),\n\t\t\t\t\t}))\n\t\t\t)\n\t\t\tlet hasError = false\n\t\t\tfor (const result of results) {\n\t\t\t\trawDispatch({ type: 'TOUCH', name: result.field.name })\n\t\t\t\trawDispatch({\n\t\t\t\t\ttype: 'SET_FIELD_ISSUES',\n\t\t\t\t\tname: result.field.name,\n\t\t\t\t\terrors: result.errors,\n\t\t\t\t})\n\t\t\t\tif (result.errors.length > 0) {\n\t\t\t\t\thasError = true\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Mirror submit: validate the step's repeater sub-fields too, so a required sub-field can't be\n\t\t\t// skipped past on a non-terminal step (errors surface inline via the composite key).\n\t\t\tconst repeaterErrors = await validateRepeaterSubFields(stepVisible)\n\t\t\tfor (const [compositeKey, errs] of Object.entries(repeaterErrors)) {\n\t\t\t\trawDispatch({ type: 'SET_FIELD_ISSUES', name: compositeKey, errors: errs })\n\t\t\t\thasError = true\n\t\t\t}\n\t\t\tif (hasError) {\n\t\t\t\t// Mark this step attempted so its fields reveal their errors, then focus the first invalid one.\n\t\t\t\trawDispatch({ type: 'MARK_STEP_ATTEMPTED', stepId: currentStepId })\n\t\t\t\trequestFocus('firstInvalid')\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst next = resolveNextStepId(flow, currentStepId, effectiveValues)\n\t\t\tif (!next) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\temitFormEvent(sinkRef.current, formIdRef.current, {\n\t\t\t\ttype: 'step.completed',\n\t\t\t\tstepId: currentStepId,\n\t\t\t})\n\t\t\tsetHistory((prev) => [...prev, currentStepId])\n\t\t\tsetCurrentStepId(next)\n\t\t\trequestFocus('stepStart')\n\t\t\temitFormEvent(sinkRef.current, formIdRef.current, { type: 'step.viewed', stepId: next })\n\t\t} finally {\n\t\t\tadvancingRef.current = false\n\t\t}\n\t}\n\n\tconst goBack = () => {\n\t\tconst prev = history[history.length - 1]\n\t\tif (prev === undefined) {\n\t\t\treturn\n\t\t}\n\t\tsetHistory((entries) => entries.slice(0, -1))\n\t\tsetCurrentStepId(prev)\n\t\trequestFocus('stepStart')\n\t\temitFormEvent(sinkRef.current, formIdRef.current, { type: 'step.viewed', stepId: prev })\n\t}\n\n\t// Jump to an earlier step (from the terminal Submit when an earlier step is invalid), rebuilding the\n\t// Back stack as the flow's linear path up to it so Back still works.\n\tconst navigateToStep = (stepId: string) => {\n\t\tif (!flow || stepId === currentStepId) {\n\t\t\treturn\n\t\t}\n\t\tconst idx = flow.steps.findIndex((flowStep) => flowStep.id === stepId)\n\t\tif (idx < 0) {\n\t\t\treturn\n\t\t}\n\t\tsetHistory(flow.steps.slice(0, idx).map((flowStep) => flowStep.id))\n\t\tsetCurrentStepId(stepId)\n\t\temitFormEvent(sinkRef.current, formIdRef.current, { type: 'step.viewed', stepId })\n\t}\n\n\t// Perform a pending focus request after the render it triggered, so it lands on the DOM that reflects\n\t// the new state (a changed step, or freshly revealed errors). Null on mount, so the first render never\n\t// steals focus. currentStepId and focusNonce are intentional re-run triggers: a blocked advance keeps\n\t// the same step, so the nonce forces a fresh run; the body reads only refs, hence the ignore.\n\t// biome-ignore lint/correctness/useExhaustiveDependencies: currentStepId/focusNonce are re-run triggers, not read in the body\n\tuseEffect(() => {\n\t\tconst intent = pendingFocusRef.current\n\t\tif (!intent) {\n\t\t\treturn\n\t\t}\n\t\tpendingFocusRef.current = null\n\t\tif (intent === 'firstInvalid') {\n\t\t\tfocusFirstIn(formRef.current, '[aria-invalid=\"true\"]')\n\t\t\treturn\n\t\t}\n\t\tconst region = formRef.current?.querySelector<HTMLElement>('[data-fb-step-region]')\n\t\tif (region) {\n\t\t\tregion.focus()\n\t\t} else {\n\t\t\tfocusFirstIn(formRef.current, 'input:not([type=\"hidden\"]), select, textarea')\n\t\t}\n\t}, [currentStepId, focusNonce])\n\n\tuseEffect(() => {\n\t\temitFormEvent(sinkRef.current, formIdRef.current, { type: 'form.viewed' })\n\t\tconst mountFlow = flowRef.current\n\t\tif (mountFlow) {\n\t\t\tconst first = firstStepId(mountFlow)\n\t\t\tif (first) {\n\t\t\t\temitFormEvent(sinkRef.current, formIdRef.current, { type: 'step.viewed', stepId: first })\n\t\t\t}\n\t\t}\n\t\treturn () => {\n\t\t\tif (!submittedRef.current && !submittingRef.current) {\n\t\t\t\temitFormEvent(sinkRef.current, formIdRef.current, { type: 'form.abandoned' })\n\t\t\t}\n\t\t}\n\t}, [])\n\n\tconst handleClose = useCallback(() => {\n\t\tonClose?.()\n\t}, [onClose])\n\n\tconst handleSubmit = async (event: ReactFormEvent<HTMLFormElement>) => {\n\t\tevent.preventDefault()\n\t\t// Re-entrancy guard: claim the in-flight slot before the async validation window so a fast second\n\t\t// activation (double-click, Enter + click) cannot reach the transport and POST the submission twice.\n\t\tif (submittingRef.current) {\n\t\t\treturn\n\t\t}\n\t\tsubmittingRef.current = true\n\t\tconst visible = visibleFields(form.fields, effectiveValues)\n\t\tconst results = await Promise.all(\n\t\t\t// Calc fields carry no rules and have no input; they are always satisfied, so skip validating them.\n\t\t\t// Display-only ('none' kind, e.g. message) and nameless (bare) fields are skipped too, mirroring the server.\n\t\t\t// A field whose `validateWhen` is unmet is skipped too, mirroring the server (no client/server divergence).\n\t\t\tvisible\n\t\t\t\t.filter((field) => !calcExpressionOf(field))\n\t\t\t\t.filter((field) => registry.get(field.blockType)?.value !== 'none')\n\t\t\t\t.filter(isNamedField)\n\t\t\t\t.filter((field) => evaluateCondition(field.validateWhen, effectiveValues))\n\t\t\t\t.map(async (field) => ({\n\t\t\t\t\tfield,\n\t\t\t\t\t...(await validateFieldValue({\n\t\t\t\t\t\tfield,\n\t\t\t\t\t\tvalue: effectiveValues[field.name],\n\t\t\t\t\t\tregistry,\n\t\t\t\t\t\truleRegistry,\n\t\t\t\t\t\tanswers: effectiveValues,\n\t\t\t\t\t\tlocale,\n\t\t\t\t\t\tt: translate,\n\t\t\t\t\t})),\n\t\t\t\t}))\n\t\t)\n\t\tconst errors: FieldErrors = {}\n\t\tfor (const result of results) {\n\t\t\tif (result.errors.length > 0) {\n\t\t\t\terrors[result.field.name] = result.errors\n\t\t\t\tconst [firstError] = result.errors\n\t\t\t\tif (firstError !== undefined) {\n\t\t\t\t\temitFormEvent(sinkRef.current, formIdRef.current, {\n\t\t\t\t\t\ttype: 'field.errored',\n\t\t\t\t\t\tfield: result.field.name,\n\t\t\t\t\t\tmessage: firstError,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Validate sub-fields within each visible repeater (composite keys `fieldName[rowIndex].subFieldName`),\n\t\t// mirroring the server's per-row pass, so the repeater renderer surfaces them inline.\n\t\tObject.assign(errors, await validateRepeaterSubFields(visible))\n\n\t\tconst hasErrors = Object.keys(errors).length > 0\n\t\t// A terminal submit validates every step, so every step is attempted (its errors may now reveal);\n\t\t// a valid submit clears stale errors without marking anything.\n\t\trawDispatch({ type: 'SET_ALL_ISSUES', errors, steps: hasErrors ? allStepIds : [] })\n\t\tif (hasErrors) {\n\t\t\tsubmittingRef.current = false\n\t\t\t// On a multi-step form, route to the first step that owns an invalid field rather than failing in\n\t\t\t// place on the terminal step, then focus its first invalid field.\n\t\t\tif (flow && currentStepId) {\n\t\t\t\tconst target = firstStepWithError(flow, errors)\n\t\t\t\tif (target) {\n\t\t\t\t\tnavigateToStep(target)\n\t\t\t\t}\n\t\t\t}\n\t\t\trequestFocus('firstInvalid')\n\t\t\treturn\n\t\t}\n\t\trawDispatch({ type: 'SUBMIT_START' })\n\t\tconst values: SubmissionValue[] = answeredValues()\n\t\tif (honeypotName) {\n\t\t\tconst decoy = honeypotRef.current?.value ?? ''\n\t\t\tif (decoy !== '') {\n\t\t\t\t// Submit the decoy under a reserved key, not its cosmetic DOM name, so a real field sharing\n\t\t\t\t// that name is never stripped or mistaken for the honeypot on the server.\n\t\t\t\tvalues.push({ field: HONEYPOT_VALUE_KEY, value: decoy })\n\t\t\t}\n\t\t}\n\t\tif (captchaToken) {\n\t\t\tvalues.push({ field: CAPTCHA_TOKEN_KEY, value: captchaToken })\n\t\t}\n\t\tif (context) {\n\t\t\t// Rides under a reserved key like the captcha token: verified and stored separately server-side,\n\t\t\t// never merged into the answers.\n\t\t\tvalues.push({ field: CONTEXT_KEY, value: context })\n\t\t}\n\t\tconst result: SubmitFormResult = onSubmit\n\t\t\t? await onSubmit({ formId: form.id, values })\n\t\t\t: await submitForm({ formId: form.id, values, apiRoute })\n\t\tsubmittingRef.current = false\n\t\tif (result.ok) {\n\t\t\t// A submission happened, so the unmount effect must not emit `form.abandoned`, in either mode.\n\t\t\tsubmittedRef.current = true\n\t\t\temitFormEvent(sinkRef.current, formIdRef.current, {\n\t\t\t\ttype: 'submission.created',\n\t\t\t\tsubmissionId: result.submissionId,\n\t\t\t})\n\t\t\t// Resolve the response before a reset clears the answers the recall reads from.\n\t\t\tonSuccess?.(result.submissionId, { response: resolveSuccessResponse() })\n\t\t\tif (successBehavior === 'reset') {\n\t\t\t\t// Reset in place: the host handles feedback (e.g. a toast via onSuccess); no success screen.\n\t\t\t\trawDispatch({\n\t\t\t\t\ttype: 'RESET',\n\t\t\t\t\tvalues: { ...seedFieldValues(form.fields), ...(initialValues ?? {}) },\n\t\t\t\t})\n\t\t\t\t// The reducer holds only values/errors; flow position and the lifecycle `started` ref live\n\t\t\t\t// outside it. Reset them too so a multi-step form returns to its first step (not stranded on\n\t\t\t\t// the terminal one) and a fresh fill re-emits `form.started`. `submittedRef` stays set: a\n\t\t\t\t// submission did happen, so the unmount guard must not report the completed form abandoned.\n\t\t\t\tif (flow) {\n\t\t\t\t\tsetCurrentStepId(firstStepId(flow))\n\t\t\t\t\tsetHistory([])\n\t\t\t\t}\n\t\t\t\tstartedRef.current = false\n\t\t\t} else {\n\t\t\t\trawDispatch({ type: 'SUBMIT_SUCCESS' })\n\t\t\t\tif (activePresentation.dismissOnSuccess) {\n\t\t\t\t\thandleClose()\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst redirectUrl =\n\t\t\t\tform.response?.type === 'redirect' ? form.response.redirect?.url : undefined\n\t\t\t// Part of submit handling, not rendering: fires on the custom-`children` path too.\n\t\t\t// Browser-only: no-op during SSR or in non-DOM test environments.\n\t\t\tif (\n\t\t\t\ttypeof redirectUrl === 'string' &&\n\t\t\t\tredirectUrl.length > 0 &&\n\t\t\t\ttypeof window !== 'undefined'\n\t\t\t) {\n\t\t\t\twindow.location.assign(redirectUrl)\n\t\t\t}\n\t\t} else {\n\t\t\tif (result.fieldErrors) {\n\t\t\t\trawDispatch({ type: 'SET_ALL_ISSUES', errors: result.fieldErrors, steps: allStepIds })\n\t\t\t}\n\t\t\tconst message = result.message ?? translate(keys.formSubmitFailed)\n\t\t\trawDispatch({ type: 'SUBMIT_ERROR', message })\n\t\t\tonError?.(message)\n\t\t}\n\t}\n\n\tconst isTerminalStep =\n\t\tflow && currentStepId ? isTerminalStepId(flow, currentStepId, effectiveValues) : true\n\n\t// Enter on a multi-step form: on a non-terminal step, advance like Next (validate, then advance or\n\t// reveal + focus); on the terminal step, fall through to native submit (guarded by `submittingRef`).\n\t// A textarea (Enter = newline), select (confirm), and buttons/submit inputs (activation) are exempt.\n\t// Single-step forms keep native Enter-to-submit. The `<form>` is the plugin's even in children mode,\n\t// so this is the only place a host could get this behavior.\n\tconst handleKeyDown = (event: ReactKeyboardEvent<HTMLFormElement>) => {\n\t\tif (!flow || event.key !== 'Enter') {\n\t\t\treturn\n\t\t}\n\t\tconst target = event.target\n\t\tif (\n\t\t\ttarget instanceof HTMLTextAreaElement ||\n\t\t\ttarget instanceof HTMLSelectElement ||\n\t\t\ttarget instanceof HTMLButtonElement\n\t\t) {\n\t\t\treturn\n\t\t}\n\t\tif (\n\t\t\ttarget instanceof HTMLInputElement &&\n\t\t\t(target.type === 'submit' || target.type === 'button')\n\t\t) {\n\t\t\treturn\n\t\t}\n\t\tif (!isTerminalStep) {\n\t\t\tevent.preventDefault()\n\t\t\tvoid goNext()\n\t\t}\n\t}\n\n\tconst step: FormStepInfo = flow\n\t\t? {\n\t\t\t\tflow,\n\t\t\t\tcurrentStepId,\n\t\t\t\tstepIndex: flow.steps.findIndex((s) => s.id === currentStepId),\n\t\t\t\tstepCount: flow.steps.length,\n\t\t\t\tisFirst: history.length === 0,\n\t\t\t\tisTerminal: isTerminalStep,\n\t\t\t\tgoNext: () => {\n\t\t\t\t\tvoid goNext()\n\t\t\t\t},\n\t\t\t\tgoBack,\n\t\t\t}\n\t\t: {\n\t\t\t\tstepIndex: 0,\n\t\t\t\tstepCount: 1,\n\t\t\t\tisFirst: true,\n\t\t\t\tisTerminal: true,\n\t\t\t\tgoNext: () => {},\n\t\t\t\tgoBack: () => {},\n\t\t\t}\n\n\t// The \"Step X of Y\" announcement (aria-live + the step region's accessible name).\n\tconst stepStatusText = flow\n\t\t? resolveMessage(translate(keys.formStepStatus), {\n\t\t\t\tcurrent: String(step.stepIndex + 1),\n\t\t\t\ttotal: String(step.stepCount),\n\t\t\t})\n\t\t: ''\n\t// Whether the current step has been attempted and still has a revealed error (drives the step-level alert).\n\tconst currentStepHasError =\n\t\tflow != null &&\n\t\tcurrentStepId != null &&\n\t\tstate.attemptedSteps.has(currentStepId) &&\n\t\tObject.entries(state.errors).some(\n\t\t\t([key, errs]) => errs.length > 0 && stepIdOfField(baseFieldKey(key)) === currentStepId\n\t\t)\n\n\tconst renderedFields = (flow ? stepVisible : visible).filter(\n\t\t(field) => field.hidden !== true && field.calcDisplay !== false\n\t)\n\n\tconst contextValue: FormContextValue = {\n\t\tform,\n\t\tstate,\n\t\tdispatch,\n\t\tvalidateField,\n\t\tlocale,\n\t\tstep,\n\t\trendererRegistry,\n\t\tlabels,\n\t\tt: translate,\n\t\teffectiveValues,\n\t\tcalcValues,\n\t\trecall,\n\t\trenderedFields,\n\t\tconverters,\n\t\tstepIdOfField,\n\t}\n\n\tconst PresentationWrapper = activePresentation.Wrapper\n\tconst wrap = (content: ReactNode): ReactNode =>\n\t\tPresentationWrapper ? (\n\t\t\t<PresentationWrapper\n\t\t\t\tpresentation={activePresentation}\n\t\t\t\topen\n\t\t\t\tonClose={handleClose}\n\t\t\t\ttitle={title}\n\t\t\t\tcloseLabel={resolvedCloseLabel}\n\t\t\t>\n\t\t\t\t{content}\n\t\t\t</PresentationWrapper>\n\t\t) : (\n\t\t\tcontent\n\t\t)\n\n\tif (children !== undefined) {\n\t\treturn (\n\t\t\t<FormContext.Provider value={contextValue}>\n\t\t\t\t{wrap(\n\t\t\t\t\t<form\n\t\t\t\t\t\tref={formRef}\n\t\t\t\t\t\tclassName={cn('fb-form-root', className)}\n\t\t\t\t\t\tnoValidate\n\t\t\t\t\t\tonSubmit={handleSubmit}\n\t\t\t\t\t\tonKeyDown={handleKeyDown}\n\t\t\t\t\t\tdata-fb-presentation={activePresentation.name}\n\t\t\t\t\t\tdata-fb-density={activePresentation.density}\n\t\t\t\t\t>\n\t\t\t\t\t\t{honeypotName ? <Honeypot name={honeypotName} inputRef={honeypotRef} /> : null}\n\t\t\t\t\t\t{children}\n\t\t\t\t\t</form>\n\t\t\t\t)}\n\t\t\t</FormContext.Provider>\n\t\t)\n\t}\n\n\tif (state.submitted) {\n\t\tconst successResponse = resolveSuccessResponse()\n\t\tconst responseHtml = successResponse?.type === 'message' ? successResponse.html : undefined\n\t\treturn (\n\t\t\t<FormContext.Provider value={contextValue}>\n\t\t\t\t{wrap(\n\t\t\t\t\tresponseHtml ? (\n\t\t\t\t\t\t// Safe to inject: serializeBody HTML-escapes all text (recall values included) and sanitizes link URLs.\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\trole=\"status\"\n\t\t\t\t\t\t\tclassName={cn('fb-form__success', className)}\n\t\t\t\t\t\t\tdata-fb-presentation={activePresentation.name}\n\t\t\t\t\t\t\tdata-fb-density={activePresentation.density}\n\t\t\t\t\t\t\t// biome-ignore lint/security/noDangerouslySetInnerHtml: HTML is produced by our escaping serializer, never raw user input\n\t\t\t\t\t\t\tdangerouslySetInnerHTML={{ __html: responseHtml }}\n\t\t\t\t\t\t/>\n\t\t\t\t\t) : (\n\t\t\t\t\t\t<p\n\t\t\t\t\t\t\trole=\"status\"\n\t\t\t\t\t\t\tclassName={cn('fb-form__success', className)}\n\t\t\t\t\t\t\tdata-fb-presentation={activePresentation.name}\n\t\t\t\t\t\t\tdata-fb-density={activePresentation.density}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{interpolate(resolvedSuccessMessage, recall)}\n\t\t\t\t\t\t</p>\n\t\t\t\t\t)\n\t\t\t\t)}\n\t\t\t</FormContext.Provider>\n\t\t)\n\t}\n\n\treturn (\n\t\t<FormContext.Provider value={contextValue}>\n\t\t\t{wrap(\n\t\t\t\t<form\n\t\t\t\t\tref={formRef}\n\t\t\t\t\tclassName={cn('fb-form-root', className)}\n\t\t\t\t\tnoValidate\n\t\t\t\t\tonSubmit={handleSubmit}\n\t\t\t\t\tonKeyDown={handleKeyDown}\n\t\t\t\t\tdata-fb-presentation={activePresentation.name}\n\t\t\t\t\tdata-fb-density={activePresentation.density}\n\t\t\t\t>\n\t\t\t\t\t{honeypotName ? <Honeypot name={honeypotName} inputRef={honeypotRef} /> : null}\n\t\t\t\t\t{header}\n\t\t\t\t\t{flow ? (\n\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t{/* Announces \"Step X of Y\" politely on each step change. A bare aria-live region, not\n\t\t\t\t\t\t\t role=status, so the single status role stays with the form's success outcome. */}\n\t\t\t\t\t\t\t<div aria-live=\"polite\" aria-atomic=\"true\" style={SR_ONLY}>\n\t\t\t\t\t\t\t\t{stepStatusText}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t{/* Focus lands here on a step change (the region's start), so keyboard/SR users move with it.\n\t\t\t\t\t\t\t A plain focusable container, not a named group; the aria-live region above does the announcing. */}\n\t\t\t\t\t\t\t<div data-fb-step-region tabIndex={-1} className=\"fb-form__step\">\n\t\t\t\t\t\t\t\t<FormFields layout={layout} />\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t{currentStepHasError ? (\n\t\t\t\t\t\t\t\t<p role=\"alert\" className=\"fb-form__step-error\">\n\t\t\t\t\t\t\t\t\t{translate(keys.formStepInvalid)}\n\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t</>\n\t\t\t\t\t) : (\n\t\t\t\t\t\t<FormFields layout={layout} />\n\t\t\t\t\t)}\n\t\t\t\t\t{state.submitError ? (\n\t\t\t\t\t\t<p role=\"alert\" className=\"fb-form__submit-error\">\n\t\t\t\t\t\t\t{state.submitError}\n\t\t\t\t\t\t</p>\n\t\t\t\t\t) : null}\n\t\t\t\t\t<FormControls\n\t\t\t\t\t\tbackButtonClassName={backButtonClassName}\n\t\t\t\t\t\tnextButtonClassName={nextButtonClassName}\n\t\t\t\t\t\tsubmitButtonClassName={submitButtonClassName}\n\t\t\t\t\t\trenderBack={renderBack}\n\t\t\t\t\t\trenderNext={renderNext}\n\t\t\t\t\t\trenderSubmit={renderSubmit}\n\t\t\t\t\t/>\n\t\t\t\t</form>\n\t\t\t)}\n\t\t</FormContext.Provider>\n\t)\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Form.js","names":[],"sources":["../../src/react/Form.tsx"],"sourcesContent":["'use client'\n\nimport {\n\ttype CSSProperties,\n\ttype FormEvent as ReactFormEvent,\n\ttype KeyboardEvent as ReactKeyboardEvent,\n\ttype ReactNode,\n\tuseCallback,\n\tuseEffect,\n\tuseMemo,\n\tuseReducer,\n\tuseRef,\n\tuseState,\n} from 'react'\nimport type { BodyConverter } from '../actions/body/converters'\nimport { serializeBody } from '../actions/body/serializeBody'\nimport { calcExpressionOf, computeCalcFields } from '../calc/computeCalcFields'\nimport { evaluateCondition } from '../conditions/evaluate'\nimport { noopEventSink } from '../events/noopSink'\nimport type { FormEventSink } from '../events/types'\nimport { fieldKey, isNamedField, type NamedFormFieldInstance } from '../fields/fieldKey'\nimport type { AnyFormFieldDefinition } from '../fields/types'\nimport { firstStepId, isTerminalStepId, resolveNextStepId, stepFieldNames } from '../flow/engine'\nimport type { FormFlow } from '../flow/types'\nimport type {\n\tFormButtonSettings,\n\tFormDocument,\n\tFormPollSettings,\n\tFormResponseSettings,\n} from '../form/types'\nimport {\n\tDEFAULT_PRESENTATION_NAME,\n\tdefaultPresentationDescriptors,\n} from '../presentations/defaults'\nimport { interpolate } from '../recall/interpolate'\nimport { buildRecallResolver, descriptorsFor } from '../recall/resolver'\nimport {\n\tCAPTCHA_TOKEN_KEY,\n\tCONTEXT_KEY,\n\tDEFAULT_HONEYPOT_FIELD,\n\tHONEYPOT_VALUE_KEY,\n} from '../spam/constants'\nimport type { FormFieldInstance, SubmissionValue } from '../submissions/types'\nimport { en } from '../translations/en'\nimport { keys } from '../translations/keys'\nimport { makeTranslate } from '../translations/makeTranslate'\nimport { resolveMessage } from '../validation/message'\nimport type { AnyValidationRuleDefinition } from '../validation/types'\nimport { defaultNavigate, type FormAdapters } from './adapters'\nimport { cn } from './cn'\nimport type { RendererTranslate } from './contract'\nimport { emitFormEvent } from './events'\nimport { FormContext, type FormContextValue, type FormStepInfo } from './FormContext'\nimport {\n\ttype BackButtonRenderProps,\n\tFormControls,\n\ttype NextButtonRenderProps,\n\ttype SubmitButtonRenderProps,\n} from './FormControls'\nimport { FormFields } from './FormFields'\nimport { Honeypot } from './Honeypot'\nimport { defaultPresentations } from './presentation/presentations'\nimport { type PresentationsConfig, resolvePresentations } from './presentation/registry'\nimport type { FormPresentation } from './presentation/types'\nimport { type RenderersConfig, resolveRenderers } from './registry'\nimport { defaultRenderers } from './renderers'\nimport { buildFieldTypeRegistry, buildValidationRuleRegistry, visibleFields } from './resolveForm'\nimport {\n\tDEFAULT_STEP_ID,\n\ttype FieldErrors,\n\ttype FormAction,\n\tformReducer,\n\tinitialFormState,\n\tseedFieldValues,\n} from './state'\nimport { type SubmitFormResult, type SubmitHandler, submitForm } from './submitForm'\nimport { validateFieldValue } from './validateField'\n\nexport type {\n\tBackButtonRenderProps,\n\tNextButtonRenderProps,\n\tSubmitButtonRenderProps,\n} from './FormControls'\n// FormResponseSettings, FormButtonSettings, FormPollSettings, and FormDocument live in\n// `../form/types` (no 'use client') so server code (e.g. `toFormDocument` in a Server Component)\n// can use them without pulling in this client module. Re-exported here so `./react` and existing\n// `from './Form'` imports keep working unchanged.\nexport type { FormButtonSettings, FormDocument, FormPollSettings, FormResponseSettings }\n\n/**\n * The success response passed to `onSuccess`, recall-resolved and (for a message) serialized with the\n * form's active converters, so a host can render or toast the resolved response without re-deriving it.\n */\nexport type FormSuccessResponse =\n\t| { type: 'message'; html?: string }\n\t| { type: 'redirect'; url?: string }\n\n/** The second argument to `onSuccess`: the resolved success response (an object, so it can grow). */\nexport type FormSuccessResult = { response?: FormSuccessResponse }\n\nexport type FormProps = {\n\tform: FormDocument\n\tfieldTypes?: AnyFormFieldDefinition[]\n\trules?: AnyValidationRuleDefinition[]\n\trenderers?: RenderersConfig\n\tapiRoute?: string\n\tonSubmit?: SubmitHandler\n\t/**\n\t * Called after a successful submission with the submission id and the resolved success response\n\t * (recall-applied, serialized with `converters`), so a host can toast or render it. Fires in both\n\t * `successBehavior` modes and on the custom-`children` path.\n\t */\n\tonSuccess?: (submissionId?: string, result?: FormSuccessResult) => void\n\tonError?: (message: string) => void\n\t/**\n\t * Custom Lexical block converters (e.g. host `icon`/`badge` blocks) spread over the defaults for the\n\t * client serializer, so those blocks survive in the success message and in the `onSuccess` response.\n\t */\n\tconverters?: Record<string, BodyConverter>\n\t/**\n\t * What happens on a successful submit. `'replace'` (default) swaps the form for the success screen;\n\t * `'reset'` clears the fields in place and shows no success screen, so a host can toast via `onSuccess`\n\t * and keep the form usable.\n\t */\n\tsuccessBehavior?: 'replace' | 'reset'\n\t/**\n\t * Client halves of the plugin's registered `calc.functions` (same keys), used for the live calc\n\t * preview only; the server always recomputes authoritatively at submit with the registry's own\n\t * functions.\n\t */\n\tcalcFunctions?: Record<string, (args: number[]) => number>\n\tevents?: FormEventSink\n\tt?: RendererTranslate\n\tlocale?: string\n\tlayout?: boolean\n\t/** Submit button label. Precedence: this prop, then the form's `buttons.submitLabel`, then the translated default. */\n\tsubmitLabel?: string\n\t/** \"Next\" button label for multi-step forms. Precedence: this prop, then the form's `buttons.nextLabel`, then the translated default. */\n\tnextLabel?: string\n\t/** \"Back\" button label for multi-step forms. Precedence: this prop, then the form's `buttons.prevLabel`, then the translated default. */\n\tprevLabel?: string\n\t/** Label for the overlay close control (modal/drawer). */\n\tcloseLabel?: string\n\tsuccessMessage?: string\n\t/** Active presentation: a name into the registry or an inline presentation. Defaults to `'page'` when omitted. */\n\tpresentation?: string | FormPresentation\n\t/** Per-render presentation overrides merged onto the defaults (add, replace, or `false` to remove). */\n\tpresentations?: PresentationsConfig\n\t/** Invoked when an overlay presentation dismisses (close button, Escape, outside click, or `dismissOnSuccess`). */\n\tonClose?: () => void\n\t/**\n\t * Accessible name for an overlay surface (modal/drawer). Hosts choosing between a trigger\n\t * label and the form's own admin title should prefer `form.title` when set, falling back to\n\t * their own label otherwise.\n\t */\n\ttitle?: string\n\t/** Seed initial field values (e.g. from `valuesFromSearchParams`). Still validated on submit. */\n\tinitialValues?: Record<string, unknown>\n\t/** Honeypot decoy (on by default). `false` removes it; `{ name }` matches a customized server `spam.honeypot.fieldName`. */\n\thoneypot?: false | { name?: string }\n\t/** A token from your captcha widget; verified server-side when a captcha provider is configured. */\n\tcaptchaToken?: string\n\t/**\n\t * A signed form-context token from `signFormContext` (server/RSC), tying this render to a document\n\t * (e.g. the profile page it is on). Carried alongside the answers, verified server-side, and\n\t * available to `email.recipientSources`; it is never stored as an answer.\n\t */\n\tcontext?: string\n\t/** Custom layout: render fields with `useField`/`useFormState` instead of the auto-rendered field loop. */\n\tchildren?: ReactNode\n\t/** Chrome rendered inside the form, above the fields, in default mode (e.g. `<FormSteps />`). */\n\theader?: ReactNode\n\t/** Additional CSS class names applied to the root `<form>` element (and the success node). */\n\tclassName?: string\n\t/** Replace the default submit button entirely. Receives the resolved label and submitting state. */\n\trenderSubmit?: (props: SubmitButtonRenderProps) => ReactNode\n\t/** Replace the default \"Next\" button in multi-step forms. */\n\trenderNext?: (props: NextButtonRenderProps) => ReactNode\n\t/** Replace the default \"Back\" button in multi-step forms. */\n\trenderBack?: (props: BackButtonRenderProps) => ReactNode\n\t/** CSS class forwarded to the default submit `<button>`. Ignored when `renderSubmit` is provided. */\n\tsubmitButtonClassName?: string\n\t/** CSS class forwarded to the default \"Next\" `<button>`. Ignored when `renderNext` is provided. */\n\tnextButtonClassName?: string\n\t/** CSS class forwarded to the default \"Back\" `<button>`. Ignored when `renderBack` is provided. */\n\tbackButtonClassName?: string\n\t/**\n\t * Host-owned effects (navigation, vote persistence, script loading). Each member defaults to\n\t * the built-in DOM behavior; see {@link FormAdapters}. Pass a stable reference (module scope or\n\t * `useMemo`), matching the other injectable props.\n\t */\n\tadapters?: FormAdapters\n}\n\nconst isEmpty = (value: unknown): boolean =>\n\tvalue == null || value === '' || (Array.isArray(value) && value.length === 0)\n\n/** Visually hidden but screen-reader-announced, for the \"Step X of Y\" live region (no host CSS required). */\nconst SR_ONLY: CSSProperties = {\n\tposition: 'absolute',\n\twidth: 1,\n\theight: 1,\n\tpadding: 0,\n\tmargin: -1,\n\toverflow: 'hidden',\n\tclip: 'rect(0, 0, 0, 0)',\n\twhiteSpace: 'nowrap',\n\tborder: 0,\n}\n\n/** The base field name of an error key: the part before a repeater composite suffix (`name[0].sub`). */\nconst baseFieldKey = (key: string): string => {\n\tconst bracket = key.indexOf('[')\n\treturn bracket === -1 ? key : key.slice(0, bracket)\n}\n\n/** The id of the first flow step (in order) that owns an errored field, or undefined when none does. */\nconst firstStepWithError = (flow: FormFlow, errors: FieldErrors): string | undefined => {\n\tconst errored = new Set(Object.keys(errors).map(baseFieldKey))\n\treturn flow.steps.find((flowStep) => flowStep.fields.some((key) => errored.has(key)))?.id\n}\n\n/** The first focusable element under `root` that a step transition should land on (skips hidden/honeypot). */\nconst focusFirstIn = (root: HTMLElement | null, selector: string): void => {\n\tconst target = root?.querySelector<HTMLElement>(selector)\n\ttarget?.focus()\n}\n\n/** A stored button label counts only when it is a non-empty string; anything else falls through. */\nconst storedLabel = (value: unknown): string | undefined =>\n\ttypeof value === 'string' && value.length > 0 ? value : undefined\n\n/** The headless form controller: state, progressive client validation, conditional visibility, submission, events. */\nexport const Form = ({\n\tform,\n\tfieldTypes,\n\trules,\n\trenderers,\n\tapiRoute,\n\tonSubmit,\n\tonSuccess,\n\tonError,\n\tconverters,\n\tsuccessBehavior = 'replace',\n\tcalcFunctions,\n\tevents,\n\tt,\n\tlocale = 'en',\n\tlayout,\n\tsubmitLabel,\n\tnextLabel,\n\tprevLabel,\n\tcloseLabel,\n\tsuccessMessage,\n\tpresentation,\n\tpresentations,\n\tonClose,\n\ttitle,\n\tinitialValues,\n\thoneypot,\n\tcaptchaToken,\n\tcontext,\n\tchildren,\n\theader,\n\tclassName,\n\trenderSubmit,\n\trenderNext,\n\trenderBack,\n\tsubmitButtonClassName,\n\tnextButtonClassName,\n\tbackButtonClassName,\n\tadapters,\n}: FormProps) => {\n\tconst honeypotName = honeypot === false ? null : (honeypot?.name ?? DEFAULT_HONEYPOT_FIELD)\n\tconst honeypotRef = useRef<HTMLInputElement>(null)\n\tconst registry = useMemo(() => buildFieldTypeRegistry(fieldTypes), [fieldTypes])\n\tconst ruleRegistry = useMemo(() => buildValidationRuleRegistry(rules), [rules])\n\tconst rendererRegistry = useMemo(() => resolveRenderers(defaultRenderers, renderers), [renderers])\n\tconst presentationRegistry = useMemo(\n\t\t() => resolvePresentations(defaultPresentations, presentations),\n\t\t[presentations]\n\t)\n\tconst activePresentation: FormPresentation =\n\t\ttypeof presentation === 'object'\n\t\t\t? presentation\n\t\t\t: (presentationRegistry.get(presentation ?? DEFAULT_PRESENTATION_NAME) ??\n\t\t\t\tpresentationRegistry.get(DEFAULT_PRESENTATION_NAME) ??\n\t\t\t\tdefaultPresentationDescriptors.page)\n\tconst fieldsByName = useMemo(\n\t\t() => new Map(form.fields.filter(isNamedField).map((field) => [field.name, field])),\n\t\t[form.fields]\n\t)\n\tconst translate = useMemo<RendererTranslate>(() => t ?? makeTranslate(en), [t])\n\tconst resolvedCloseLabel = closeLabel ?? translate(keys.formClose)\n\tconst resolvedSuccessMessage = successMessage ?? translate(keys.formSuccess)\n\tconst docButtons: FormButtonSettings | undefined = form.buttons\n\tconst labels = useMemo(\n\t\t() => ({\n\t\t\tprev: prevLabel ?? storedLabel(docButtons?.prevLabel) ?? translate(keys.formBack),\n\t\t\tnext: nextLabel ?? storedLabel(docButtons?.nextLabel) ?? translate(keys.formNext),\n\t\t\tsubmit: submitLabel ?? storedLabel(docButtons?.submitLabel) ?? translate(keys.formSubmit),\n\t\t}),\n\t\t[prevLabel, nextLabel, submitLabel, docButtons, translate]\n\t)\n\n\t// Latest-value refs so event emission and the mount/unmount effect tolerate an inline `events` prop or a changing form id.\n\tconst sinkRef = useRef<FormEventSink>(noopEventSink)\n\tsinkRef.current = events ?? noopEventSink\n\tconst formIdRef = useRef('')\n\tformIdRef.current = String(form.id)\n\n\tconst [state, rawDispatch] = useReducer(formReducer, form.fields, (fields) =>\n\t\tinitialFormState({\n\t\t\t...seedFieldValues(fields),\n\t\t\t...(initialValues ?? {}),\n\t\t})\n\t)\n\n\t// Authoritative values for derived (calc) fields, recomputed from user answers on every change. The\n\t// server recomputes these too at submit; the client copy drives the live calc renderer, recall, and\n\t// submit. Server-resolved source values ride the document (`calcResolved`); custom function halves\n\t// come from the `calcFunctions` prop (functions never serialize).\n\tconst effectiveValues = useMemo(\n\t\t() =>\n\t\t\tcomputeCalcFields(form.fields, state.values, {\n\t\t\t\t...(form.calcResolved ?? {}),\n\t\t\t\t...(calcFunctions ? { functions: calcFunctions } : {}),\n\t\t\t}),\n\t\t[form.fields, state.values, form.calcResolved, calcFunctions]\n\t)\n\n\t// The calc fields' slice of effectiveValues, exposed on context for composed frontends.\n\tconst calcValues = useMemo(() => {\n\t\tconst values: Record<string, number> = {}\n\t\tfor (const field of form.fields) {\n\t\t\tif (calcExpressionOf(field) && isNamedField(field)) {\n\t\t\t\tconst computed = effectiveValues[field.name]\n\t\t\t\tvalues[field.name] = typeof computed === 'number' ? computed : 0\n\t\t\t}\n\t\t}\n\t\treturn values\n\t}, [form.fields, effectiveValues])\n\n\tconst recall = useMemo(\n\t\t() =>\n\t\t\tbuildRecallResolver({\n\t\t\t\tfields: form.fields,\n\t\t\t\tvalues: effectiveValues,\n\t\t\t\tregistry,\n\t\t\t\tlocale,\n\t\t\t\tt: translate,\n\t\t\t}),\n\t\t[form.fields, effectiveValues, registry, locale, translate]\n\t)\n\n\t// Multi-step is active only when the form is flagged `multistep` and its flow declares two or more\n\t// steps. With the flag off the form renders as a single page even if flow data is still stored.\n\tconst flow =\n\t\tform.multistep === true && form.flow && form.flow.steps.length >= 2 ? form.flow : undefined\n\tconst [currentStepId, setCurrentStepId] = useState<string | undefined>(() =>\n\t\tflow ? firstStepId(flow) : undefined\n\t)\n\tconst [history, setHistory] = useState<string[]>([])\n\n\tconst startedRef = useRef(false)\n\tconst submittedRef = useRef(false)\n\tconst submittingRef = useRef(false)\n\tconst advancingRef = useRef(false)\n\tconst flowRef = useRef(flow)\n\tflowRef.current = flow\n\n\t// Per-step error reveal: a field maps to the id of the flow step whose `fields` include it, else the\n\t// default step (single-step forms, or a field assigned to no step), so a single-step form collapses to\n\t// one step and today's behavior.\n\tconst stepIdOfField = useMemo(() => {\n\t\tconst map = new Map<string, string>()\n\t\tif (flow) {\n\t\t\tfor (const flowStep of flow.steps) {\n\t\t\t\tfor (const key of flowStep.fields) {\n\t\t\t\t\tmap.set(key, flowStep.id)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn (fieldKey: string): string => map.get(fieldKey) ?? DEFAULT_STEP_ID\n\t}, [flow])\n\tconst allStepIds = useMemo(\n\t\t() => (flow ? flow.steps.map((flowStep) => flowStep.id) : [DEFAULT_STEP_ID]),\n\t\t[flow]\n\t)\n\n\t// Focus management for step transitions and blocked advances/submits. A pending request is performed\n\t// by an effect after the render it triggers, so focus lands on the DOM that reflects the new state.\n\tconst formRef = useRef<HTMLFormElement>(null)\n\tconst pendingFocusRef = useRef<'stepStart' | 'firstInvalid' | null>(null)\n\tconst [focusNonce, setFocusNonce] = useState(0)\n\tconst requestFocus = (intent: 'stepStart' | 'firstInvalid') => {\n\t\tpendingFocusRef.current = intent\n\t\tsetFocusNonce((nonce) => nonce + 1)\n\t}\n\n\tconst dispatch = useCallback((action: FormAction) => {\n\t\tif (action.type === 'SET_VALUE' && !startedRef.current) {\n\t\t\tstartedRef.current = true\n\t\t\temitFormEvent(sinkRef.current, formIdRef.current, { type: 'form.started' })\n\t\t}\n\t\trawDispatch(action)\n\t}, [])\n\n\tconst validateField = useCallback(\n\t\t(name: string, value: unknown) => {\n\t\t\tconst field = fieldsByName.get(name)\n\t\t\tif (!field) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst answers = { ...effectiveValues, [name]: value }\n\t\t\t// Mirror the server: a field whose `validateWhen` is unmet is not validated; clear any stale error.\n\t\t\tif (!evaluateCondition(field.validateWhen, answers)) {\n\t\t\t\trawDispatch({ type: 'SET_FIELD_ISSUES', name, errors: [] })\n\t\t\t\treturn\n\t\t\t}\n\t\t\tvoid validateFieldValue({\n\t\t\t\tfield,\n\t\t\t\tvalue,\n\t\t\t\tregistry,\n\t\t\t\truleRegistry,\n\t\t\t\tanswers,\n\t\t\t\tlocale,\n\t\t\t\tt: translate,\n\t\t\t}).then(({ errors }) => {\n\t\t\t\trawDispatch({ type: 'SET_FIELD_ISSUES', name, errors })\n\t\t\t\tconst [firstError] = errors\n\t\t\t\tif (firstError !== undefined) {\n\t\t\t\t\temitFormEvent(sinkRef.current, formIdRef.current, {\n\t\t\t\t\t\ttype: 'field.errored',\n\t\t\t\t\t\tfield: name,\n\t\t\t\t\t\tmessage: firstError,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\t\t[fieldsByName, effectiveValues, registry, ruleRegistry, locale, translate]\n\t)\n\n\tconst visible = visibleFields(form.fields, effectiveValues)\n\n\t/** Visible, answered named fields. Display-only ('none' kind) and nameless (bare) fields never contribute. */\n\tconst answerableFields = (): NamedFormFieldInstance[] =>\n\t\tvisibleFields(form.fields, effectiveValues)\n\t\t\t.filter((field) => registry.get(field.blockType)?.value !== 'none')\n\t\t\t.filter(isNamedField)\n\t\t\t.filter((field) => !isEmpty(effectiveValues[field.name]))\n\n\t/** Answered visible fields as raw submission values, sent to the server on submit. */\n\tconst answeredValues = (): SubmissionValue[] =>\n\t\tanswerableFields().map((field) => ({ field: field.name, value: effectiveValues[field.name] }))\n\n\t/**\n\t * Same fields, formatted via `recall` (option labels, Yes/No, localized dates): recall-fidelity\n\t * values for client-rendered templates, matching what the server gives email-team's body.\n\t */\n\tconst formattedValues = (): SubmissionValue[] =>\n\t\tanswerableFields().map((field) => ({ field: field.name, value: recall(field.name) }))\n\n\t/**\n\t * The recall-resolved success response: a redirect's url, or the response message serialized with\n\t * the active `converters` (so host blocks survive). Shared by the success screen and the value\n\t * handed to `onSuccess`, so both stay identical.\n\t */\n\tconst resolveSuccessResponse = (): FormSuccessResponse | undefined => {\n\t\tconst response = form.response\n\t\tif (response?.type === 'redirect') {\n\t\t\treturn { type: 'redirect', url: response.redirect?.url ?? undefined }\n\t\t}\n\t\tconst message =\n\t\t\tresponse?.type === 'message' || response?.type == null ? response?.message : undefined\n\t\tif (!message) {\n\t\t\treturn undefined\n\t\t}\n\t\treturn {\n\t\t\ttype: 'message',\n\t\t\thtml: serializeBody(message, {\n\t\t\t\tvalues: formattedValues(),\n\t\t\t\tdescriptors: descriptorsFor(answerableFields()),\n\t\t\t\tconverters,\n\t\t\t}),\n\t\t}\n\t}\n\n\t// Steps address fields by key: machine names for named fields, block row ids for bare blocks.\n\t// `step.fields` is membership only; render order follows `form.fields` (the order of `visible`),\n\t// so a step shows its fields in the form's field order, not the flow-builder entry order.\n\tconst stepKeys = flow && currentStepId ? stepFieldNames(flow, currentStepId) : []\n\tconst stepKeySet = new Set(stepKeys)\n\tconst stepVisible: FormFieldInstance[] = visible.filter((field) =>\n\t\tstepKeySet.has(fieldKey(field))\n\t)\n\n\t// Validate the sub-fields of the given repeaters, one pass per visible row, returning composite-key\n\t// errors (`fieldName[rowIndex].subFieldName`). Shared by submit and step navigation so both mirror\n\t// the server's per-row pass and neither lets an invalid required sub-field slip through.\n\tconst validateRepeaterSubFields = async (\n\t\trepeaters: FormFieldInstance[]\n\t): Promise<FieldErrors> => {\n\t\tconst errors: FieldErrors = {}\n\t\tfor (const field of repeaters.filter(\n\t\t\t(f): f is NamedFormFieldInstance => f.blockType === 'repeater' && isNamedField(f)\n\t\t)) {\n\t\t\tconst rows = Array.isArray(effectiveValues[field.name])\n\t\t\t\t? (effectiveValues[field.name] as Array<Record<string, unknown>>)\n\t\t\t\t: []\n\t\t\tconst subFields = (\n\t\t\t\tArray.isArray(field.subFields) ? (field.subFields as FormFieldInstance[]) : []\n\t\t\t).filter(isNamedField)\n\t\t\tfor (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {\n\t\t\t\tconst row = rows[rowIndex] ?? {}\n\t\t\t\tfor (const subField of subFields) {\n\t\t\t\t\tif (!evaluateCondition(subField.visibleWhen, row)) continue\n\t\t\t\t\tif (!evaluateCondition(subField.validateWhen, row)) continue\n\t\t\t\t\tconst subResult = await validateFieldValue({\n\t\t\t\t\t\tfield: subField,\n\t\t\t\t\t\tvalue: row[subField.name],\n\t\t\t\t\t\tregistry,\n\t\t\t\t\t\truleRegistry,\n\t\t\t\t\t\tanswers: row,\n\t\t\t\t\t\tlocale,\n\t\t\t\t\t\tt: translate,\n\t\t\t\t\t})\n\t\t\t\t\tconst compositeKey = `${field.name}[${rowIndex}].${subField.name}`\n\t\t\t\t\tif (subResult.errors.length > 0) errors[compositeKey] = subResult.errors\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn errors\n\t}\n\n\tconst goNext = async () => {\n\t\t// Re-entrancy guard: a double-click during async validation must not push the same step onto\n\t\t// history twice (which would need two Back presses to undo).\n\t\tif (!flow || !currentStepId || advancingRef.current) {\n\t\t\treturn\n\t\t}\n\t\tadvancingRef.current = true\n\t\ttry {\n\t\t\tconst results = await Promise.all(\n\t\t\t\tstepVisible\n\t\t\t\t\t.filter((field) => !calcExpressionOf(field))\n\t\t\t\t\t.filter((field) => registry.get(field.blockType)?.value !== 'none')\n\t\t\t\t\t.filter(isNamedField)\n\t\t\t\t\t.filter((field) => evaluateCondition(field.validateWhen, effectiveValues))\n\t\t\t\t\t.map(async (field) => ({\n\t\t\t\t\t\tfield,\n\t\t\t\t\t\t...(await validateFieldValue({\n\t\t\t\t\t\t\tfield,\n\t\t\t\t\t\t\tvalue: effectiveValues[field.name],\n\t\t\t\t\t\t\tregistry,\n\t\t\t\t\t\t\truleRegistry,\n\t\t\t\t\t\t\tanswers: effectiveValues,\n\t\t\t\t\t\t\tlocale,\n\t\t\t\t\t\t\tt: translate,\n\t\t\t\t\t\t})),\n\t\t\t\t\t}))\n\t\t\t)\n\t\t\tlet hasError = false\n\t\t\tfor (const result of results) {\n\t\t\t\trawDispatch({ type: 'TOUCH', name: result.field.name })\n\t\t\t\trawDispatch({\n\t\t\t\t\ttype: 'SET_FIELD_ISSUES',\n\t\t\t\t\tname: result.field.name,\n\t\t\t\t\terrors: result.errors,\n\t\t\t\t})\n\t\t\t\tif (result.errors.length > 0) {\n\t\t\t\t\thasError = true\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Mirror submit: validate the step's repeater sub-fields too, so a required sub-field can't be\n\t\t\t// skipped past on a non-terminal step (errors surface inline via the composite key).\n\t\t\tconst repeaterErrors = await validateRepeaterSubFields(stepVisible)\n\t\t\tfor (const [compositeKey, errs] of Object.entries(repeaterErrors)) {\n\t\t\t\trawDispatch({ type: 'SET_FIELD_ISSUES', name: compositeKey, errors: errs })\n\t\t\t\thasError = true\n\t\t\t}\n\t\t\tif (hasError) {\n\t\t\t\t// Mark this step attempted so its fields reveal their errors, then focus the first invalid one.\n\t\t\t\trawDispatch({ type: 'MARK_STEP_ATTEMPTED', stepId: currentStepId })\n\t\t\t\trequestFocus('firstInvalid')\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst next = resolveNextStepId(flow, currentStepId, effectiveValues)\n\t\t\tif (!next) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\temitFormEvent(sinkRef.current, formIdRef.current, {\n\t\t\t\ttype: 'step.completed',\n\t\t\t\tstepId: currentStepId,\n\t\t\t})\n\t\t\tsetHistory((prev) => [...prev, currentStepId])\n\t\t\tsetCurrentStepId(next)\n\t\t\trequestFocus('stepStart')\n\t\t\temitFormEvent(sinkRef.current, formIdRef.current, { type: 'step.viewed', stepId: next })\n\t\t} finally {\n\t\t\tadvancingRef.current = false\n\t\t}\n\t}\n\n\tconst goBack = () => {\n\t\tconst prev = history[history.length - 1]\n\t\tif (prev === undefined) {\n\t\t\treturn\n\t\t}\n\t\tsetHistory((entries) => entries.slice(0, -1))\n\t\tsetCurrentStepId(prev)\n\t\trequestFocus('stepStart')\n\t\temitFormEvent(sinkRef.current, formIdRef.current, { type: 'step.viewed', stepId: prev })\n\t}\n\n\t// Jump to an earlier step (from the terminal Submit when an earlier step is invalid), rebuilding the\n\t// Back stack as the flow's linear path up to it so Back still works.\n\tconst navigateToStep = (stepId: string) => {\n\t\tif (!flow || stepId === currentStepId) {\n\t\t\treturn\n\t\t}\n\t\tconst idx = flow.steps.findIndex((flowStep) => flowStep.id === stepId)\n\t\tif (idx < 0) {\n\t\t\treturn\n\t\t}\n\t\tsetHistory(flow.steps.slice(0, idx).map((flowStep) => flowStep.id))\n\t\tsetCurrentStepId(stepId)\n\t\temitFormEvent(sinkRef.current, formIdRef.current, { type: 'step.viewed', stepId })\n\t}\n\n\t// Perform a pending focus request after the render it triggered, so it lands on the DOM that reflects\n\t// the new state (a changed step, or freshly revealed errors). Null on mount, so the first render never\n\t// steals focus. currentStepId and focusNonce are intentional re-run triggers: a blocked advance keeps\n\t// the same step, so the nonce forces a fresh run; the body reads only refs, hence the ignore.\n\t// biome-ignore lint/correctness/useExhaustiveDependencies: currentStepId/focusNonce are re-run triggers, not read in the body\n\tuseEffect(() => {\n\t\tconst intent = pendingFocusRef.current\n\t\tif (!intent) {\n\t\t\treturn\n\t\t}\n\t\tpendingFocusRef.current = null\n\t\tif (intent === 'firstInvalid') {\n\t\t\tfocusFirstIn(formRef.current, '[aria-invalid=\"true\"]')\n\t\t\treturn\n\t\t}\n\t\tconst region = formRef.current?.querySelector<HTMLElement>('[data-fb-step-region]')\n\t\tif (region) {\n\t\t\tregion.focus()\n\t\t} else {\n\t\t\tfocusFirstIn(formRef.current, 'input:not([type=\"hidden\"]), select, textarea')\n\t\t}\n\t}, [currentStepId, focusNonce])\n\n\tuseEffect(() => {\n\t\temitFormEvent(sinkRef.current, formIdRef.current, { type: 'form.viewed' })\n\t\tconst mountFlow = flowRef.current\n\t\tif (mountFlow) {\n\t\t\tconst first = firstStepId(mountFlow)\n\t\t\tif (first) {\n\t\t\t\temitFormEvent(sinkRef.current, formIdRef.current, { type: 'step.viewed', stepId: first })\n\t\t\t}\n\t\t}\n\t\treturn () => {\n\t\t\tif (!submittedRef.current && !submittingRef.current) {\n\t\t\t\temitFormEvent(sinkRef.current, formIdRef.current, { type: 'form.abandoned' })\n\t\t\t}\n\t\t}\n\t}, [])\n\n\tconst handleClose = useCallback(() => {\n\t\tonClose?.()\n\t}, [onClose])\n\n\tconst handleSubmit = async (event: ReactFormEvent<HTMLFormElement>) => {\n\t\tevent.preventDefault()\n\t\t// Re-entrancy guard: claim the in-flight slot before the async validation window so a fast second\n\t\t// activation (double-click, Enter + click) cannot reach the transport and POST the submission twice.\n\t\tif (submittingRef.current) {\n\t\t\treturn\n\t\t}\n\t\tsubmittingRef.current = true\n\t\tconst visible = visibleFields(form.fields, effectiveValues)\n\t\tconst results = await Promise.all(\n\t\t\t// Calc fields carry no rules and have no input; they are always satisfied, so skip validating them.\n\t\t\t// Display-only ('none' kind, e.g. message) and nameless (bare) fields are skipped too, mirroring the server.\n\t\t\t// A field whose `validateWhen` is unmet is skipped too, mirroring the server (no client/server divergence).\n\t\t\tvisible\n\t\t\t\t.filter((field) => !calcExpressionOf(field))\n\t\t\t\t.filter((field) => registry.get(field.blockType)?.value !== 'none')\n\t\t\t\t.filter(isNamedField)\n\t\t\t\t.filter((field) => evaluateCondition(field.validateWhen, effectiveValues))\n\t\t\t\t.map(async (field) => ({\n\t\t\t\t\tfield,\n\t\t\t\t\t...(await validateFieldValue({\n\t\t\t\t\t\tfield,\n\t\t\t\t\t\tvalue: effectiveValues[field.name],\n\t\t\t\t\t\tregistry,\n\t\t\t\t\t\truleRegistry,\n\t\t\t\t\t\tanswers: effectiveValues,\n\t\t\t\t\t\tlocale,\n\t\t\t\t\t\tt: translate,\n\t\t\t\t\t})),\n\t\t\t\t}))\n\t\t)\n\t\tconst errors: FieldErrors = {}\n\t\tfor (const result of results) {\n\t\t\tif (result.errors.length > 0) {\n\t\t\t\terrors[result.field.name] = result.errors\n\t\t\t\tconst [firstError] = result.errors\n\t\t\t\tif (firstError !== undefined) {\n\t\t\t\t\temitFormEvent(sinkRef.current, formIdRef.current, {\n\t\t\t\t\t\ttype: 'field.errored',\n\t\t\t\t\t\tfield: result.field.name,\n\t\t\t\t\t\tmessage: firstError,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Validate sub-fields within each visible repeater (composite keys `fieldName[rowIndex].subFieldName`),\n\t\t// mirroring the server's per-row pass, so the repeater renderer surfaces them inline.\n\t\tObject.assign(errors, await validateRepeaterSubFields(visible))\n\n\t\tconst hasErrors = Object.keys(errors).length > 0\n\t\t// A terminal submit validates every step, so every step is attempted (its errors may now reveal);\n\t\t// a valid submit clears stale errors without marking anything.\n\t\trawDispatch({ type: 'SET_ALL_ISSUES', errors, steps: hasErrors ? allStepIds : [] })\n\t\tif (hasErrors) {\n\t\t\tsubmittingRef.current = false\n\t\t\t// On a multi-step form, route to the first step that owns an invalid field rather than failing in\n\t\t\t// place on the terminal step, then focus its first invalid field.\n\t\t\tif (flow && currentStepId) {\n\t\t\t\tconst target = firstStepWithError(flow, errors)\n\t\t\t\tif (target) {\n\t\t\t\t\tnavigateToStep(target)\n\t\t\t\t}\n\t\t\t}\n\t\t\trequestFocus('firstInvalid')\n\t\t\treturn\n\t\t}\n\t\trawDispatch({ type: 'SUBMIT_START' })\n\t\tconst values: SubmissionValue[] = answeredValues()\n\t\tif (honeypotName) {\n\t\t\tconst decoy = honeypotRef.current?.value ?? ''\n\t\t\tif (decoy !== '') {\n\t\t\t\t// Submit the decoy under a reserved key, not its cosmetic DOM name, so a real field sharing\n\t\t\t\t// that name is never stripped or mistaken for the honeypot on the server.\n\t\t\t\tvalues.push({ field: HONEYPOT_VALUE_KEY, value: decoy })\n\t\t\t}\n\t\t}\n\t\tif (captchaToken) {\n\t\t\tvalues.push({ field: CAPTCHA_TOKEN_KEY, value: captchaToken })\n\t\t}\n\t\tif (context) {\n\t\t\t// Rides under a reserved key like the captcha token: verified and stored separately server-side,\n\t\t\t// never merged into the answers.\n\t\t\tvalues.push({ field: CONTEXT_KEY, value: context })\n\t\t}\n\t\tconst result: SubmitFormResult = onSubmit\n\t\t\t? await onSubmit({ formId: form.id, values })\n\t\t\t: await submitForm({ formId: form.id, values, apiRoute })\n\t\tsubmittingRef.current = false\n\t\tif (result.ok) {\n\t\t\t// A submission happened, so the unmount effect must not emit `form.abandoned`, in either mode.\n\t\t\tsubmittedRef.current = true\n\t\t\temitFormEvent(sinkRef.current, formIdRef.current, {\n\t\t\t\ttype: 'submission.created',\n\t\t\t\tsubmissionId: result.submissionId,\n\t\t\t})\n\t\t\t// Resolve the response before a reset clears the answers the recall reads from.\n\t\t\tonSuccess?.(result.submissionId, { response: resolveSuccessResponse() })\n\t\t\tif (successBehavior === 'reset') {\n\t\t\t\t// Reset in place: the host handles feedback (e.g. a toast via onSuccess); no success screen.\n\t\t\t\trawDispatch({\n\t\t\t\t\ttype: 'RESET',\n\t\t\t\t\tvalues: { ...seedFieldValues(form.fields), ...(initialValues ?? {}) },\n\t\t\t\t})\n\t\t\t\t// The reducer holds only values/errors; flow position and the lifecycle `started` ref live\n\t\t\t\t// outside it. Reset them too so a multi-step form returns to its first step (not stranded on\n\t\t\t\t// the terminal one) and a fresh fill re-emits `form.started`. `submittedRef` stays set: a\n\t\t\t\t// submission did happen, so the unmount guard must not report the completed form abandoned.\n\t\t\t\tif (flow) {\n\t\t\t\t\tsetCurrentStepId(firstStepId(flow))\n\t\t\t\t\tsetHistory([])\n\t\t\t\t}\n\t\t\t\tstartedRef.current = false\n\t\t\t} else {\n\t\t\t\trawDispatch({ type: 'SUBMIT_SUCCESS' })\n\t\t\t\tif (activePresentation.dismissOnSuccess) {\n\t\t\t\t\thandleClose()\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst redirectUrl =\n\t\t\t\tform.response?.type === 'redirect' ? form.response.redirect?.url : undefined\n\t\t\t// Part of submit handling, not rendering: fires on the custom-`children` path too. With a\n\t\t\t// host `navigate` adapter the plugin never touches window.location (no fallback, a throw\n\t\t\t// propagates); push semantics stay the default, passed as intent so the adapter decides how.\n\t\t\tif (typeof redirectUrl === 'string' && redirectUrl.length > 0) {\n\t\t\t\tconst navigate = adapters?.navigate ?? defaultNavigate\n\t\t\t\tnavigate(redirectUrl, { replace: false })\n\t\t\t}\n\t\t} else {\n\t\t\tif (result.fieldErrors) {\n\t\t\t\trawDispatch({ type: 'SET_ALL_ISSUES', errors: result.fieldErrors, steps: allStepIds })\n\t\t\t}\n\t\t\tconst message = result.message ?? translate(keys.formSubmitFailed)\n\t\t\trawDispatch({ type: 'SUBMIT_ERROR', message })\n\t\t\tonError?.(message)\n\t\t}\n\t}\n\n\tconst isTerminalStep =\n\t\tflow && currentStepId ? isTerminalStepId(flow, currentStepId, effectiveValues) : true\n\n\t// Enter on a multi-step form: on a non-terminal step, advance like Next (validate, then advance or\n\t// reveal + focus); on the terminal step, fall through to native submit (guarded by `submittingRef`).\n\t// A textarea (Enter = newline), select (confirm), and buttons/submit inputs (activation) are exempt.\n\t// Single-step forms keep native Enter-to-submit. The `<form>` is the plugin's even in children mode,\n\t// so this is the only place a host could get this behavior.\n\tconst handleKeyDown = (event: ReactKeyboardEvent<HTMLFormElement>) => {\n\t\tif (!flow || event.key !== 'Enter') {\n\t\t\treturn\n\t\t}\n\t\tconst target = event.target\n\t\tif (\n\t\t\ttarget instanceof HTMLTextAreaElement ||\n\t\t\ttarget instanceof HTMLSelectElement ||\n\t\t\ttarget instanceof HTMLButtonElement\n\t\t) {\n\t\t\treturn\n\t\t}\n\t\tif (\n\t\t\ttarget instanceof HTMLInputElement &&\n\t\t\t(target.type === 'submit' || target.type === 'button')\n\t\t) {\n\t\t\treturn\n\t\t}\n\t\tif (!isTerminalStep) {\n\t\t\tevent.preventDefault()\n\t\t\tvoid goNext()\n\t\t}\n\t}\n\n\tconst step: FormStepInfo = flow\n\t\t? {\n\t\t\t\tflow,\n\t\t\t\tcurrentStepId,\n\t\t\t\tstepIndex: flow.steps.findIndex((s) => s.id === currentStepId),\n\t\t\t\tstepCount: flow.steps.length,\n\t\t\t\tisFirst: history.length === 0,\n\t\t\t\tisTerminal: isTerminalStep,\n\t\t\t\tgoNext: () => {\n\t\t\t\t\tvoid goNext()\n\t\t\t\t},\n\t\t\t\tgoBack,\n\t\t\t}\n\t\t: {\n\t\t\t\tstepIndex: 0,\n\t\t\t\tstepCount: 1,\n\t\t\t\tisFirst: true,\n\t\t\t\tisTerminal: true,\n\t\t\t\tgoNext: () => {},\n\t\t\t\tgoBack: () => {},\n\t\t\t}\n\n\t// The \"Step X of Y\" announcement (aria-live + the step region's accessible name).\n\tconst stepStatusText = flow\n\t\t? resolveMessage(translate(keys.formStepStatus), {\n\t\t\t\tcurrent: String(step.stepIndex + 1),\n\t\t\t\ttotal: String(step.stepCount),\n\t\t\t})\n\t\t: ''\n\t// Whether the current step has been attempted and still has a revealed error (drives the step-level alert).\n\tconst currentStepHasError =\n\t\tflow != null &&\n\t\tcurrentStepId != null &&\n\t\tstate.attemptedSteps.has(currentStepId) &&\n\t\tObject.entries(state.errors).some(\n\t\t\t([key, errs]) => errs.length > 0 && stepIdOfField(baseFieldKey(key)) === currentStepId\n\t\t)\n\n\tconst renderedFields = (flow ? stepVisible : visible).filter(\n\t\t(field) => field.hidden !== true && field.calcDisplay !== false\n\t)\n\n\tconst contextValue: FormContextValue = {\n\t\tform,\n\t\tstate,\n\t\tdispatch,\n\t\tvalidateField,\n\t\tlocale,\n\t\tstep,\n\t\trendererRegistry,\n\t\tlabels,\n\t\tt: translate,\n\t\teffectiveValues,\n\t\tcalcValues,\n\t\trecall,\n\t\trenderedFields,\n\t\tconverters,\n\t\tstepIdOfField,\n\t}\n\n\tconst PresentationWrapper = activePresentation.Wrapper\n\tconst wrap = (content: ReactNode): ReactNode =>\n\t\tPresentationWrapper ? (\n\t\t\t<PresentationWrapper\n\t\t\t\tpresentation={activePresentation}\n\t\t\t\topen\n\t\t\t\tonClose={handleClose}\n\t\t\t\ttitle={title}\n\t\t\t\tcloseLabel={resolvedCloseLabel}\n\t\t\t>\n\t\t\t\t{content}\n\t\t\t</PresentationWrapper>\n\t\t) : (\n\t\t\tcontent\n\t\t)\n\n\tif (children !== undefined) {\n\t\treturn (\n\t\t\t<FormContext.Provider value={contextValue}>\n\t\t\t\t{wrap(\n\t\t\t\t\t<form\n\t\t\t\t\t\tref={formRef}\n\t\t\t\t\t\tclassName={cn('fb-form-root', className)}\n\t\t\t\t\t\tnoValidate\n\t\t\t\t\t\tonSubmit={handleSubmit}\n\t\t\t\t\t\tonKeyDown={handleKeyDown}\n\t\t\t\t\t\tdata-fb-presentation={activePresentation.name}\n\t\t\t\t\t\tdata-fb-density={activePresentation.density}\n\t\t\t\t\t>\n\t\t\t\t\t\t{honeypotName ? <Honeypot name={honeypotName} inputRef={honeypotRef} /> : null}\n\t\t\t\t\t\t{children}\n\t\t\t\t\t</form>\n\t\t\t\t)}\n\t\t\t</FormContext.Provider>\n\t\t)\n\t}\n\n\tif (state.submitted) {\n\t\tconst successResponse = resolveSuccessResponse()\n\t\tconst responseHtml = successResponse?.type === 'message' ? successResponse.html : undefined\n\t\treturn (\n\t\t\t<FormContext.Provider value={contextValue}>\n\t\t\t\t{wrap(\n\t\t\t\t\tresponseHtml ? (\n\t\t\t\t\t\t// Safe to inject: serializeBody HTML-escapes all text (recall values included) and sanitizes link URLs.\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\trole=\"status\"\n\t\t\t\t\t\t\tclassName={cn('fb-form__success', className)}\n\t\t\t\t\t\t\tdata-fb-presentation={activePresentation.name}\n\t\t\t\t\t\t\tdata-fb-density={activePresentation.density}\n\t\t\t\t\t\t\t// biome-ignore lint/security/noDangerouslySetInnerHtml: HTML is produced by our escaping serializer, never raw user input\n\t\t\t\t\t\t\tdangerouslySetInnerHTML={{ __html: responseHtml }}\n\t\t\t\t\t\t/>\n\t\t\t\t\t) : (\n\t\t\t\t\t\t<p\n\t\t\t\t\t\t\trole=\"status\"\n\t\t\t\t\t\t\tclassName={cn('fb-form__success', className)}\n\t\t\t\t\t\t\tdata-fb-presentation={activePresentation.name}\n\t\t\t\t\t\t\tdata-fb-density={activePresentation.density}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{interpolate(resolvedSuccessMessage, recall)}\n\t\t\t\t\t\t</p>\n\t\t\t\t\t)\n\t\t\t\t)}\n\t\t\t</FormContext.Provider>\n\t\t)\n\t}\n\n\treturn (\n\t\t<FormContext.Provider value={contextValue}>\n\t\t\t{wrap(\n\t\t\t\t<form\n\t\t\t\t\tref={formRef}\n\t\t\t\t\tclassName={cn('fb-form-root', className)}\n\t\t\t\t\tnoValidate\n\t\t\t\t\tonSubmit={handleSubmit}\n\t\t\t\t\tonKeyDown={handleKeyDown}\n\t\t\t\t\tdata-fb-presentation={activePresentation.name}\n\t\t\t\t\tdata-fb-density={activePresentation.density}\n\t\t\t\t>\n\t\t\t\t\t{honeypotName ? <Honeypot name={honeypotName} inputRef={honeypotRef} /> : null}\n\t\t\t\t\t{header}\n\t\t\t\t\t{flow ? (\n\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t{/* Announces \"Step X of Y\" politely on each step change. A bare aria-live region, not\n\t\t\t\t\t\t\t role=status, so the single status role stays with the form's success outcome. */}\n\t\t\t\t\t\t\t<div aria-live=\"polite\" aria-atomic=\"true\" style={SR_ONLY}>\n\t\t\t\t\t\t\t\t{stepStatusText}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t{/* Focus lands here on a step change (the region's start), so keyboard/SR users move with it.\n\t\t\t\t\t\t\t A plain focusable container, not a named group; the aria-live region above does the announcing. */}\n\t\t\t\t\t\t\t<div data-fb-step-region tabIndex={-1} className=\"fb-form__step\">\n\t\t\t\t\t\t\t\t<FormFields layout={layout} />\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t{currentStepHasError ? (\n\t\t\t\t\t\t\t\t<p role=\"alert\" className=\"fb-form__step-error\">\n\t\t\t\t\t\t\t\t\t{translate(keys.formStepInvalid)}\n\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t</>\n\t\t\t\t\t) : (\n\t\t\t\t\t\t<FormFields layout={layout} />\n\t\t\t\t\t)}\n\t\t\t\t\t{state.submitError ? (\n\t\t\t\t\t\t<p role=\"alert\" className=\"fb-form__submit-error\">\n\t\t\t\t\t\t\t{state.submitError}\n\t\t\t\t\t\t</p>\n\t\t\t\t\t) : null}\n\t\t\t\t\t<FormControls\n\t\t\t\t\t\tbackButtonClassName={backButtonClassName}\n\t\t\t\t\t\tnextButtonClassName={nextButtonClassName}\n\t\t\t\t\t\tsubmitButtonClassName={submitButtonClassName}\n\t\t\t\t\t\trenderBack={renderBack}\n\t\t\t\t\t\trenderNext={renderNext}\n\t\t\t\t\t\trenderSubmit={renderSubmit}\n\t\t\t\t\t/>\n\t\t\t\t</form>\n\t\t\t)}\n\t\t</FormContext.Provider>\n\t)\n}\n"],"mappings":"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"}
|
package/dist/react/Poll.d.ts
CHANGED
|
@@ -3,7 +3,11 @@ import { fetchFormResults } from "./fetchResults.js";
|
|
|
3
3
|
|
|
4
4
|
//#region src/react/Poll.d.ts
|
|
5
5
|
type PollProps = FormProps & {
|
|
6
|
-
/** The choice field whose results are shown after voting (should match the form's public `poll.resultsField`). */resultsField: string;
|
|
6
|
+
/** The choice field whose results are shown after voting (should match the form's public `poll.resultsField`). */resultsField: string;
|
|
7
|
+
/**
|
|
8
|
+
* Key for the per-browser voted guard (localStorage by default; overridable or disabled via
|
|
9
|
+
* `adapters.voteStorage`). Default `fb-poll-{form.id}`.
|
|
10
|
+
*/
|
|
7
11
|
storageKey?: string;
|
|
8
12
|
/**
|
|
9
13
|
* Server-known voted state, ORed with the localStorage guard: `true` marks the visitor as voted;
|
package/dist/react/Poll.js
CHANGED
|
@@ -9,17 +9,28 @@ import { fetchFormResults } from "./fetchResults.js";
|
|
|
9
9
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
10
10
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
11
11
|
//#region src/react/Poll.tsx
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
const localStorageVoteStorage = {
|
|
13
|
+
read: (key) => {
|
|
14
|
+
try {
|
|
15
|
+
return window.localStorage.getItem(key) != null;
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
write: (key) => {
|
|
21
|
+
try {
|
|
22
|
+
window.localStorage.setItem(key, "1");
|
|
23
|
+
} catch {}
|
|
17
24
|
}
|
|
18
25
|
};
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
26
|
+
/** `false` disables client persistence entirely (`hasVoted` still marks voted). */
|
|
27
|
+
const noopVoteStorage = {
|
|
28
|
+
read: () => false,
|
|
29
|
+
write: () => {}
|
|
30
|
+
};
|
|
31
|
+
const resolveVoteStorage = (configured) => {
|
|
32
|
+
if (configured === false) return noopVoteStorage;
|
|
33
|
+
return configured ?? localStorageVoteStorage;
|
|
23
34
|
};
|
|
24
35
|
/**
|
|
25
36
|
* A poll: renders `<Form>` while open and not yet voted, then fetches the aggregate results and shows
|
|
@@ -44,6 +55,8 @@ const Poll = ({ resultsField, storageKey, hasVoted, fetchResultsImpl = fetchForm
|
|
|
44
55
|
const [results, setResults] = useState(null);
|
|
45
56
|
const [loadFailed, setLoadFailed] = useState(false);
|
|
46
57
|
const translate = useMemo(() => formProps.t ?? makeTranslate(en), [formProps.t]);
|
|
58
|
+
const configuredVoteStorage = formProps.adapters?.voteStorage;
|
|
59
|
+
const voteStorage = useMemo(() => resolveVoteStorage(configuredVoteStorage), [configuredVoteStorage]);
|
|
47
60
|
const loadResults = useCallback(async () => {
|
|
48
61
|
const result = await fetchResultsImpl({
|
|
49
62
|
formId: formProps.form.id,
|
|
@@ -66,7 +79,7 @@ const Poll = ({ resultsField, storageKey, hasVoted, fetchResultsImpl = fetchForm
|
|
|
66
79
|
children: translate(keys.pollResultsError)
|
|
67
80
|
});
|
|
68
81
|
useEffect(() => {
|
|
69
|
-
const already = hasVoted === true ||
|
|
82
|
+
const already = hasVoted === true || voteStorage.read(key);
|
|
70
83
|
if (already) setVoted(true);
|
|
71
84
|
if (already && !resultsAwaitClose || closed || finalized) loadResults();
|
|
72
85
|
}, [
|
|
@@ -75,10 +88,11 @@ const Poll = ({ resultsField, storageKey, hasVoted, fetchResultsImpl = fetchForm
|
|
|
75
88
|
loadResults,
|
|
76
89
|
closed,
|
|
77
90
|
resultsAwaitClose,
|
|
78
|
-
finalized
|
|
91
|
+
finalized,
|
|
92
|
+
voteStorage
|
|
79
93
|
]);
|
|
80
94
|
const handleSuccess = useCallback((submissionId, result) => {
|
|
81
|
-
|
|
95
|
+
voteStorage.write(key);
|
|
82
96
|
setVoted(true);
|
|
83
97
|
if (!resultsAwaitClose) loadResults();
|
|
84
98
|
onSuccess?.(submissionId, result);
|
|
@@ -86,7 +100,8 @@ const Poll = ({ resultsField, storageKey, hasVoted, fetchResultsImpl = fetchForm
|
|
|
86
100
|
key,
|
|
87
101
|
loadResults,
|
|
88
102
|
onSuccess,
|
|
89
|
-
resultsAwaitClose
|
|
103
|
+
resultsAwaitClose,
|
|
104
|
+
voteStorage
|
|
90
105
|
]);
|
|
91
106
|
if (finalized) return /* @__PURE__ */ jsxs("div", {
|
|
92
107
|
className: "fb-poll fb-poll--final",
|
package/dist/react/Poll.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Poll.js","names":[],"sources":["../../src/react/Poll.tsx"],"sourcesContent":["'use client'\n\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport type { FieldAggregation } from '../aggregation/types'\nimport { isPollClosed } from '../form/pollState'\nimport { en } from '../translations/en'\nimport { keys } from '../translations/keys'\nimport { makeTranslate } from '../translations/makeTranslate'\nimport { Form, type FormProps } from './Form'\nimport { FormResults } from './FormResults'\nimport { type FetchResultsResult, fetchFormResults } from './fetchResults'\n\nexport type PollProps = FormProps & {\n\t/** The choice field whose results are shown after voting (should match the form's public `poll.resultsField`). */\n\tresultsField: string\n\t
|
|
1
|
+
{"version":3,"file":"Poll.js","names":[],"sources":["../../src/react/Poll.tsx"],"sourcesContent":["'use client'\n\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport type { FieldAggregation } from '../aggregation/types'\nimport { isPollClosed } from '../form/pollState'\nimport { en } from '../translations/en'\nimport { keys } from '../translations/keys'\nimport { makeTranslate } from '../translations/makeTranslate'\nimport type { VoteStorage } from './adapters'\nimport { Form, type FormProps } from './Form'\nimport { FormResults } from './FormResults'\nimport { type FetchResultsResult, fetchFormResults } from './fetchResults'\n\nexport type PollProps = FormProps & {\n\t/** The choice field whose results are shown after voting (should match the form's public `poll.resultsField`). */\n\tresultsField: string\n\t/**\n\t * Key for the per-browser voted guard (localStorage by default; overridable or disabled via\n\t * `adapters.voteStorage`). Default `fb-poll-{form.id}`.\n\t */\n\tstorageKey?: string\n\t/**\n\t * Server-known voted state, ORed with the localStorage guard: `true` marks the visitor as voted;\n\t * `false`/omitted falls back to localStorage. SSR hosts using the plugin's `poll.votedCookie`\n\t * option pass `hasVotedCookie(cookieHeader, form.id)` here.\n\t */\n\thasVoted?: boolean\n\t/**\n\t * Injectable results fetch (testing); defaults to `fetchFormResults`. Pass a stable reference (module\n\t * scope, `useCallback`, or `useMemo`): an inline function re-runs the load effect and double-fetches.\n\t */\n\tfetchResultsImpl?: typeof fetchFormResults\n}\n\nconst localStorageVoteStorage: VoteStorage = {\n\tread: (key) => {\n\t\ttry {\n\t\t\treturn window.localStorage.getItem(key) != null\n\t\t} catch {\n\t\t\treturn false\n\t\t}\n\t},\n\twrite: (key) => {\n\t\ttry {\n\t\t\twindow.localStorage.setItem(key, '1')\n\t\t} catch {\n\t\t\t// Private mode / storage disabled: the guard is best-effort UX, never integrity.\n\t\t}\n\t},\n}\n\n/** `false` disables client persistence entirely (`hasVoted` still marks voted). */\nconst noopVoteStorage: VoteStorage = { read: () => false, write: () => {} }\n\nconst resolveVoteStorage = (configured: VoteStorage | false | undefined): VoteStorage => {\n\tif (configured === false) {\n\t\treturn noopVoteStorage\n\t}\n\treturn configured ?? localStorageVoteStorage\n}\n\n/**\n * A poll: renders `<Form>` while open and not yet voted, then fetches the aggregate results and shows\n * `<FormResults>`. Lifecycle comes from `form.poll`: past `closesAt` the poll is closed (a translated\n * notice plus results, which the endpoint serves for any visibility once closed); a voted-but-open\n * `afterClose` poll shows a translated wait notice instead of fetching (the endpoint would refuse). A\n * recorded `outcome.winningValues` (via `resolvePollOutcome`) supersedes everything: a translated final\n * notice plus results with every winning bucket highlighted (a tie highlights more than one). A\n * per-browser localStorage flag (`storageKey`) skips straight to results on revisit; `hasVoted: true`\n * (e.g. from the server-set voted cookie) marks voted regardless of localStorage. The guard is UX, not\n * integrity (bypassable): server-enforced\n * one-per-identity dedup composes via `req.user` (authed forms) or a `notAlreadySubmitted` rule.\n */\nexport const Poll = ({\n\tresultsField,\n\tstorageKey,\n\thasVoted,\n\tfetchResultsImpl = fetchFormResults,\n\tapiRoute,\n\tonSuccess,\n\t...formProps\n}: PollProps) => {\n\tconst key = storageKey ?? `fb-poll-${formProps.form.id}`\n\tconst poll = formProps.form.poll\n\tconst closed = isPollClosed(poll)\n\tconst winningValues = poll?.outcome?.winningValues\n\tconst finalized = Array.isArray(winningValues) && winningValues.length > 0\n\tconst resultsAwaitClose = !closed && !finalized && poll?.resultsVisibility === 'afterClose'\n\tconst [voted, setVoted] = useState(false)\n\tconst [results, setResults] = useState<FieldAggregation[] | null>(null)\n\tconst [loadFailed, setLoadFailed] = useState(false)\n\tconst translate = useMemo(() => formProps.t ?? makeTranslate(en), [formProps.t])\n\tconst configuredVoteStorage = formProps.adapters?.voteStorage\n\tconst voteStorage = useMemo(\n\t\t() => resolveVoteStorage(configuredVoteStorage),\n\t\t[configuredVoteStorage]\n\t)\n\n\tconst loadResults = useCallback(async () => {\n\t\tconst result: FetchResultsResult = await fetchResultsImpl({\n\t\t\tformId: formProps.form.id,\n\t\t\tfield: resultsField,\n\t\t\tapiRoute,\n\t\t})\n\t\t// A failed load surfaces as an error, not an empty result set: `[]` would read as \"no votes yet\".\n\t\tif (result.ok) {\n\t\t\tsetResults(result.results)\n\t\t\tsetLoadFailed(false)\n\t\t} else {\n\t\t\tsetLoadFailed(true)\n\t\t}\n\t}, [fetchResultsImpl, formProps.form.id, resultsField, apiRoute])\n\n\tconst resultsError = (\n\t\t<p className=\"fb-poll__error\" role=\"alert\">\n\t\t\t{translate(keys.pollResultsError)}\n\t\t</p>\n\t)\n\n\tuseEffect(() => {\n\t\tconst already = hasVoted === true || voteStorage.read(key)\n\t\tif (already) {\n\t\t\tsetVoted(true)\n\t\t}\n\t\tif ((already && !resultsAwaitClose) || closed || finalized) {\n\t\t\tvoid loadResults()\n\t\t}\n\t}, [hasVoted, key, loadResults, closed, resultsAwaitClose, finalized, voteStorage])\n\n\tconst handleSuccess = useCallback<NonNullable<FormProps['onSuccess']>>(\n\t\t(submissionId, result) => {\n\t\t\tvoteStorage.write(key)\n\t\t\tsetVoted(true)\n\t\t\tif (!resultsAwaitClose) {\n\t\t\t\tvoid loadResults()\n\t\t\t}\n\t\t\t// Forward the resolved success response so a Poll host gets the same onSuccess payload as a Form host.\n\t\t\tonSuccess?.(submissionId, result)\n\t\t},\n\t\t[key, loadResults, onSuccess, resultsAwaitClose, voteStorage]\n\t)\n\n\tif (finalized) {\n\t\treturn (\n\t\t\t<div className=\"fb-poll fb-poll--final\">\n\t\t\t\t<p className=\"fb-poll__final\">{translate(keys.pollFinalResult)}</p>\n\t\t\t\t{loadFailed ? (\n\t\t\t\t\tresultsError\n\t\t\t\t) : results ? (\n\t\t\t\t\t<FormResults\n\t\t\t\t\t\tresults={results}\n\t\t\t\t\t\twinningValues={winningValues}\n\t\t\t\t\t\tt={formProps.t}\n\t\t\t\t\t\tlocale={formProps.locale}\n\t\t\t\t\t/>\n\t\t\t\t) : null}\n\t\t\t</div>\n\t\t)\n\t}\n\n\tif (closed) {\n\t\treturn (\n\t\t\t<div className=\"fb-poll fb-poll--closed\">\n\t\t\t\t<p className=\"fb-poll__closed\">{translate(keys.pollClosed)}</p>\n\t\t\t\t{loadFailed ? (\n\t\t\t\t\tresultsError\n\t\t\t\t) : results ? (\n\t\t\t\t\t<FormResults results={results} t={formProps.t} locale={formProps.locale} />\n\t\t\t\t) : null}\n\t\t\t</div>\n\t\t)\n\t}\n\n\tif (voted) {\n\t\tif (resultsAwaitClose) {\n\t\t\treturn <p className=\"fb-poll__await-close\">{translate(keys.pollResultsAfterClose)}</p>\n\t\t}\n\t\tif (loadFailed) {\n\t\t\treturn resultsError\n\t\t}\n\t\treturn results ? (\n\t\t\t<FormResults results={results} t={formProps.t} locale={formProps.locale} />\n\t\t) : null\n\t}\n\n\treturn <Form {...formProps} apiRoute={apiRoute} onSuccess={handleSuccess} />\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
//#region src/react/adapters.d.ts
|
|
2
|
+
/** The plugin's navigation intent for a post-submit redirect, passed through to the adapter. */
|
|
3
|
+
type NavigateOptions = {
|
|
4
|
+
replace: boolean;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Client persistence for a poll's per-browser voted flag. `read` reports whether the flag is set;
|
|
8
|
+
* `write` sets it after a successful vote.
|
|
9
|
+
*/
|
|
10
|
+
type VoteStorage = {
|
|
11
|
+
read: (key: string) => boolean;
|
|
12
|
+
write: (key: string) => void;
|
|
13
|
+
};
|
|
14
|
+
/** Loads a third-party script and resolves once it is executable (e.g. a consent-gated loader). */
|
|
15
|
+
type LoadScript = (src: string) => Promise<void>;
|
|
16
|
+
/**
|
|
17
|
+
* Host-owned effects. Every member is optional and defaults to the current DOM behavior, so
|
|
18
|
+
* omitting the prop entirely preserves the default semantics exactly.
|
|
19
|
+
*
|
|
20
|
+
* - `navigate`: replaces `window.location.assign`/`.replace` for the post-submit redirect. When
|
|
21
|
+
* set, the plugin never touches `window.location` for that form; a throwing adapter propagates
|
|
22
|
+
* and never falls back to a hard navigation. Not awaited.
|
|
23
|
+
* - `voteStorage`: replaces the `<Poll>` localStorage voted-flag read/write; `false` disables
|
|
24
|
+
* client persistence entirely (e.g. when the server-side `poll.votedCookie` is the source of truth).
|
|
25
|
+
* - `loadScript`: replaces the captcha widgets' `document.head` script injection (consent
|
|
26
|
+
* managers, CSP nonces). The per-src cache and evict-on-failure retry still apply.
|
|
27
|
+
*/
|
|
28
|
+
type FormAdapters = {
|
|
29
|
+
navigate?: (url: string, options: NavigateOptions) => void;
|
|
30
|
+
voteStorage?: VoteStorage | false;
|
|
31
|
+
loadScript?: LoadScript;
|
|
32
|
+
};
|
|
33
|
+
//#endregion
|
|
34
|
+
export { FormAdapters, LoadScript, NavigateOptions, VoteStorage };
|
|
35
|
+
//# sourceMappingURL=adapters.d.ts.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//#region src/react/adapters.ts
|
|
2
|
+
/** The default navigation: a full document load, push semantics unless `replace` is requested. */
|
|
3
|
+
const defaultNavigate = (url, options) => {
|
|
4
|
+
if (typeof window === "undefined") return;
|
|
5
|
+
if (options.replace) window.location.replace(url);
|
|
6
|
+
else window.location.assign(url);
|
|
7
|
+
};
|
|
8
|
+
//#endregion
|
|
9
|
+
export { defaultNavigate };
|
|
10
|
+
|
|
11
|
+
//# sourceMappingURL=adapters.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapters.js","names":[],"sources":["../../src/react/adapters.ts"],"sourcesContent":["/** The plugin's navigation intent for a post-submit redirect, passed through to the adapter. */\nexport type NavigateOptions = { replace: boolean }\n\n/**\n * Client persistence for a poll's per-browser voted flag. `read` reports whether the flag is set;\n * `write` sets it after a successful vote.\n */\nexport type VoteStorage = {\n\tread: (key: string) => boolean\n\twrite: (key: string) => void\n}\n\n/** Loads a third-party script and resolves once it is executable (e.g. a consent-gated loader). */\nexport type LoadScript = (src: string) => Promise<void>\n\n/**\n * Host-owned effects. Every member is optional and defaults to the current DOM behavior, so\n * omitting the prop entirely preserves the default semantics exactly.\n *\n * - `navigate`: replaces `window.location.assign`/`.replace` for the post-submit redirect. When\n * set, the plugin never touches `window.location` for that form; a throwing adapter propagates\n * and never falls back to a hard navigation. Not awaited.\n * - `voteStorage`: replaces the `<Poll>` localStorage voted-flag read/write; `false` disables\n * client persistence entirely (e.g. when the server-side `poll.votedCookie` is the source of truth).\n * - `loadScript`: replaces the captcha widgets' `document.head` script injection (consent\n * managers, CSP nonces). The per-src cache and evict-on-failure retry still apply.\n */\nexport type FormAdapters = {\n\tnavigate?: (url: string, options: NavigateOptions) => void\n\tvoteStorage?: VoteStorage | false\n\tloadScript?: LoadScript\n}\n\n/** The default navigation: a full document load, push semantics unless `replace` is requested. */\nexport const defaultNavigate = (url: string, options: NavigateOptions): void => {\n\tif (typeof window === 'undefined') {\n\t\treturn\n\t}\n\tif (options.replace) {\n\t\twindow.location.replace(url)\n\t} else {\n\t\twindow.location.assign(url)\n\t}\n}\n"],"mappings":";;AAkCA,MAAa,mBAAmB,KAAa,YAAmC;CAC/E,IAAI,OAAO,WAAW,aACrB;CAED,IAAI,QAAQ,SACX,OAAO,SAAS,QAAQ,GAAG;MAE3B,OAAO,SAAS,OAAO,GAAG;AAE5B"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { FormAdapters } from "../adapters.js";
|
|
1
2
|
import { CaptchaWidgetHandle } from "./types.js";
|
|
2
3
|
import { Ref } from "react";
|
|
3
4
|
|
|
@@ -8,7 +9,8 @@ type HcaptchaCaptchaProps = {
|
|
|
8
9
|
onToken: (token: string | null) => void; /** Extra vendor render params (theme, size, ...). */
|
|
9
10
|
options?: Record<string, unknown>;
|
|
10
11
|
className?: string; /** Override the vendor script URL. */
|
|
11
|
-
scriptSrc?: string;
|
|
12
|
+
scriptSrc?: string; /** Host-owned effects; only `loadScript` applies here (consent-gated or CSP-nonced script loading). */
|
|
13
|
+
adapters?: Pick<FormAdapters, 'loadScript'>;
|
|
12
14
|
ref?: Ref<CaptchaWidgetHandle>;
|
|
13
15
|
};
|
|
14
16
|
/**
|
|
@@ -21,6 +23,7 @@ declare const HcaptchaCaptcha: ({
|
|
|
21
23
|
options,
|
|
22
24
|
className,
|
|
23
25
|
scriptSrc,
|
|
26
|
+
adapters,
|
|
24
27
|
ref
|
|
25
28
|
}: HcaptchaCaptchaProps) => import("react/jsx-runtime").JSX.Element;
|
|
26
29
|
//#endregion
|
|
@@ -9,14 +9,14 @@ const HCAPTCHA_SCRIPT_URL = "https://js.hcaptcha.com/1/api.js?render=explicit";
|
|
|
9
9
|
* Headless hCaptcha widget: loads the vendor script once, renders the widget into a bare div,
|
|
10
10
|
* and reports tokens through `onToken`. Pass the latest token to `<Form captchaToken>`.
|
|
11
11
|
*/
|
|
12
|
-
const HcaptchaCaptcha = ({ siteKey, onToken, options, className, scriptSrc = HCAPTCHA_SCRIPT_URL, ref }) => {
|
|
12
|
+
const HcaptchaCaptcha = ({ siteKey, onToken, options, className, scriptSrc = HCAPTCHA_SCRIPT_URL, adapters, ref }) => {
|
|
13
13
|
const containerRef = useRef(null);
|
|
14
14
|
const widgetIdRef = useRef(null);
|
|
15
15
|
const onTokenRef = useRef(onToken);
|
|
16
16
|
onTokenRef.current = onToken;
|
|
17
17
|
const optionsRef = useRef(options);
|
|
18
18
|
optionsRef.current = options;
|
|
19
|
-
const ready = useCaptchaScript(scriptSrc);
|
|
19
|
+
const ready = useCaptchaScript(scriptSrc, adapters?.loadScript);
|
|
20
20
|
useImperativeHandle(ref, () => ({ reset: () => {
|
|
21
21
|
const api = getHcaptcha();
|
|
22
22
|
if (api && widgetIdRef.current !== null) api.reset(widgetIdRef.current);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HcaptchaCaptcha.js","names":[],"sources":["../../../src/react/captcha/HcaptchaCaptcha.tsx"],"sourcesContent":["'use client'\n\nimport { type Ref, useEffect, useImperativeHandle, useRef } from 'react'\nimport type { CaptchaWidgetHandle } from './types'\nimport { useCaptchaScript } from './useCaptchaScript'\nimport { getHcaptcha } from './vendors'\n\nexport const HCAPTCHA_SCRIPT_URL = 'https://js.hcaptcha.com/1/api.js?render=explicit'\n\nexport type HcaptchaCaptchaProps = {\n\t/** hCaptcha site key (public). */\n\tsiteKey: string\n\t/** Receives each fresh token, and null when the token expires or errors. */\n\tonToken: (token: string | null) => void\n\t/** Extra vendor render params (theme, size, ...). */\n\toptions?: Record<string, unknown>\n\tclassName?: string\n\t/** Override the vendor script URL. */\n\tscriptSrc?: string\n\tref?: Ref<CaptchaWidgetHandle>\n}\n\n/**\n * Headless hCaptcha widget: loads the vendor script once, renders the widget into a bare div,\n * and reports tokens through `onToken`. Pass the latest token to `<Form captchaToken>`.\n */\nexport const HcaptchaCaptcha = ({\n\tsiteKey,\n\tonToken,\n\toptions,\n\tclassName,\n\tscriptSrc = HCAPTCHA_SCRIPT_URL,\n\tref,\n}: HcaptchaCaptchaProps) => {\n\tconst containerRef = useRef<HTMLDivElement>(null)\n\tconst widgetIdRef = useRef<string | null>(null)\n\tconst onTokenRef = useRef(onToken)\n\tonTokenRef.current = onToken\n\tconst optionsRef = useRef(options)\n\toptionsRef.current = options\n\tconst ready = useCaptchaScript(scriptSrc)\n\n\tuseImperativeHandle(\n\t\tref,\n\t\t() => ({\n\t\t\treset: () => {\n\t\t\t\tconst api = getHcaptcha()\n\t\t\t\tif (api && widgetIdRef.current !== null) {\n\t\t\t\t\tapi.reset(widgetIdRef.current)\n\t\t\t\t}\n\t\t\t\tonTokenRef.current(null)\n\t\t\t},\n\t\t}),\n\t\t[]\n\t)\n\n\tuseEffect(() => {\n\t\tif (!ready) {\n\t\t\treturn\n\t\t}\n\t\tconst container = containerRef.current\n\t\tconst api = getHcaptcha()\n\t\tif (!container || !api) {\n\t\t\treturn\n\t\t}\n\t\tconst widgetId = api.render(container, {\n\t\t\t...optionsRef.current,\n\t\t\tsitekey: siteKey,\n\t\t\tcallback: (token: string) => onTokenRef.current(token),\n\t\t\t'expired-callback': () => onTokenRef.current(null),\n\t\t\t'error-callback': () => onTokenRef.current(null),\n\t\t})\n\t\twidgetIdRef.current = widgetId\n\t\treturn () => {\n\t\t\twidgetIdRef.current = null\n\t\t\ttry {\n\t\t\t\tapi.remove(widgetId)\n\t\t\t} catch {}\n\t\t}\n\t}, [ready, siteKey])\n\n\treturn <div ref={containerRef} className={className} />\n}\n"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"HcaptchaCaptcha.js","names":[],"sources":["../../../src/react/captcha/HcaptchaCaptcha.tsx"],"sourcesContent":["'use client'\n\nimport { type Ref, useEffect, useImperativeHandle, useRef } from 'react'\nimport type { FormAdapters } from '../adapters'\nimport type { CaptchaWidgetHandle } from './types'\nimport { useCaptchaScript } from './useCaptchaScript'\nimport { getHcaptcha } from './vendors'\n\nexport const HCAPTCHA_SCRIPT_URL = 'https://js.hcaptcha.com/1/api.js?render=explicit'\n\nexport type HcaptchaCaptchaProps = {\n\t/** hCaptcha site key (public). */\n\tsiteKey: string\n\t/** Receives each fresh token, and null when the token expires or errors. */\n\tonToken: (token: string | null) => void\n\t/** Extra vendor render params (theme, size, ...). */\n\toptions?: Record<string, unknown>\n\tclassName?: string\n\t/** Override the vendor script URL. */\n\tscriptSrc?: string\n\t/** Host-owned effects; only `loadScript` applies here (consent-gated or CSP-nonced script loading). */\n\tadapters?: Pick<FormAdapters, 'loadScript'>\n\tref?: Ref<CaptchaWidgetHandle>\n}\n\n/**\n * Headless hCaptcha widget: loads the vendor script once, renders the widget into a bare div,\n * and reports tokens through `onToken`. Pass the latest token to `<Form captchaToken>`.\n */\nexport const HcaptchaCaptcha = ({\n\tsiteKey,\n\tonToken,\n\toptions,\n\tclassName,\n\tscriptSrc = HCAPTCHA_SCRIPT_URL,\n\tadapters,\n\tref,\n}: HcaptchaCaptchaProps) => {\n\tconst containerRef = useRef<HTMLDivElement>(null)\n\tconst widgetIdRef = useRef<string | null>(null)\n\tconst onTokenRef = useRef(onToken)\n\tonTokenRef.current = onToken\n\tconst optionsRef = useRef(options)\n\toptionsRef.current = options\n\tconst ready = useCaptchaScript(scriptSrc, adapters?.loadScript)\n\n\tuseImperativeHandle(\n\t\tref,\n\t\t() => ({\n\t\t\treset: () => {\n\t\t\t\tconst api = getHcaptcha()\n\t\t\t\tif (api && widgetIdRef.current !== null) {\n\t\t\t\t\tapi.reset(widgetIdRef.current)\n\t\t\t\t}\n\t\t\t\tonTokenRef.current(null)\n\t\t\t},\n\t\t}),\n\t\t[]\n\t)\n\n\tuseEffect(() => {\n\t\tif (!ready) {\n\t\t\treturn\n\t\t}\n\t\tconst container = containerRef.current\n\t\tconst api = getHcaptcha()\n\t\tif (!container || !api) {\n\t\t\treturn\n\t\t}\n\t\tconst widgetId = api.render(container, {\n\t\t\t...optionsRef.current,\n\t\t\tsitekey: siteKey,\n\t\t\tcallback: (token: string) => onTokenRef.current(token),\n\t\t\t'expired-callback': () => onTokenRef.current(null),\n\t\t\t'error-callback': () => onTokenRef.current(null),\n\t\t})\n\t\twidgetIdRef.current = widgetId\n\t\treturn () => {\n\t\t\twidgetIdRef.current = null\n\t\t\ttry {\n\t\t\t\tapi.remove(widgetId)\n\t\t\t} catch {}\n\t\t}\n\t}, [ready, siteKey])\n\n\treturn <div ref={containerRef} className={className} />\n}\n"],"mappings":";;;;;;AAQA,MAAa,sBAAsB;;;;;AAqBnC,MAAa,mBAAmB,EAC/B,SACA,SACA,SACA,WACA,YAAY,qBACZ,UACA,UAC2B;CAC3B,MAAM,eAAe,OAAuB,IAAI;CAChD,MAAM,cAAc,OAAsB,IAAI;CAC9C,MAAM,aAAa,OAAO,OAAO;CACjC,WAAW,UAAU;CACrB,MAAM,aAAa,OAAO,OAAO;CACjC,WAAW,UAAU;CACrB,MAAM,QAAQ,iBAAiB,WAAW,UAAU,UAAU;CAE9D,oBACC,YACO,EACN,aAAa;EACZ,MAAM,MAAM,YAAY;EACxB,IAAI,OAAO,YAAY,YAAY,MAClC,IAAI,MAAM,YAAY,OAAO;EAE9B,WAAW,QAAQ,IAAI;CACxB,EACD,IACA,CAAC,CACF;CAEA,gBAAgB;EACf,IAAI,CAAC,OACJ;EAED,MAAM,YAAY,aAAa;EAC/B,MAAM,MAAM,YAAY;EACxB,IAAI,CAAC,aAAa,CAAC,KAClB;EAED,MAAM,WAAW,IAAI,OAAO,WAAW;GACtC,GAAG,WAAW;GACd,SAAS;GACT,WAAW,UAAkB,WAAW,QAAQ,KAAK;GACrD,0BAA0B,WAAW,QAAQ,IAAI;GACjD,wBAAwB,WAAW,QAAQ,IAAI;EAChD,CAAC;EACD,YAAY,UAAU;EACtB,aAAa;GACZ,YAAY,UAAU;GACtB,IAAI;IACH,IAAI,OAAO,QAAQ;GACpB,QAAQ,CAAC;EACV;CACD,GAAG,CAAC,OAAO,OAAO,CAAC;CAEnB,OAAO,oBAAC,OAAD;EAAK,KAAK;EAAyB;CAAY,CAAA;AACvD"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { FormAdapters } from "../adapters.js";
|
|
1
2
|
import { CaptchaWidgetHandle } from "./types.js";
|
|
2
3
|
import { Ref } from "react";
|
|
3
4
|
|
|
@@ -18,7 +19,8 @@ type RecaptchaCaptchaProps = {
|
|
|
18
19
|
onToken: (token: string | null) => void; /** Extra vendor render params for v2 (theme, size, ...). */
|
|
19
20
|
options?: Record<string, unknown>;
|
|
20
21
|
className?: string; /** Override the vendor script URL (the version-specific `render` query is not appended). */
|
|
21
|
-
scriptSrc?: string;
|
|
22
|
+
scriptSrc?: string; /** Host-owned effects; only `loadScript` applies here (consent-gated or CSP-nonced script loading). */
|
|
23
|
+
adapters?: Pick<FormAdapters, 'loadScript'>;
|
|
22
24
|
ref?: Ref<RecaptchaCaptchaHandle>;
|
|
23
25
|
};
|
|
24
26
|
/**
|
|
@@ -34,6 +36,7 @@ declare const RecaptchaCaptcha: ({
|
|
|
34
36
|
options,
|
|
35
37
|
className,
|
|
36
38
|
scriptSrc,
|
|
39
|
+
adapters,
|
|
37
40
|
ref
|
|
38
41
|
}: RecaptchaCaptchaProps) => import("react/jsx-runtime").JSX.Element;
|
|
39
42
|
//#endregion
|
|
@@ -10,7 +10,7 @@ const RECAPTCHA_SCRIPT_URL = "https://www.google.com/recaptcha/api.js";
|
|
|
10
10
|
* tokens as the user solves the challenge; v3 fetches an initial token on mount and exposes
|
|
11
11
|
* `execute` on the ref for on-demand refresh. Pass the latest token to `<Form captchaToken>`.
|
|
12
12
|
*/
|
|
13
|
-
const RecaptchaCaptcha = ({ siteKey, version, action, onToken, options, className, scriptSrc, ref }) => {
|
|
13
|
+
const RecaptchaCaptcha = ({ siteKey, version, action, onToken, options, className, scriptSrc, adapters, ref }) => {
|
|
14
14
|
const src = scriptSrc ?? `https://www.google.com/recaptcha/api.js?render=${version === "v3" ? siteKey : "explicit"}`;
|
|
15
15
|
const containerRef = useRef(null);
|
|
16
16
|
const widgetIdRef = useRef(null);
|
|
@@ -20,7 +20,7 @@ const RecaptchaCaptcha = ({ siteKey, version, action, onToken, options, classNam
|
|
|
20
20
|
optionsRef.current = options;
|
|
21
21
|
const actionRef = useRef(action);
|
|
22
22
|
actionRef.current = action;
|
|
23
|
-
const ready = useCaptchaScript(src);
|
|
23
|
+
const ready = useCaptchaScript(src, adapters?.loadScript);
|
|
24
24
|
const execute = useCallback(async (overrideAction) => {
|
|
25
25
|
const api = getGrecaptcha();
|
|
26
26
|
if (!api || version !== "v3") return null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RecaptchaCaptcha.js","names":[],"sources":["../../../src/react/captcha/RecaptchaCaptcha.tsx"],"sourcesContent":["'use client'\n\nimport { type Ref, useCallback, useEffect, useImperativeHandle, useRef } from 'react'\nimport type { CaptchaWidgetHandle } from './types'\nimport { useCaptchaScript } from './useCaptchaScript'\nimport { getGrecaptcha } from './vendors'\n\nexport const RECAPTCHA_SCRIPT_URL = 'https://www.google.com/recaptcha/api.js'\n\nexport type RecaptchaCaptchaHandle = CaptchaWidgetHandle & {\n\t/**\n\t * v3 only: fetch a fresh token on demand and report it through `onToken`. v3 tokens expire\n\t * after about two minutes, so call this right before submit for a long-lived form. Resolves\n\t * null for v2 or when execution fails.\n\t */\n\texecute: (action?: string) => Promise<string | null>\n}\n\nexport type RecaptchaCaptchaProps = {\n\t/** reCAPTCHA site key (public). */\n\tsiteKey: string\n\t/** 'v2' renders the checkbox widget; 'v3' runs invisibly and produces score-based tokens. */\n\tversion: 'v2' | 'v3'\n\t/** v3 action name reported to reCAPTCHA. Default 'submit'. */\n\taction?: string\n\t/** Receives each fresh token, and null when the token expires, errors, or is reset. */\n\tonToken: (token: string | null) => void\n\t/** Extra vendor render params for v2 (theme, size, ...). */\n\toptions?: Record<string, unknown>\n\tclassName?: string\n\t/** Override the vendor script URL (the version-specific `render` query is not appended). */\n\tscriptSrc?: string\n\tref?: Ref<RecaptchaCaptchaHandle>\n}\n\n/**\n * Headless Google reCAPTCHA widget for v2 (checkbox) and v3 (invisible, score-based). v2 reports\n * tokens as the user solves the challenge; v3 fetches an initial token on mount and exposes\n * `execute` on the ref for on-demand refresh. Pass the latest token to `<Form captchaToken>`.\n */\nexport const RecaptchaCaptcha = ({\n\tsiteKey,\n\tversion,\n\taction,\n\tonToken,\n\toptions,\n\tclassName,\n\tscriptSrc,\n\tref,\n}: RecaptchaCaptchaProps) => {\n\tconst src =\n\t\tscriptSrc ?? `${RECAPTCHA_SCRIPT_URL}?render=${version === 'v3' ? siteKey : 'explicit'}`\n\tconst containerRef = useRef<HTMLDivElement>(null)\n\tconst widgetIdRef = useRef<number | null>(null)\n\tconst onTokenRef = useRef(onToken)\n\tonTokenRef.current = onToken\n\tconst optionsRef = useRef(options)\n\toptionsRef.current = options\n\tconst actionRef = useRef(action)\n\tactionRef.current = action\n\tconst ready = useCaptchaScript(src)\n\n\tconst execute = useCallback(\n\t\tasync (overrideAction?: string): Promise<string | null> => {\n\t\t\tconst api = getGrecaptcha()\n\t\t\tif (!api || version !== 'v3') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst token = await api.execute(siteKey, {\n\t\t\t\t\taction: overrideAction ?? actionRef.current ?? 'submit',\n\t\t\t\t})\n\t\t\t\tonTokenRef.current(token)\n\t\t\t\treturn token\n\t\t\t} catch {\n\t\t\t\tonTokenRef.current(null)\n\t\t\t\treturn null\n\t\t\t}\n\t\t},\n\t\t[siteKey, version]\n\t)\n\n\tuseImperativeHandle(\n\t\tref,\n\t\t() => ({\n\t\t\treset: () => {\n\t\t\t\tconst api = getGrecaptcha()\n\t\t\t\tif (api && version === 'v2' && widgetIdRef.current !== null) {\n\t\t\t\t\tapi.reset(widgetIdRef.current)\n\t\t\t\t}\n\t\t\t\tonTokenRef.current(null)\n\t\t\t},\n\t\t\texecute,\n\t\t}),\n\t\t[execute, version]\n\t)\n\n\tuseEffect(() => {\n\t\tif (!ready) {\n\t\t\treturn\n\t\t}\n\t\tconst api = getGrecaptcha()\n\t\tif (!api) {\n\t\t\treturn\n\t\t}\n\t\tlet active = true\n\t\tif (version === 'v3') {\n\t\t\tapi.ready(() => {\n\t\t\t\tif (active) {\n\t\t\t\t\tvoid execute()\n\t\t\t\t}\n\t\t\t})\n\t\t\treturn () => {\n\t\t\t\tactive = false\n\t\t\t}\n\t\t}\n\t\tconst container = containerRef.current\n\t\tif (!container) {\n\t\t\treturn\n\t\t}\n\t\tapi.ready(() => {\n\t\t\tif (!active || widgetIdRef.current !== null) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\twidgetIdRef.current = api.render(container, {\n\t\t\t\t...optionsRef.current,\n\t\t\t\tsitekey: siteKey,\n\t\t\t\tcallback: (token: string) => onTokenRef.current(token),\n\t\t\t\t'expired-callback': () => onTokenRef.current(null),\n\t\t\t\t'error-callback': () => onTokenRef.current(null),\n\t\t\t})\n\t\t})\n\t\treturn () => {\n\t\t\tactive = false\n\t\t\tif (widgetIdRef.current !== null) {\n\t\t\t\ttry {\n\t\t\t\t\tapi.reset(widgetIdRef.current)\n\t\t\t\t} catch {}\n\t\t\t\twidgetIdRef.current = null\n\t\t\t}\n\t\t\t// grecaptcha has no remove(); clearing the container drops the detached widget DOM.\n\t\t\tcontainer.replaceChildren()\n\t\t}\n\t}, [ready, siteKey, version, execute])\n\n\treturn <div ref={containerRef} className={className} />\n}\n"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"RecaptchaCaptcha.js","names":[],"sources":["../../../src/react/captcha/RecaptchaCaptcha.tsx"],"sourcesContent":["'use client'\n\nimport { type Ref, useCallback, useEffect, useImperativeHandle, useRef } from 'react'\nimport type { FormAdapters } from '../adapters'\nimport type { CaptchaWidgetHandle } from './types'\nimport { useCaptchaScript } from './useCaptchaScript'\nimport { getGrecaptcha } from './vendors'\n\nexport const RECAPTCHA_SCRIPT_URL = 'https://www.google.com/recaptcha/api.js'\n\nexport type RecaptchaCaptchaHandle = CaptchaWidgetHandle & {\n\t/**\n\t * v3 only: fetch a fresh token on demand and report it through `onToken`. v3 tokens expire\n\t * after about two minutes, so call this right before submit for a long-lived form. Resolves\n\t * null for v2 or when execution fails.\n\t */\n\texecute: (action?: string) => Promise<string | null>\n}\n\nexport type RecaptchaCaptchaProps = {\n\t/** reCAPTCHA site key (public). */\n\tsiteKey: string\n\t/** 'v2' renders the checkbox widget; 'v3' runs invisibly and produces score-based tokens. */\n\tversion: 'v2' | 'v3'\n\t/** v3 action name reported to reCAPTCHA. Default 'submit'. */\n\taction?: string\n\t/** Receives each fresh token, and null when the token expires, errors, or is reset. */\n\tonToken: (token: string | null) => void\n\t/** Extra vendor render params for v2 (theme, size, ...). */\n\toptions?: Record<string, unknown>\n\tclassName?: string\n\t/** Override the vendor script URL (the version-specific `render` query is not appended). */\n\tscriptSrc?: string\n\t/** Host-owned effects; only `loadScript` applies here (consent-gated or CSP-nonced script loading). */\n\tadapters?: Pick<FormAdapters, 'loadScript'>\n\tref?: Ref<RecaptchaCaptchaHandle>\n}\n\n/**\n * Headless Google reCAPTCHA widget for v2 (checkbox) and v3 (invisible, score-based). v2 reports\n * tokens as the user solves the challenge; v3 fetches an initial token on mount and exposes\n * `execute` on the ref for on-demand refresh. Pass the latest token to `<Form captchaToken>`.\n */\nexport const RecaptchaCaptcha = ({\n\tsiteKey,\n\tversion,\n\taction,\n\tonToken,\n\toptions,\n\tclassName,\n\tscriptSrc,\n\tadapters,\n\tref,\n}: RecaptchaCaptchaProps) => {\n\tconst src =\n\t\tscriptSrc ?? `${RECAPTCHA_SCRIPT_URL}?render=${version === 'v3' ? siteKey : 'explicit'}`\n\tconst containerRef = useRef<HTMLDivElement>(null)\n\tconst widgetIdRef = useRef<number | null>(null)\n\tconst onTokenRef = useRef(onToken)\n\tonTokenRef.current = onToken\n\tconst optionsRef = useRef(options)\n\toptionsRef.current = options\n\tconst actionRef = useRef(action)\n\tactionRef.current = action\n\tconst ready = useCaptchaScript(src, adapters?.loadScript)\n\n\tconst execute = useCallback(\n\t\tasync (overrideAction?: string): Promise<string | null> => {\n\t\t\tconst api = getGrecaptcha()\n\t\t\tif (!api || version !== 'v3') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst token = await api.execute(siteKey, {\n\t\t\t\t\taction: overrideAction ?? actionRef.current ?? 'submit',\n\t\t\t\t})\n\t\t\t\tonTokenRef.current(token)\n\t\t\t\treturn token\n\t\t\t} catch {\n\t\t\t\tonTokenRef.current(null)\n\t\t\t\treturn null\n\t\t\t}\n\t\t},\n\t\t[siteKey, version]\n\t)\n\n\tuseImperativeHandle(\n\t\tref,\n\t\t() => ({\n\t\t\treset: () => {\n\t\t\t\tconst api = getGrecaptcha()\n\t\t\t\tif (api && version === 'v2' && widgetIdRef.current !== null) {\n\t\t\t\t\tapi.reset(widgetIdRef.current)\n\t\t\t\t}\n\t\t\t\tonTokenRef.current(null)\n\t\t\t},\n\t\t\texecute,\n\t\t}),\n\t\t[execute, version]\n\t)\n\n\tuseEffect(() => {\n\t\tif (!ready) {\n\t\t\treturn\n\t\t}\n\t\tconst api = getGrecaptcha()\n\t\tif (!api) {\n\t\t\treturn\n\t\t}\n\t\tlet active = true\n\t\tif (version === 'v3') {\n\t\t\tapi.ready(() => {\n\t\t\t\tif (active) {\n\t\t\t\t\tvoid execute()\n\t\t\t\t}\n\t\t\t})\n\t\t\treturn () => {\n\t\t\t\tactive = false\n\t\t\t}\n\t\t}\n\t\tconst container = containerRef.current\n\t\tif (!container) {\n\t\t\treturn\n\t\t}\n\t\tapi.ready(() => {\n\t\t\tif (!active || widgetIdRef.current !== null) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\twidgetIdRef.current = api.render(container, {\n\t\t\t\t...optionsRef.current,\n\t\t\t\tsitekey: siteKey,\n\t\t\t\tcallback: (token: string) => onTokenRef.current(token),\n\t\t\t\t'expired-callback': () => onTokenRef.current(null),\n\t\t\t\t'error-callback': () => onTokenRef.current(null),\n\t\t\t})\n\t\t})\n\t\treturn () => {\n\t\t\tactive = false\n\t\t\tif (widgetIdRef.current !== null) {\n\t\t\t\ttry {\n\t\t\t\t\tapi.reset(widgetIdRef.current)\n\t\t\t\t} catch {}\n\t\t\t\twidgetIdRef.current = null\n\t\t\t}\n\t\t\t// grecaptcha has no remove(); clearing the container drops the detached widget DOM.\n\t\t\tcontainer.replaceChildren()\n\t\t}\n\t}, [ready, siteKey, version, execute])\n\n\treturn <div ref={containerRef} className={className} />\n}\n"],"mappings":";;;;;;AAQA,MAAa,uBAAuB;;;;;;AAmCpC,MAAa,oBAAoB,EAChC,SACA,SACA,QACA,SACA,SACA,WACA,WACA,UACA,UAC4B;CAC5B,MAAM,MACL,aAAa,kDAAkC,YAAY,OAAO,UAAU;CAC7E,MAAM,eAAe,OAAuB,IAAI;CAChD,MAAM,cAAc,OAAsB,IAAI;CAC9C,MAAM,aAAa,OAAO,OAAO;CACjC,WAAW,UAAU;CACrB,MAAM,aAAa,OAAO,OAAO;CACjC,WAAW,UAAU;CACrB,MAAM,YAAY,OAAO,MAAM;CAC/B,UAAU,UAAU;CACpB,MAAM,QAAQ,iBAAiB,KAAK,UAAU,UAAU;CAExD,MAAM,UAAU,YACf,OAAO,mBAAoD;EAC1D,MAAM,MAAM,cAAc;EAC1B,IAAI,CAAC,OAAO,YAAY,MACvB,OAAO;EAER,IAAI;GACH,MAAM,QAAQ,MAAM,IAAI,QAAQ,SAAS,EACxC,QAAQ,kBAAkB,UAAU,WAAW,SAChD,CAAC;GACD,WAAW,QAAQ,KAAK;GACxB,OAAO;EACR,QAAQ;GACP,WAAW,QAAQ,IAAI;GACvB,OAAO;EACR;CACD,GACA,CAAC,SAAS,OAAO,CAClB;CAEA,oBACC,YACO;EACN,aAAa;GACZ,MAAM,MAAM,cAAc;GAC1B,IAAI,OAAO,YAAY,QAAQ,YAAY,YAAY,MACtD,IAAI,MAAM,YAAY,OAAO;GAE9B,WAAW,QAAQ,IAAI;EACxB;EACA;CACD,IACA,CAAC,SAAS,OAAO,CAClB;CAEA,gBAAgB;EACf,IAAI,CAAC,OACJ;EAED,MAAM,MAAM,cAAc;EAC1B,IAAI,CAAC,KACJ;EAED,IAAI,SAAS;EACb,IAAI,YAAY,MAAM;GACrB,IAAI,YAAY;IACf,IAAI,QACH,QAAa;GAEf,CAAC;GACD,aAAa;IACZ,SAAS;GACV;EACD;EACA,MAAM,YAAY,aAAa;EAC/B,IAAI,CAAC,WACJ;EAED,IAAI,YAAY;GACf,IAAI,CAAC,UAAU,YAAY,YAAY,MACtC;GAED,YAAY,UAAU,IAAI,OAAO,WAAW;IAC3C,GAAG,WAAW;IACd,SAAS;IACT,WAAW,UAAkB,WAAW,QAAQ,KAAK;IACrD,0BAA0B,WAAW,QAAQ,IAAI;IACjD,wBAAwB,WAAW,QAAQ,IAAI;GAChD,CAAC;EACF,CAAC;EACD,aAAa;GACZ,SAAS;GACT,IAAI,YAAY,YAAY,MAAM;IACjC,IAAI;KACH,IAAI,MAAM,YAAY,OAAO;IAC9B,QAAQ,CAAC;IACT,YAAY,UAAU;GACvB;GAEA,UAAU,gBAAgB;EAC3B;CACD,GAAG;EAAC;EAAO;EAAS;EAAS;CAAO,CAAC;CAErC,OAAO,oBAAC,OAAD;EAAK,KAAK;EAAyB;CAAY,CAAA;AACvD"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { FormAdapters } from "../adapters.js";
|
|
1
2
|
import { CaptchaWidgetHandle } from "./types.js";
|
|
2
3
|
import { Ref } from "react";
|
|
3
4
|
|
|
@@ -8,7 +9,8 @@ type TurnstileCaptchaProps = {
|
|
|
8
9
|
onToken: (token: string | null) => void; /** Extra vendor render params (theme, size, appearance, ...). */
|
|
9
10
|
options?: Record<string, unknown>;
|
|
10
11
|
className?: string; /** Override the vendor script URL. */
|
|
11
|
-
scriptSrc?: string;
|
|
12
|
+
scriptSrc?: string; /** Host-owned effects; only `loadScript` applies here (consent-gated or CSP-nonced script loading). */
|
|
13
|
+
adapters?: Pick<FormAdapters, 'loadScript'>;
|
|
12
14
|
ref?: Ref<CaptchaWidgetHandle>;
|
|
13
15
|
};
|
|
14
16
|
/**
|
|
@@ -21,6 +23,7 @@ declare const TurnstileCaptcha: ({
|
|
|
21
23
|
options,
|
|
22
24
|
className,
|
|
23
25
|
scriptSrc,
|
|
26
|
+
adapters,
|
|
24
27
|
ref
|
|
25
28
|
}: TurnstileCaptchaProps) => import("react/jsx-runtime").JSX.Element;
|
|
26
29
|
//#endregion
|
|
@@ -9,14 +9,14 @@ const TURNSTILE_SCRIPT_URL = "https://challenges.cloudflare.com/turnstile/v0/api
|
|
|
9
9
|
* Headless Cloudflare Turnstile widget: loads the vendor script once, renders the widget into a
|
|
10
10
|
* bare div, and reports tokens through `onToken`. Pass the latest token to `<Form captchaToken>`.
|
|
11
11
|
*/
|
|
12
|
-
const TurnstileCaptcha = ({ siteKey, onToken, options, className, scriptSrc = TURNSTILE_SCRIPT_URL, ref }) => {
|
|
12
|
+
const TurnstileCaptcha = ({ siteKey, onToken, options, className, scriptSrc = TURNSTILE_SCRIPT_URL, adapters, ref }) => {
|
|
13
13
|
const containerRef = useRef(null);
|
|
14
14
|
const widgetIdRef = useRef(null);
|
|
15
15
|
const onTokenRef = useRef(onToken);
|
|
16
16
|
onTokenRef.current = onToken;
|
|
17
17
|
const optionsRef = useRef(options);
|
|
18
18
|
optionsRef.current = options;
|
|
19
|
-
const ready = useCaptchaScript(scriptSrc);
|
|
19
|
+
const ready = useCaptchaScript(scriptSrc, adapters?.loadScript);
|
|
20
20
|
useImperativeHandle(ref, () => ({ reset: () => {
|
|
21
21
|
const api = getTurnstile();
|
|
22
22
|
if (api && widgetIdRef.current !== null) api.reset(widgetIdRef.current);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TurnstileCaptcha.js","names":[],"sources":["../../../src/react/captcha/TurnstileCaptcha.tsx"],"sourcesContent":["'use client'\n\nimport { type Ref, useEffect, useImperativeHandle, useRef } from 'react'\nimport type { CaptchaWidgetHandle } from './types'\nimport { useCaptchaScript } from './useCaptchaScript'\nimport { getTurnstile } from './vendors'\n\nexport const TURNSTILE_SCRIPT_URL =\n\t'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit'\n\nexport type TurnstileCaptchaProps = {\n\t/** Turnstile site key (public). */\n\tsiteKey: string\n\t/** Receives each fresh token, and null when the token expires or errors. */\n\tonToken: (token: string | null) => void\n\t/** Extra vendor render params (theme, size, appearance, ...). */\n\toptions?: Record<string, unknown>\n\tclassName?: string\n\t/** Override the vendor script URL. */\n\tscriptSrc?: string\n\tref?: Ref<CaptchaWidgetHandle>\n}\n\n/**\n * Headless Cloudflare Turnstile widget: loads the vendor script once, renders the widget into a\n * bare div, and reports tokens through `onToken`. Pass the latest token to `<Form captchaToken>`.\n */\nexport const TurnstileCaptcha = ({\n\tsiteKey,\n\tonToken,\n\toptions,\n\tclassName,\n\tscriptSrc = TURNSTILE_SCRIPT_URL,\n\tref,\n}: TurnstileCaptchaProps) => {\n\tconst containerRef = useRef<HTMLDivElement>(null)\n\tconst widgetIdRef = useRef<string | null>(null)\n\tconst onTokenRef = useRef(onToken)\n\tonTokenRef.current = onToken\n\tconst optionsRef = useRef(options)\n\toptionsRef.current = options\n\tconst ready = useCaptchaScript(scriptSrc)\n\n\tuseImperativeHandle(\n\t\tref,\n\t\t() => ({\n\t\t\treset: () => {\n\t\t\t\tconst api = getTurnstile()\n\t\t\t\tif (api && widgetIdRef.current !== null) {\n\t\t\t\t\tapi.reset(widgetIdRef.current)\n\t\t\t\t}\n\t\t\t\tonTokenRef.current(null)\n\t\t\t},\n\t\t}),\n\t\t[]\n\t)\n\n\tuseEffect(() => {\n\t\tif (!ready) {\n\t\t\treturn\n\t\t}\n\t\tconst container = containerRef.current\n\t\tconst api = getTurnstile()\n\t\tif (!container || !api) {\n\t\t\treturn\n\t\t}\n\t\tconst widgetId = api.render(container, {\n\t\t\t...optionsRef.current,\n\t\t\tsitekey: siteKey,\n\t\t\tcallback: (token: string) => onTokenRef.current(token),\n\t\t\t'expired-callback': () => onTokenRef.current(null),\n\t\t\t'error-callback': () => onTokenRef.current(null),\n\t\t})\n\t\twidgetIdRef.current = widgetId\n\t\treturn () => {\n\t\t\twidgetIdRef.current = null\n\t\t\ttry {\n\t\t\t\tapi.remove(widgetId)\n\t\t\t} catch {}\n\t\t}\n\t}, [ready, siteKey])\n\n\treturn <div ref={containerRef} className={className} />\n}\n"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"TurnstileCaptcha.js","names":[],"sources":["../../../src/react/captcha/TurnstileCaptcha.tsx"],"sourcesContent":["'use client'\n\nimport { type Ref, useEffect, useImperativeHandle, useRef } from 'react'\nimport type { FormAdapters } from '../adapters'\nimport type { CaptchaWidgetHandle } from './types'\nimport { useCaptchaScript } from './useCaptchaScript'\nimport { getTurnstile } from './vendors'\n\nexport const TURNSTILE_SCRIPT_URL =\n\t'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit'\n\nexport type TurnstileCaptchaProps = {\n\t/** Turnstile site key (public). */\n\tsiteKey: string\n\t/** Receives each fresh token, and null when the token expires or errors. */\n\tonToken: (token: string | null) => void\n\t/** Extra vendor render params (theme, size, appearance, ...). */\n\toptions?: Record<string, unknown>\n\tclassName?: string\n\t/** Override the vendor script URL. */\n\tscriptSrc?: string\n\t/** Host-owned effects; only `loadScript` applies here (consent-gated or CSP-nonced script loading). */\n\tadapters?: Pick<FormAdapters, 'loadScript'>\n\tref?: Ref<CaptchaWidgetHandle>\n}\n\n/**\n * Headless Cloudflare Turnstile widget: loads the vendor script once, renders the widget into a\n * bare div, and reports tokens through `onToken`. Pass the latest token to `<Form captchaToken>`.\n */\nexport const TurnstileCaptcha = ({\n\tsiteKey,\n\tonToken,\n\toptions,\n\tclassName,\n\tscriptSrc = TURNSTILE_SCRIPT_URL,\n\tadapters,\n\tref,\n}: TurnstileCaptchaProps) => {\n\tconst containerRef = useRef<HTMLDivElement>(null)\n\tconst widgetIdRef = useRef<string | null>(null)\n\tconst onTokenRef = useRef(onToken)\n\tonTokenRef.current = onToken\n\tconst optionsRef = useRef(options)\n\toptionsRef.current = options\n\tconst ready = useCaptchaScript(scriptSrc, adapters?.loadScript)\n\n\tuseImperativeHandle(\n\t\tref,\n\t\t() => ({\n\t\t\treset: () => {\n\t\t\t\tconst api = getTurnstile()\n\t\t\t\tif (api && widgetIdRef.current !== null) {\n\t\t\t\t\tapi.reset(widgetIdRef.current)\n\t\t\t\t}\n\t\t\t\tonTokenRef.current(null)\n\t\t\t},\n\t\t}),\n\t\t[]\n\t)\n\n\tuseEffect(() => {\n\t\tif (!ready) {\n\t\t\treturn\n\t\t}\n\t\tconst container = containerRef.current\n\t\tconst api = getTurnstile()\n\t\tif (!container || !api) {\n\t\t\treturn\n\t\t}\n\t\tconst widgetId = api.render(container, {\n\t\t\t...optionsRef.current,\n\t\t\tsitekey: siteKey,\n\t\t\tcallback: (token: string) => onTokenRef.current(token),\n\t\t\t'expired-callback': () => onTokenRef.current(null),\n\t\t\t'error-callback': () => onTokenRef.current(null),\n\t\t})\n\t\twidgetIdRef.current = widgetId\n\t\treturn () => {\n\t\t\twidgetIdRef.current = null\n\t\t\ttry {\n\t\t\t\tapi.remove(widgetId)\n\t\t\t} catch {}\n\t\t}\n\t}, [ready, siteKey])\n\n\treturn <div ref={containerRef} className={className} />\n}\n"],"mappings":";;;;;;AAQA,MAAa,uBACZ;;;;;AAqBD,MAAa,oBAAoB,EAChC,SACA,SACA,SACA,WACA,YAAY,sBACZ,UACA,UAC4B;CAC5B,MAAM,eAAe,OAAuB,IAAI;CAChD,MAAM,cAAc,OAAsB,IAAI;CAC9C,MAAM,aAAa,OAAO,OAAO;CACjC,WAAW,UAAU;CACrB,MAAM,aAAa,OAAO,OAAO;CACjC,WAAW,UAAU;CACrB,MAAM,QAAQ,iBAAiB,WAAW,UAAU,UAAU;CAE9D,oBACC,YACO,EACN,aAAa;EACZ,MAAM,MAAM,aAAa;EACzB,IAAI,OAAO,YAAY,YAAY,MAClC,IAAI,MAAM,YAAY,OAAO;EAE9B,WAAW,QAAQ,IAAI;CACxB,EACD,IACA,CAAC,CACF;CAEA,gBAAgB;EACf,IAAI,CAAC,OACJ;EAED,MAAM,YAAY,aAAa;EAC/B,MAAM,MAAM,aAAa;EACzB,IAAI,CAAC,aAAa,CAAC,KAClB;EAED,MAAM,WAAW,IAAI,OAAO,WAAW;GACtC,GAAG,WAAW;GACd,SAAS;GACT,WAAW,UAAkB,WAAW,QAAQ,KAAK;GACrD,0BAA0B,WAAW,QAAQ,IAAI;GACjD,wBAAwB,WAAW,QAAQ,IAAI;EAChD,CAAC;EACD,YAAY,UAAU;EACtB,aAAa;GACZ,YAAY,UAAU;GACtB,IAAI;IACH,IAAI,OAAO,QAAQ;GACpB,QAAQ,CAAC;EACV;CACD,GAAG,CAAC,OAAO,OAAO,CAAC;CAEnB,OAAO,oBAAC,OAAD;EAAK,KAAK;EAAyB;CAAY,CAAA;AACvD"}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { useEffect, useState } from "react";
|
|
3
3
|
//#region src/react/captcha/useCaptchaScript.ts
|
|
4
4
|
const pending = /* @__PURE__ */ new Map();
|
|
5
|
-
const
|
|
5
|
+
const injectScript = (src) => {
|
|
6
6
|
const cached = pending.get(src);
|
|
7
7
|
if (cached) return cached;
|
|
8
8
|
const promise = new Promise((resolve, reject) => {
|
|
@@ -20,23 +20,41 @@ const loadScript = (src) => {
|
|
|
20
20
|
pending.set(src, promise);
|
|
21
21
|
return promise;
|
|
22
22
|
};
|
|
23
|
+
const customCaches = /* @__PURE__ */ new WeakMap();
|
|
24
|
+
const loadThrough = (loader, src) => {
|
|
25
|
+
let cache = customCaches.get(loader);
|
|
26
|
+
if (!cache) {
|
|
27
|
+
cache = /* @__PURE__ */ new Map();
|
|
28
|
+
customCaches.set(loader, cache);
|
|
29
|
+
}
|
|
30
|
+
const cached = cache.get(src);
|
|
31
|
+
if (cached) return cached;
|
|
32
|
+
const promise = loader(src).catch((error) => {
|
|
33
|
+
cache.delete(src);
|
|
34
|
+
throw error;
|
|
35
|
+
});
|
|
36
|
+
cache.set(src, promise);
|
|
37
|
+
return promise;
|
|
38
|
+
};
|
|
23
39
|
/**
|
|
24
40
|
* Load a vendor captcha script once per src, shared across all widget instances. SSR-safe (the
|
|
25
41
|
* DOM is only touched inside the effect). A failed load evicts the cache entry so a later mount
|
|
26
|
-
* can retry; consumers just see `ready` stay false.
|
|
42
|
+
* can retry; consumers just see `ready` stay false. A host `loadScript` (e.g. a consent-gated
|
|
43
|
+
* loader) replaces the `document.head` injection; pass a stable reference, since a new function
|
|
44
|
+
* identity re-runs the load with a fresh cache.
|
|
27
45
|
*/
|
|
28
|
-
const useCaptchaScript = (src) => {
|
|
46
|
+
const useCaptchaScript = (src, loadScript) => {
|
|
29
47
|
const [ready, setReady] = useState(false);
|
|
30
48
|
useEffect(() => {
|
|
31
49
|
let active = true;
|
|
32
50
|
setReady(false);
|
|
33
|
-
loadScript(src).then(() => {
|
|
51
|
+
(loadScript ? loadThrough(loadScript, src) : injectScript(src)).then(() => {
|
|
34
52
|
if (active) setReady(true);
|
|
35
53
|
}, () => {});
|
|
36
54
|
return () => {
|
|
37
55
|
active = false;
|
|
38
56
|
};
|
|
39
|
-
}, [src]);
|
|
57
|
+
}, [src, loadScript]);
|
|
40
58
|
return ready;
|
|
41
59
|
};
|
|
42
60
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useCaptchaScript.js","names":[],"sources":["../../../src/react/captcha/useCaptchaScript.ts"],"sourcesContent":["'use client'\n\nimport { useEffect, useState } from 'react'\n\nconst pending = new Map<string, Promise<void>>()\n\nconst
|
|
1
|
+
{"version":3,"file":"useCaptchaScript.js","names":[],"sources":["../../../src/react/captcha/useCaptchaScript.ts"],"sourcesContent":["'use client'\n\nimport { useEffect, useState } from 'react'\nimport type { LoadScript } from '../adapters'\n\nconst pending = new Map<string, Promise<void>>()\n\nconst injectScript = (src: string): Promise<void> => {\n\tconst cached = pending.get(src)\n\tif (cached) {\n\t\treturn cached\n\t}\n\tconst promise = new Promise<void>((resolve, reject) => {\n\t\tconst script = document.createElement('script')\n\t\tscript.src = src\n\t\tscript.async = true\n\t\tscript.addEventListener('load', () => resolve())\n\t\tscript.addEventListener('error', () => {\n\t\t\tpending.delete(src)\n\t\t\tscript.remove()\n\t\t\treject(new Error(`Failed to load ${src}`))\n\t\t})\n\t\tdocument.head.appendChild(script)\n\t})\n\tpending.set(src, promise)\n\treturn promise\n}\n\n// A custom loader gets the same per-src cache and evict-on-failure retry, keyed per loader so two\n// adapters (or the default injector) never share entries.\nconst customCaches = new WeakMap<LoadScript, Map<string, Promise<void>>>()\n\nconst loadThrough = (loader: LoadScript, src: string): Promise<void> => {\n\tlet cache = customCaches.get(loader)\n\tif (!cache) {\n\t\tcache = new Map()\n\t\tcustomCaches.set(loader, cache)\n\t}\n\tconst cached = cache.get(src)\n\tif (cached) {\n\t\treturn cached\n\t}\n\tconst promise = loader(src).catch((error) => {\n\t\tcache.delete(src)\n\t\tthrow error\n\t})\n\tcache.set(src, promise)\n\treturn promise\n}\n\n/**\n * Load a vendor captcha script once per src, shared across all widget instances. SSR-safe (the\n * DOM is only touched inside the effect). A failed load evicts the cache entry so a later mount\n * can retry; consumers just see `ready` stay false. A host `loadScript` (e.g. a consent-gated\n * loader) replaces the `document.head` injection; pass a stable reference, since a new function\n * identity re-runs the load with a fresh cache.\n */\nexport const useCaptchaScript = (src: string, loadScript?: LoadScript): boolean => {\n\tconst [ready, setReady] = useState(false)\n\tuseEffect(() => {\n\t\tlet active = true\n\t\tsetReady(false)\n\t\tconst load = loadScript ? loadThrough(loadScript, src) : injectScript(src)\n\t\tload.then(\n\t\t\t() => {\n\t\t\t\tif (active) {\n\t\t\t\t\tsetReady(true)\n\t\t\t\t}\n\t\t\t},\n\t\t\t() => {}\n\t\t)\n\t\treturn () => {\n\t\t\tactive = false\n\t\t}\n\t}, [src, loadScript])\n\treturn ready\n}\n"],"mappings":";;;AAKA,MAAM,0BAAU,IAAI,IAA2B;AAE/C,MAAM,gBAAgB,QAA+B;CACpD,MAAM,SAAS,QAAQ,IAAI,GAAG;CAC9B,IAAI,QACH,OAAO;CAER,MAAM,UAAU,IAAI,SAAe,SAAS,WAAW;EACtD,MAAM,SAAS,SAAS,cAAc,QAAQ;EAC9C,OAAO,MAAM;EACb,OAAO,QAAQ;EACf,OAAO,iBAAiB,cAAc,QAAQ,CAAC;EAC/C,OAAO,iBAAiB,eAAe;GACtC,QAAQ,OAAO,GAAG;GAClB,OAAO,OAAO;GACd,uBAAO,IAAI,MAAM,kBAAkB,KAAK,CAAC;EAC1C,CAAC;EACD,SAAS,KAAK,YAAY,MAAM;CACjC,CAAC;CACD,QAAQ,IAAI,KAAK,OAAO;CACxB,OAAO;AACR;AAIA,MAAM,+BAAe,IAAI,QAAgD;AAEzE,MAAM,eAAe,QAAoB,QAA+B;CACvE,IAAI,QAAQ,aAAa,IAAI,MAAM;CACnC,IAAI,CAAC,OAAO;EACX,wBAAQ,IAAI,IAAI;EAChB,aAAa,IAAI,QAAQ,KAAK;CAC/B;CACA,MAAM,SAAS,MAAM,IAAI,GAAG;CAC5B,IAAI,QACH,OAAO;CAER,MAAM,UAAU,OAAO,GAAG,EAAE,OAAO,UAAU;EAC5C,MAAM,OAAO,GAAG;EAChB,MAAM;CACP,CAAC;CACD,MAAM,IAAI,KAAK,OAAO;CACtB,OAAO;AACR;;;;;;;;AASA,MAAa,oBAAoB,KAAa,eAAqC;CAClF,MAAM,CAAC,OAAO,YAAY,SAAS,KAAK;CACxC,gBAAgB;EACf,IAAI,SAAS;EACb,SAAS,KAAK;EAEd,CADa,aAAa,YAAY,YAAY,GAAG,IAAI,aAAa,GAAG,GACpE,WACE;GACL,IAAI,QACH,SAAS,IAAI;EAEf,SACM,CAAC,CACR;EACA,aAAa;GACZ,SAAS;EACV;CACD,GAAG,CAAC,KAAK,UAAU,CAAC;CACpB,OAAO;AACR"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@10x-media/form-builder",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.14",
|
|
4
4
|
"description": "End-to-end forms platform for Payload: author, validate, render, collect, aggregate, and act.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -100,9 +100,9 @@
|
|
|
100
100
|
"typescript": "5.9.3",
|
|
101
101
|
"vitest": "4.1.7",
|
|
102
102
|
"@10x-media/payload-test-harness": "0.0.0",
|
|
103
|
-
"@10x-media/
|
|
103
|
+
"@10x-media/tsdown-config": "0.0.0",
|
|
104
104
|
"@10x-media/tsconfig": "0.0.0",
|
|
105
|
-
"@10x-media/
|
|
105
|
+
"@10x-media/vitest-config": "0.0.0"
|
|
106
106
|
},
|
|
107
107
|
"publishConfig": {
|
|
108
108
|
"access": "public"
|