@form-engine-ts/react 2.9.4 → 2.9.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -81,6 +81,11 @@ relevant feature state, while localization slots receive policy and translation-
81
81
  editors remain unchanged. A slot collection can set `sectionOrder` to arrange `basicSettings`, `completionMessage`,
82
82
  `questions`, `addQuestion`, and `localization` as independent authoring sections.
83
83
 
84
+ Slot actions include `setManualTranslation(locale, target, property, text)`. It resolves the target's source text and
85
+ existing translation metadata, invokes `createManualTranslationMetadata`, and then stores the translated text and
86
+ generated metadata together. Custom field, page, option, and localization slots should use this action for user-edited
87
+ translations; `setLocaleTranslation` remains available for callers that already provide explicit metadata.
88
+
84
89
  Input primitives receive design-system-friendly `name`, `label`, `required`, `error`, and `helperText` props in addition
85
90
  to their normalized string-value callbacks. The `translationActions` slot also receives `translationError`, the latest
86
91
  `translationReport`, and `onClearTranslationError`, allowing a custom MUI action surface to represent the complete
@@ -114,8 +119,8 @@ own labels. Builder action icons can be supplied with `renderIcon`, which resolv
114
119
  renderHeader: ({ title }) => <MyHeader>{title}</MyHeader>,
115
120
  renderPageHeader: ({ page }) => <MyPageHeader page={page} />,
116
121
  renderField: (props) => <MyField {...props} />,
117
- renderSubmitButton: ({ isSubmitting, onSubmit }) => (
118
- <MyButton disabled={isSubmitting} onClick={onSubmit}>Save</MyButton>
122
+ renderSubmitButton: ({ isSubmitting, disabled, onSubmit }) => (
123
+ <MyButton loading={isSubmitting} disabled={disabled} onClick={onSubmit}>Save</MyButton>
119
124
  ),
120
125
  renderSubmitError: ({ error, onRetry }) => <MyError error={error} onRetry={onRetry} />
121
126
  }}
@@ -124,7 +129,11 @@ own labels. Builder action icons can be supplied with `renderIcon`, which resolv
124
129
 
125
130
  Validation runs before `beforeSubmit`. A `"cancel"` result does not call `onSubmit` and preserves values and drafts.
126
131
  Header, page-header, field, navigation, submit, validation-summary, completion, and submit-error slots can replace the
127
- default UI. The localized `completionMessage` is displayed after a successful submission.
132
+ default UI. The localized `completionMessage` is displayed after a successful submission. Set
133
+ `successRenderMode="replace"` to remove the questions, navigation, and submit button after success and focus the
134
+ completion status region; the default `"append"` mode preserves the existing form UI. `hideFormOnSuccess` is retained
135
+ as a deprecated alias for `successRenderMode="replace"`. The submit-button slot receives `submitStatus` and `disabled`
136
+ in addition to `isSubmitting` and `onSubmit`.
128
137
 
129
138
  Pass ordered `submissionGuards` to allow, block, or require confirmation before `onSubmit`. Confirmation receives all
130
139
  guard findings through `renderSubmissionConfirmation`. A `receiptStore` prevents accidental repeat submissions and can
package/dist/index.cjs CHANGED
@@ -1088,7 +1088,7 @@ function BuilderButton(props) {
1088
1088
  ...props,
1089
1089
  className: primitiveClassName(props.className, "feb-button", context.unstyled),
1090
1090
  disabled: context.readOnly || props.disabled === true,
1091
- readOnly: context.readOnly
1091
+ readOnly: context.readOnly || props.readOnly === true
1092
1092
  }
1093
1093
  );
1094
1094
  }
@@ -1103,7 +1103,7 @@ function BuilderIconButton(props) {
1103
1103
  icon,
1104
1104
  className: primitiveClassName(props.className, "feb-icon-button", context.unstyled),
1105
1105
  disabled: context.readOnly || props.disabled === true,
1106
- readOnly: context.readOnly
1106
+ readOnly: context.readOnly || props.readOnly === true
1107
1107
  }
1108
1108
  );
1109
1109
  }
@@ -1116,7 +1116,7 @@ function BuilderTextInput(props) {
1116
1116
  ...props,
1117
1117
  className: primitiveClassName(props.className, "feb-input", context.unstyled),
1118
1118
  disabled: context.readOnly || props.disabled === true,
1119
- readOnly: context.readOnly
1119
+ readOnly: context.readOnly || props.readOnly === true
1120
1120
  }
1121
1121
  );
1122
1122
  }
@@ -1129,7 +1129,7 @@ function BuilderTextArea(props) {
1129
1129
  ...props,
1130
1130
  className: primitiveClassName(props.className, "feb-textarea", context.unstyled),
1131
1131
  disabled: context.readOnly || props.disabled === true,
1132
- readOnly: context.readOnly
1132
+ readOnly: context.readOnly || props.readOnly === true
1133
1133
  }
1134
1134
  );
1135
1135
  }
@@ -1142,7 +1142,7 @@ function BuilderSelect(props) {
1142
1142
  ...props,
1143
1143
  className: primitiveClassName(props.className, "feb-select", context.unstyled),
1144
1144
  disabled: context.readOnly || props.disabled === true,
1145
- readOnly: context.readOnly
1145
+ readOnly: context.readOnly || props.readOnly === true
1146
1146
  }
1147
1147
  );
1148
1148
  }
@@ -1155,7 +1155,7 @@ function BuilderCheckbox(props) {
1155
1155
  ...props,
1156
1156
  className: primitiveClassName(props.className, "feb-checkbox", context.unstyled),
1157
1157
  disabled: context.readOnly || props.disabled === true,
1158
- readOnly: context.readOnly
1158
+ readOnly: context.readOnly || props.readOnly === true
1159
1159
  }
1160
1160
  );
1161
1161
  }
@@ -1301,6 +1301,59 @@ function fieldTypeKey(type) {
1301
1301
  function operatorKey(operator) {
1302
1302
  return `builder.operator.${operator}`;
1303
1303
  }
1304
+ function manualTranslationContext(schema, locale, target, property, translatedText) {
1305
+ if (target.kind === "form") {
1306
+ if (property !== "title" && property !== "description" && property !== "completionMessage") return void 0;
1307
+ return {
1308
+ locale,
1309
+ kind: "form",
1310
+ nodeId: schema.id,
1311
+ property,
1312
+ sourceText: schema[property] ?? "",
1313
+ translatedText,
1314
+ ...schema.translationMetadata?.[locale]?.[property] === void 0 ? {} : { existingTranslationMetadata: schema.translationMetadata[locale]?.[property] }
1315
+ };
1316
+ }
1317
+ if (target.id === void 0) return void 0;
1318
+ if (target.kind === "field") {
1319
+ const field = schema.fields.find((candidate) => candidate.id === target.id);
1320
+ if (field === void 0 || property !== "title" && property !== "description") return void 0;
1321
+ return {
1322
+ locale,
1323
+ kind: "field",
1324
+ nodeId: field.id,
1325
+ property,
1326
+ sourceText: field[property] ?? "",
1327
+ translatedText,
1328
+ ...field.translationMetadata?.[locale]?.[property] === void 0 ? {} : { existingTranslationMetadata: field.translationMetadata[locale]?.[property] }
1329
+ };
1330
+ }
1331
+ if (target.kind === "page") {
1332
+ const page = schema.pages?.find((candidate) => candidate.id === target.id);
1333
+ if (page === void 0 || property !== "title" && property !== "description") return void 0;
1334
+ return {
1335
+ locale,
1336
+ kind: "page",
1337
+ nodeId: page.id,
1338
+ property,
1339
+ sourceText: page[property] ?? "",
1340
+ translatedText,
1341
+ ...page.translationMetadata?.[locale]?.[property] === void 0 ? {} : { existingTranslationMetadata: page.translationMetadata[locale]?.[property] }
1342
+ };
1343
+ }
1344
+ if (property !== "label") return void 0;
1345
+ const option = schema.fields.filter((field) => "options" in field).flatMap((field) => field.options).find((candidate) => candidate.id === target.id);
1346
+ if (option === void 0) return void 0;
1347
+ return {
1348
+ locale,
1349
+ kind: "option",
1350
+ nodeId: option.id,
1351
+ property,
1352
+ sourceText: option.label,
1353
+ translatedText,
1354
+ ...option.translationMetadata?.[locale]?.[property] === void 0 ? {} : { existingTranslationMetadata: option.translationMetadata[locale]?.[property] }
1355
+ };
1356
+ }
1304
1357
  function defaultConditionValue(field) {
1305
1358
  if (field.type === "checkbox") return true;
1306
1359
  if (field.type === "number" || field.type === "rating") return field.min ?? 1;
@@ -1559,25 +1612,30 @@ function FormBuilder({
1559
1612
  setIsTranslating(false);
1560
1613
  }
1561
1614
  };
1562
- const updateManualTranslation = (context) => {
1563
- if (readOnly) return;
1564
- const metadata = createManualTranslationMetadata?.(context);
1565
- const target = context.kind === "form" ? { kind: "form" } : { kind: context.kind, id: context.nodeId };
1566
- executeAction(
1615
+ const setManualTranslation = (targetLocale, target, property, text) => {
1616
+ if (readOnly) return { success: true };
1617
+ const normalizedLocale = targetLocale.trim();
1618
+ const context = manualTranslationContext(schema, normalizedLocale, target, property, text);
1619
+ const metadata = context === void 0 ? void 0 : createManualTranslationMetadata?.(context);
1620
+ return executeAction(
1567
1621
  () => headless.setLocaleTranslation(
1568
- context.locale,
1622
+ normalizedLocale,
1569
1623
  target,
1570
- context.property,
1571
- context.translatedText,
1624
+ property,
1625
+ text,
1572
1626
  metadata === void 0 ? void 0 : { metadata }
1573
1627
  ),
1574
1628
  {
1575
- action: "setLocaleTranslation",
1576
- targetId: context.nodeId,
1577
- params: { locale: context.locale, kind: context.kind, property: context.property }
1629
+ action: "setManualTranslation",
1630
+ ...target.id === void 0 ? {} : { targetId: target.id },
1631
+ params: { locale: normalizedLocale, kind: target.kind, property }
1578
1632
  }
1579
1633
  );
1580
1634
  };
1635
+ const updateManualTranslation = (context) => {
1636
+ const target = context.kind === "form" ? { kind: "form" } : { kind: context.kind, id: context.nodeId };
1637
+ setManualTranslation(context.locale, target, context.property, context.translatedText);
1638
+ };
1581
1639
  const updateFormTranslation = (property, translatedText) => {
1582
1640
  if (editingLocale.length === 0) return;
1583
1641
  updateManualTranslation({
@@ -1686,6 +1744,7 @@ function FormBuilder({
1686
1744
  ...target.id === void 0 ? {} : { targetId: target.id },
1687
1745
  params: { locale: targetLocale, property }
1688
1746
  }),
1747
+ setManualTranslation,
1689
1748
  addLocale: (targetLocale) => executeAction(() => headless.addLocale(targetLocale), {
1690
1749
  action: "addLocale",
1691
1750
  params: { locale: targetLocale }
@@ -3063,6 +3122,8 @@ function ContextFormRenderer({
3063
3122
  autoSaveKey,
3064
3123
  beforeSubmit,
3065
3124
  onDraftSave,
3125
+ successRenderMode = "append",
3126
+ hideFormOnSuccess = false,
3066
3127
  submissionGuards = [],
3067
3128
  receiptStore,
3068
3129
  attemptStore,
@@ -3078,9 +3139,11 @@ function ContextFormRenderer({
3078
3139
  const [focusFieldId, setFocusFieldId] = (0, import_react5.useState)(null);
3079
3140
  const [confirmation, setConfirmation] = (0, import_react5.useState)(null);
3080
3141
  const [guardMessage, setGuardMessage] = (0, import_react5.useState)(null);
3142
+ const [guardsPending, setGuardsPending] = (0, import_react5.useState)(false);
3081
3143
  const [receipt, setReceipt] = (0, import_react5.useState)(null);
3082
3144
  const [receiptLoaded, setReceiptLoaded] = (0, import_react5.useState)(receiptStore === void 0);
3083
3145
  const rendererSubmissionInFlight = (0, import_react5.useRef)(false);
3146
+ const completionRef = (0, import_react5.useRef)(null);
3084
3147
  const pages = form.schema.pages;
3085
3148
  const visiblePageIndexes = (0, import_react5.useMemo)(
3086
3149
  () => pages === void 0 ? [] : pages.flatMap((page, index) => form.pageVisibility[page.id] === true ? [index] : []),
@@ -3090,8 +3153,9 @@ function ContextFormRenderer({
3090
3153
  const activeVisibleIndex = visiblePageIndexes.indexOf(currentPageIndex);
3091
3154
  const fieldIds = activePage === void 0 ? void 0 : new Set(activePage.questionIds);
3092
3155
  const visibleValues = (0, import_react5.useMemo)(() => (0, import_core4.selectVisibleAnswers)(form.schema, form.values), [form.schema, form.values]);
3093
- const submitState = confirmation === null ? form.submitStatus : "confirming";
3156
+ const submitState = confirmation === null && !guardsPending ? form.submitStatus : "confirming";
3094
3157
  const interactionLocked = submitState === "confirming" || submitState === "submitting";
3158
+ const isReplaceMode = successRenderMode === "replace" || hideFormOnSuccess;
3095
3159
  (0, import_react5.useEffect)(() => {
3096
3160
  let active = true;
3097
3161
  if (receiptStore === void 0) {
@@ -3131,6 +3195,10 @@ function ContextFormRenderer({
3131
3195
  setFocusFieldId(null);
3132
3196
  }
3133
3197
  }, [focusFieldId]);
3198
+ (0, import_react5.useEffect)(() => {
3199
+ if (!isReplaceMode || form.submitStatus !== "success") return;
3200
+ completionRef.current?.focus();
3201
+ }, [form.submitStatus, isReplaceMode]);
3134
3202
  (0, import_react5.useEffect)(() => {
3135
3203
  if (autoSaveKey === void 0 || typeof globalThis.localStorage === "undefined") return;
3136
3204
  const loadIdentity = `${autoSaveKey}:${form.schema.id}:${form.schema.version}`;
@@ -3196,13 +3264,14 @@ function ContextFormRenderer({
3196
3264
  };
3197
3265
  };
3198
3266
  const submitValues = async (guardsConfirmed = false) => {
3199
- if (rendererSubmissionInFlight.current || confirmation !== null && !guardsConfirmed) {
3267
+ if (rendererSubmissionInFlight.current || submitState === "submitting" || submitState === "success" || confirmation !== null && !guardsConfirmed) {
3200
3268
  return { status: "cancelled" };
3201
3269
  }
3202
3270
  const validation = (0, import_core4.validateAnswers)(form.schema, form.values);
3203
3271
  const firstInvalidFieldId = validation.issues[0]?.fieldId;
3204
3272
  if (validation.valid && !guardsConfirmed && submissionGuards.length > 0) {
3205
3273
  rendererSubmissionInFlight.current = true;
3274
+ setGuardsPending(true);
3206
3275
  try {
3207
3276
  const guardResult = await runSubmissionGuards(submissionGuards);
3208
3277
  if (guardResult.status === "block") {
@@ -3219,6 +3288,7 @@ function ContextFormRenderer({
3219
3288
  }
3220
3289
  } finally {
3221
3290
  rendererSubmissionInFlight.current = false;
3291
+ setGuardsPending(false);
3222
3292
  }
3223
3293
  }
3224
3294
  setGuardMessage(null);
@@ -3256,7 +3326,6 @@ function ContextFormRenderer({
3256
3326
  };
3257
3327
  try {
3258
3328
  await receiptStore.save(storedReceipt);
3259
- setReceipt(storedReceipt);
3260
3329
  } catch (cause) {
3261
3330
  const error = cause instanceof Error ? cause : new Error(String(cause));
3262
3331
  try {
@@ -3283,7 +3352,7 @@ function ContextFormRenderer({
3283
3352
  };
3284
3353
  const handleSubmit = (event) => {
3285
3354
  event.preventDefault();
3286
- if (interactionLocked) return;
3355
+ if (submitState === "submitting" || submitState === "confirming" || submitState === "success") return;
3287
3356
  void submitValues();
3288
3357
  };
3289
3358
  const confirmSubmission = () => {
@@ -3302,7 +3371,17 @@ function ContextFormRenderer({
3302
3371
  const validationIssues = Object.values(form.errors).filter((issue) => issue !== void 0);
3303
3372
  const canPrev = pages !== void 0 && activeVisibleIndex > 0;
3304
3373
  const canNext = pages !== void 0 && activeVisibleIndex < visiblePageIndexes.length - 1;
3305
- const renderSubmitButton = () => slots.renderSubmitButton?.({ isSubmitting: interactionLocked, onSubmit: () => void submitValues() }) ?? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { className: "fe-submit", type: "submit", disabled: interactionLocked, children: form.translate(form.schema.submitLabelKey ?? "form.submit") });
3374
+ const renderSubmitButton = () => {
3375
+ const submitButtonProps = {
3376
+ isSubmitting: submitState === "submitting",
3377
+ submitStatus: submitState,
3378
+ disabled: interactionLocked || submitState === "success",
3379
+ onSubmit: () => void submitValues()
3380
+ };
3381
+ return slots.renderSubmitButton?.(submitButtonProps) ?? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { className: "fe-submit", type: "submit", disabled: submitButtonProps.disabled, children: form.translate(form.schema.submitLabelKey ?? "form.submit") });
3382
+ };
3383
+ const completionMessage = form.schema.completionMessage ?? (successMessageKey === void 0 ? "Submitted." : form.translate(successMessageKey));
3384
+ const completionRegion = /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { ref: completionRef, className: "fe-completion", role: "status", "aria-live": "polite", tabIndex: -1, children: slots.renderCompletion?.({ message: completionMessage }) ?? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { children: completionMessage }) });
3306
3385
  if (!receiptLoaded) return null;
3307
3386
  if (receipt !== null) {
3308
3387
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: `fe-form fe-already-submitted ${className}`.trim(), children: slots.renderAlreadySubmitted?.({
@@ -3313,6 +3392,9 @@ function ContextFormRenderer({
3313
3392
  receiptStore === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", onClick: () => void resetReceipt(), children: form.translate("form.submitAnother") })
3314
3393
  ] }) });
3315
3394
  }
3395
+ if (form.submitStatus === "success" && isReplaceMode) {
3396
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: `fe-form ${className}`.trim(), children: completionRegion });
3397
+ }
3316
3398
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("form", { ref: formRef, className: `fe-form ${className}`.trim(), noValidate: true, onSubmit: handleSubmit, children: [
3317
3399
  slots.renderHeader?.({
3318
3400
  title: form.schema.title,
@@ -3431,9 +3513,7 @@ function ContextFormRenderer({
3431
3513
  canNext ? null : renderSubmitButton()
3432
3514
  ] }),
3433
3515
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "fe-status", "aria-live": "polite", children: [
3434
- form.submitStatus === "success" ? slots.renderCompletion?.({
3435
- message: form.schema.completionMessage ?? (successMessageKey === void 0 ? "Submitted." : form.translate(successMessageKey))
3436
- }) ?? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { role: "status", children: form.schema.completionMessage ?? (successMessageKey === void 0 ? "Submitted." : form.translate(successMessageKey)) }) : null,
3516
+ form.submitStatus === "success" ? completionRegion : null,
3437
3517
  form.submitStatus === "error" && form.submitError !== null ? slots.renderSubmitError?.({ error: form.submitError, onRetry: () => void submitValues() }) ?? (errorMessageKey === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { role: "alert", children: form.translate(errorMessageKey) })) : null
3438
3518
  ] })
3439
3519
  ] });
package/dist/index.d.cts CHANGED
@@ -229,10 +229,17 @@ interface FormBuilderComponents {
229
229
  readonly renderIcon?: (actionType: BuilderActionIconType) => ReactNode;
230
230
  }
231
231
  type FormBuilderActions = Omit<FormBuilderResult, "schema" | "validationIssues">;
232
+ interface ManualTranslationTarget {
233
+ readonly kind: "form" | "page" | "field" | "option";
234
+ readonly id?: string;
235
+ }
236
+ interface BuilderSlotActions extends FormBuilderActions {
237
+ readonly setManualTranslation: (locale: string, target: ManualTranslationTarget, property: "title" | "description" | "label" | "completionMessage", text: string) => BuilderActionResult;
238
+ }
232
239
  interface BuilderSlotBaseProps {
233
240
  readonly schema: FormSchema;
234
241
  readonly readOnly: boolean;
235
- readonly actions: FormBuilderActions;
242
+ readonly actions: BuilderSlotActions;
236
243
  readonly components: Required<FormBuilderComponents>;
237
244
  readonly translate: (key: string, params?: Record<string, unknown>) => string;
238
245
  }
@@ -302,7 +309,7 @@ interface FormBuilderSlots {
302
309
  readonly sectionOrder?: readonly FormBuilderSectionName[];
303
310
  }
304
311
  interface BuilderActionContext {
305
- readonly action: "addField" | "removeField" | "moveField" | "changeFieldType" | "updateField" | "addOption" | "removeOption" | "moveOption" | "updateOption" | "addPage" | "removePage" | "movePage" | "updatePage" | "assignFieldToPage" | "addLocale" | "setDefaultLocale" | "setDisplayCondition" | "setSourceText" | "setLocaleTranslation";
312
+ readonly action: "addField" | "removeField" | "moveField" | "changeFieldType" | "updateField" | "addOption" | "removeOption" | "moveOption" | "updateOption" | "addPage" | "removePage" | "movePage" | "updatePage" | "assignFieldToPage" | "addLocale" | "setDefaultLocale" | "setDisplayCondition" | "setSourceText" | "setLocaleTranslation" | "setManualTranslation";
306
313
  readonly targetId?: string;
307
314
  readonly params?: Record<string, unknown>;
308
315
  }
@@ -344,7 +351,16 @@ type SubmissionGuardResult = {
344
351
  readonly message?: string;
345
352
  };
346
353
  type SubmissionGuard = (schema: FormSchema, values: Record<string, unknown>) => SubmissionGuardResult | Promise<SubmissionGuardResult>;
354
+ type FormSuccessRenderMode = "append" | "replace";
355
+ type FormSubmitStatus = "idle" | "submitting" | "confirming" | "success" | "error";
356
+ /** @deprecated Use FormSubmitStatus instead. */
347
357
  type FormSubmitState = "idle" | "submitting" | "confirming" | "success" | "error";
358
+ interface RenderSubmitButtonProps {
359
+ readonly isSubmitting: boolean;
360
+ readonly submitStatus: FormSubmitStatus;
361
+ readonly disabled: boolean;
362
+ readonly onSubmit: () => void;
363
+ }
348
364
  interface SubmissionConfirmationSlotProps {
349
365
  readonly findings: readonly SensitiveDataFinding[];
350
366
  readonly message?: string;
@@ -377,10 +393,7 @@ interface FormRendererSlots {
377
393
  readonly onPrev: () => void;
378
394
  readonly onNext: () => void;
379
395
  }) => ReactNode;
380
- readonly renderSubmitButton?: (props: {
381
- readonly isSubmitting: boolean;
382
- readonly onSubmit: () => void;
383
- }) => ReactNode;
396
+ readonly renderSubmitButton?: (props: RenderSubmitButtonProps) => ReactNode;
384
397
  readonly renderValidationSummary?: (props: {
385
398
  readonly issues: readonly ValidationError[];
386
399
  }) => ReactNode;
@@ -494,6 +507,13 @@ type FieldComponents = Partial<Record<FieldType, ComponentType<FieldComponentPro
494
507
  interface FormRendererPresentationProps extends SubmissionProtectionProps {
495
508
  readonly components?: FieldComponents;
496
509
  readonly className?: string;
510
+ /**
511
+ * Controls where the completion message is rendered after a successful submission.
512
+ * Defaults to "append" for backwards compatibility.
513
+ */
514
+ readonly successRenderMode?: FormSuccessRenderMode;
515
+ /** @deprecated Use successRenderMode="replace" instead. */
516
+ readonly hideFormOnSuccess?: boolean;
497
517
  readonly successMessageKey?: string;
498
518
  readonly errorMessageKey?: string;
499
519
  readonly autoSaveKey?: string;
@@ -512,4 +532,4 @@ interface StandaloneFormRendererProps extends FormRendererPresentationProps {
512
532
  type FormRendererProps = FormRendererPresentationProps | StandaloneFormRendererProps;
513
533
  declare function FormRenderer(props: FormRendererProps): react.JSX.Element;
514
534
 
515
- export { type BeforeSubmit, type BuilderActionContext, type BuilderActionError, type BuilderActionIconType, type BuilderActionResult, type BuilderButtonProps, type BuilderCheckboxProps, type BuilderErrorMessageProps, type BuilderFactories, type BuilderFieldEditorSlotProps, type BuilderFieldsetProps, type BuilderIconButtonProps, type BuilderIdKind, type BuilderLocalizationSlotProps, type BuilderOptionEditorSlotProps, type BuilderPagesSlotProps, type BuilderPolicy, type BuilderSectionProps, type BuilderSelectOption, type BuilderSelectProps, type BuilderTextAreaProps, type BuilderTextInputProps, type BuilderTextTarget, type BuilderToolbarSlotProps, type BuilderTranslationActionsSlotProps, type ComponentBaseProps, type FieldComponentProps, type FieldComponents, type FieldState, FormBuilder, type FormBuilderActions, type FormBuilderComponents, type FormBuilderFeatures, type FormBuilderOptions, type FormBuilderProps, type FormBuilderResult, type FormBuilderSectionName, type FormBuilderSlots, type FormContextValue, FormProvider, type FormProviderProps, FormRenderer, type FormRendererPresentationProps, type FormRendererProps, type FormRendererSlots, type FormSubmitHandler, type FormSubmitState, type IconButtonProps, type InputComponentProps, type ManualTranslationContext, type StandaloneFormRendererProps, type SubmissionAttempt, type SubmissionAttemptStore, type SubmissionConfirmationSlotProps, type SubmissionGuard, type SubmissionGuardResult, type SubmissionProtectionProps, type SubmissionReceipt, type SubmissionReceiptQuery, type SubmissionReceiptStore, type SubmitResponse, type SubmitResult, type SubmitStatus, type UseSubmissionReceiptsResult, createLocalStorageSubmissionAttemptStore, createLocalStorageSubmissionReceiptStore, resolveInitialFieldType, submissionReceiptQueryKey, useField, useForm, useFormBuilder, useSubmissionReceipts };
535
+ export { type BeforeSubmit, type BuilderActionContext, type BuilderActionError, type BuilderActionIconType, type BuilderActionResult, type BuilderButtonProps, type BuilderCheckboxProps, type BuilderErrorMessageProps, type BuilderFactories, type BuilderFieldEditorSlotProps, type BuilderFieldsetProps, type BuilderIconButtonProps, type BuilderIdKind, type BuilderLocalizationSlotProps, type BuilderOptionEditorSlotProps, type BuilderPagesSlotProps, type BuilderPolicy, type BuilderSectionProps, type BuilderSelectOption, type BuilderSelectProps, type BuilderSlotActions, type BuilderTextAreaProps, type BuilderTextInputProps, type BuilderTextTarget, type BuilderToolbarSlotProps, type BuilderTranslationActionsSlotProps, type ComponentBaseProps, type FieldComponentProps, type FieldComponents, type FieldState, FormBuilder, type FormBuilderActions, type FormBuilderComponents, type FormBuilderFeatures, type FormBuilderOptions, type FormBuilderProps, type FormBuilderResult, type FormBuilderSectionName, type FormBuilderSlots, type FormContextValue, FormProvider, type FormProviderProps, FormRenderer, type FormRendererPresentationProps, type FormRendererProps, type FormRendererSlots, type FormSubmitHandler, type FormSubmitState, type FormSubmitStatus, type FormSuccessRenderMode, type IconButtonProps, type InputComponentProps, type ManualTranslationContext, type ManualTranslationTarget, type RenderSubmitButtonProps, type StandaloneFormRendererProps, type SubmissionAttempt, type SubmissionAttemptStore, type SubmissionConfirmationSlotProps, type SubmissionGuard, type SubmissionGuardResult, type SubmissionProtectionProps, type SubmissionReceipt, type SubmissionReceiptQuery, type SubmissionReceiptStore, type SubmitResponse, type SubmitResult, type SubmitStatus, type UseSubmissionReceiptsResult, createLocalStorageSubmissionAttemptStore, createLocalStorageSubmissionReceiptStore, resolveInitialFieldType, submissionReceiptQueryKey, useField, useForm, useFormBuilder, useSubmissionReceipts };
package/dist/index.d.ts CHANGED
@@ -229,10 +229,17 @@ interface FormBuilderComponents {
229
229
  readonly renderIcon?: (actionType: BuilderActionIconType) => ReactNode;
230
230
  }
231
231
  type FormBuilderActions = Omit<FormBuilderResult, "schema" | "validationIssues">;
232
+ interface ManualTranslationTarget {
233
+ readonly kind: "form" | "page" | "field" | "option";
234
+ readonly id?: string;
235
+ }
236
+ interface BuilderSlotActions extends FormBuilderActions {
237
+ readonly setManualTranslation: (locale: string, target: ManualTranslationTarget, property: "title" | "description" | "label" | "completionMessage", text: string) => BuilderActionResult;
238
+ }
232
239
  interface BuilderSlotBaseProps {
233
240
  readonly schema: FormSchema;
234
241
  readonly readOnly: boolean;
235
- readonly actions: FormBuilderActions;
242
+ readonly actions: BuilderSlotActions;
236
243
  readonly components: Required<FormBuilderComponents>;
237
244
  readonly translate: (key: string, params?: Record<string, unknown>) => string;
238
245
  }
@@ -302,7 +309,7 @@ interface FormBuilderSlots {
302
309
  readonly sectionOrder?: readonly FormBuilderSectionName[];
303
310
  }
304
311
  interface BuilderActionContext {
305
- readonly action: "addField" | "removeField" | "moveField" | "changeFieldType" | "updateField" | "addOption" | "removeOption" | "moveOption" | "updateOption" | "addPage" | "removePage" | "movePage" | "updatePage" | "assignFieldToPage" | "addLocale" | "setDefaultLocale" | "setDisplayCondition" | "setSourceText" | "setLocaleTranslation";
312
+ readonly action: "addField" | "removeField" | "moveField" | "changeFieldType" | "updateField" | "addOption" | "removeOption" | "moveOption" | "updateOption" | "addPage" | "removePage" | "movePage" | "updatePage" | "assignFieldToPage" | "addLocale" | "setDefaultLocale" | "setDisplayCondition" | "setSourceText" | "setLocaleTranslation" | "setManualTranslation";
306
313
  readonly targetId?: string;
307
314
  readonly params?: Record<string, unknown>;
308
315
  }
@@ -344,7 +351,16 @@ type SubmissionGuardResult = {
344
351
  readonly message?: string;
345
352
  };
346
353
  type SubmissionGuard = (schema: FormSchema, values: Record<string, unknown>) => SubmissionGuardResult | Promise<SubmissionGuardResult>;
354
+ type FormSuccessRenderMode = "append" | "replace";
355
+ type FormSubmitStatus = "idle" | "submitting" | "confirming" | "success" | "error";
356
+ /** @deprecated Use FormSubmitStatus instead. */
347
357
  type FormSubmitState = "idle" | "submitting" | "confirming" | "success" | "error";
358
+ interface RenderSubmitButtonProps {
359
+ readonly isSubmitting: boolean;
360
+ readonly submitStatus: FormSubmitStatus;
361
+ readonly disabled: boolean;
362
+ readonly onSubmit: () => void;
363
+ }
348
364
  interface SubmissionConfirmationSlotProps {
349
365
  readonly findings: readonly SensitiveDataFinding[];
350
366
  readonly message?: string;
@@ -377,10 +393,7 @@ interface FormRendererSlots {
377
393
  readonly onPrev: () => void;
378
394
  readonly onNext: () => void;
379
395
  }) => ReactNode;
380
- readonly renderSubmitButton?: (props: {
381
- readonly isSubmitting: boolean;
382
- readonly onSubmit: () => void;
383
- }) => ReactNode;
396
+ readonly renderSubmitButton?: (props: RenderSubmitButtonProps) => ReactNode;
384
397
  readonly renderValidationSummary?: (props: {
385
398
  readonly issues: readonly ValidationError[];
386
399
  }) => ReactNode;
@@ -494,6 +507,13 @@ type FieldComponents = Partial<Record<FieldType, ComponentType<FieldComponentPro
494
507
  interface FormRendererPresentationProps extends SubmissionProtectionProps {
495
508
  readonly components?: FieldComponents;
496
509
  readonly className?: string;
510
+ /**
511
+ * Controls where the completion message is rendered after a successful submission.
512
+ * Defaults to "append" for backwards compatibility.
513
+ */
514
+ readonly successRenderMode?: FormSuccessRenderMode;
515
+ /** @deprecated Use successRenderMode="replace" instead. */
516
+ readonly hideFormOnSuccess?: boolean;
497
517
  readonly successMessageKey?: string;
498
518
  readonly errorMessageKey?: string;
499
519
  readonly autoSaveKey?: string;
@@ -512,4 +532,4 @@ interface StandaloneFormRendererProps extends FormRendererPresentationProps {
512
532
  type FormRendererProps = FormRendererPresentationProps | StandaloneFormRendererProps;
513
533
  declare function FormRenderer(props: FormRendererProps): react.JSX.Element;
514
534
 
515
- export { type BeforeSubmit, type BuilderActionContext, type BuilderActionError, type BuilderActionIconType, type BuilderActionResult, type BuilderButtonProps, type BuilderCheckboxProps, type BuilderErrorMessageProps, type BuilderFactories, type BuilderFieldEditorSlotProps, type BuilderFieldsetProps, type BuilderIconButtonProps, type BuilderIdKind, type BuilderLocalizationSlotProps, type BuilderOptionEditorSlotProps, type BuilderPagesSlotProps, type BuilderPolicy, type BuilderSectionProps, type BuilderSelectOption, type BuilderSelectProps, type BuilderTextAreaProps, type BuilderTextInputProps, type BuilderTextTarget, type BuilderToolbarSlotProps, type BuilderTranslationActionsSlotProps, type ComponentBaseProps, type FieldComponentProps, type FieldComponents, type FieldState, FormBuilder, type FormBuilderActions, type FormBuilderComponents, type FormBuilderFeatures, type FormBuilderOptions, type FormBuilderProps, type FormBuilderResult, type FormBuilderSectionName, type FormBuilderSlots, type FormContextValue, FormProvider, type FormProviderProps, FormRenderer, type FormRendererPresentationProps, type FormRendererProps, type FormRendererSlots, type FormSubmitHandler, type FormSubmitState, type IconButtonProps, type InputComponentProps, type ManualTranslationContext, type StandaloneFormRendererProps, type SubmissionAttempt, type SubmissionAttemptStore, type SubmissionConfirmationSlotProps, type SubmissionGuard, type SubmissionGuardResult, type SubmissionProtectionProps, type SubmissionReceipt, type SubmissionReceiptQuery, type SubmissionReceiptStore, type SubmitResponse, type SubmitResult, type SubmitStatus, type UseSubmissionReceiptsResult, createLocalStorageSubmissionAttemptStore, createLocalStorageSubmissionReceiptStore, resolveInitialFieldType, submissionReceiptQueryKey, useField, useForm, useFormBuilder, useSubmissionReceipts };
535
+ export { type BeforeSubmit, type BuilderActionContext, type BuilderActionError, type BuilderActionIconType, type BuilderActionResult, type BuilderButtonProps, type BuilderCheckboxProps, type BuilderErrorMessageProps, type BuilderFactories, type BuilderFieldEditorSlotProps, type BuilderFieldsetProps, type BuilderIconButtonProps, type BuilderIdKind, type BuilderLocalizationSlotProps, type BuilderOptionEditorSlotProps, type BuilderPagesSlotProps, type BuilderPolicy, type BuilderSectionProps, type BuilderSelectOption, type BuilderSelectProps, type BuilderSlotActions, type BuilderTextAreaProps, type BuilderTextInputProps, type BuilderTextTarget, type BuilderToolbarSlotProps, type BuilderTranslationActionsSlotProps, type ComponentBaseProps, type FieldComponentProps, type FieldComponents, type FieldState, FormBuilder, type FormBuilderActions, type FormBuilderComponents, type FormBuilderFeatures, type FormBuilderOptions, type FormBuilderProps, type FormBuilderResult, type FormBuilderSectionName, type FormBuilderSlots, type FormContextValue, FormProvider, type FormProviderProps, FormRenderer, type FormRendererPresentationProps, type FormRendererProps, type FormRendererSlots, type FormSubmitHandler, type FormSubmitState, type FormSubmitStatus, type FormSuccessRenderMode, type IconButtonProps, type InputComponentProps, type ManualTranslationContext, type ManualTranslationTarget, type RenderSubmitButtonProps, type StandaloneFormRendererProps, type SubmissionAttempt, type SubmissionAttemptStore, type SubmissionConfirmationSlotProps, type SubmissionGuard, type SubmissionGuardResult, type SubmissionProtectionProps, type SubmissionReceipt, type SubmissionReceiptQuery, type SubmissionReceiptStore, type SubmitResponse, type SubmitResult, type SubmitStatus, type UseSubmissionReceiptsResult, createLocalStorageSubmissionAttemptStore, createLocalStorageSubmissionReceiptStore, resolveInitialFieldType, submissionReceiptQueryKey, useField, useForm, useFormBuilder, useSubmissionReceipts };
package/dist/index.js CHANGED
@@ -1058,7 +1058,7 @@ function BuilderButton(props) {
1058
1058
  ...props,
1059
1059
  className: primitiveClassName(props.className, "feb-button", context.unstyled),
1060
1060
  disabled: context.readOnly || props.disabled === true,
1061
- readOnly: context.readOnly
1061
+ readOnly: context.readOnly || props.readOnly === true
1062
1062
  }
1063
1063
  );
1064
1064
  }
@@ -1073,7 +1073,7 @@ function BuilderIconButton(props) {
1073
1073
  icon,
1074
1074
  className: primitiveClassName(props.className, "feb-icon-button", context.unstyled),
1075
1075
  disabled: context.readOnly || props.disabled === true,
1076
- readOnly: context.readOnly
1076
+ readOnly: context.readOnly || props.readOnly === true
1077
1077
  }
1078
1078
  );
1079
1079
  }
@@ -1086,7 +1086,7 @@ function BuilderTextInput(props) {
1086
1086
  ...props,
1087
1087
  className: primitiveClassName(props.className, "feb-input", context.unstyled),
1088
1088
  disabled: context.readOnly || props.disabled === true,
1089
- readOnly: context.readOnly
1089
+ readOnly: context.readOnly || props.readOnly === true
1090
1090
  }
1091
1091
  );
1092
1092
  }
@@ -1099,7 +1099,7 @@ function BuilderTextArea(props) {
1099
1099
  ...props,
1100
1100
  className: primitiveClassName(props.className, "feb-textarea", context.unstyled),
1101
1101
  disabled: context.readOnly || props.disabled === true,
1102
- readOnly: context.readOnly
1102
+ readOnly: context.readOnly || props.readOnly === true
1103
1103
  }
1104
1104
  );
1105
1105
  }
@@ -1112,7 +1112,7 @@ function BuilderSelect(props) {
1112
1112
  ...props,
1113
1113
  className: primitiveClassName(props.className, "feb-select", context.unstyled),
1114
1114
  disabled: context.readOnly || props.disabled === true,
1115
- readOnly: context.readOnly
1115
+ readOnly: context.readOnly || props.readOnly === true
1116
1116
  }
1117
1117
  );
1118
1118
  }
@@ -1125,7 +1125,7 @@ function BuilderCheckbox(props) {
1125
1125
  ...props,
1126
1126
  className: primitiveClassName(props.className, "feb-checkbox", context.unstyled),
1127
1127
  disabled: context.readOnly || props.disabled === true,
1128
- readOnly: context.readOnly
1128
+ readOnly: context.readOnly || props.readOnly === true
1129
1129
  }
1130
1130
  );
1131
1131
  }
@@ -1271,6 +1271,59 @@ function fieldTypeKey(type) {
1271
1271
  function operatorKey(operator) {
1272
1272
  return `builder.operator.${operator}`;
1273
1273
  }
1274
+ function manualTranslationContext(schema, locale, target, property, translatedText) {
1275
+ if (target.kind === "form") {
1276
+ if (property !== "title" && property !== "description" && property !== "completionMessage") return void 0;
1277
+ return {
1278
+ locale,
1279
+ kind: "form",
1280
+ nodeId: schema.id,
1281
+ property,
1282
+ sourceText: schema[property] ?? "",
1283
+ translatedText,
1284
+ ...schema.translationMetadata?.[locale]?.[property] === void 0 ? {} : { existingTranslationMetadata: schema.translationMetadata[locale]?.[property] }
1285
+ };
1286
+ }
1287
+ if (target.id === void 0) return void 0;
1288
+ if (target.kind === "field") {
1289
+ const field = schema.fields.find((candidate) => candidate.id === target.id);
1290
+ if (field === void 0 || property !== "title" && property !== "description") return void 0;
1291
+ return {
1292
+ locale,
1293
+ kind: "field",
1294
+ nodeId: field.id,
1295
+ property,
1296
+ sourceText: field[property] ?? "",
1297
+ translatedText,
1298
+ ...field.translationMetadata?.[locale]?.[property] === void 0 ? {} : { existingTranslationMetadata: field.translationMetadata[locale]?.[property] }
1299
+ };
1300
+ }
1301
+ if (target.kind === "page") {
1302
+ const page = schema.pages?.find((candidate) => candidate.id === target.id);
1303
+ if (page === void 0 || property !== "title" && property !== "description") return void 0;
1304
+ return {
1305
+ locale,
1306
+ kind: "page",
1307
+ nodeId: page.id,
1308
+ property,
1309
+ sourceText: page[property] ?? "",
1310
+ translatedText,
1311
+ ...page.translationMetadata?.[locale]?.[property] === void 0 ? {} : { existingTranslationMetadata: page.translationMetadata[locale]?.[property] }
1312
+ };
1313
+ }
1314
+ if (property !== "label") return void 0;
1315
+ const option = schema.fields.filter((field) => "options" in field).flatMap((field) => field.options).find((candidate) => candidate.id === target.id);
1316
+ if (option === void 0) return void 0;
1317
+ return {
1318
+ locale,
1319
+ kind: "option",
1320
+ nodeId: option.id,
1321
+ property,
1322
+ sourceText: option.label,
1323
+ translatedText,
1324
+ ...option.translationMetadata?.[locale]?.[property] === void 0 ? {} : { existingTranslationMetadata: option.translationMetadata[locale]?.[property] }
1325
+ };
1326
+ }
1274
1327
  function defaultConditionValue(field) {
1275
1328
  if (field.type === "checkbox") return true;
1276
1329
  if (field.type === "number" || field.type === "rating") return field.min ?? 1;
@@ -1529,25 +1582,30 @@ function FormBuilder({
1529
1582
  setIsTranslating(false);
1530
1583
  }
1531
1584
  };
1532
- const updateManualTranslation = (context) => {
1533
- if (readOnly) return;
1534
- const metadata = createManualTranslationMetadata?.(context);
1535
- const target = context.kind === "form" ? { kind: "form" } : { kind: context.kind, id: context.nodeId };
1536
- executeAction(
1585
+ const setManualTranslation = (targetLocale, target, property, text) => {
1586
+ if (readOnly) return { success: true };
1587
+ const normalizedLocale = targetLocale.trim();
1588
+ const context = manualTranslationContext(schema, normalizedLocale, target, property, text);
1589
+ const metadata = context === void 0 ? void 0 : createManualTranslationMetadata?.(context);
1590
+ return executeAction(
1537
1591
  () => headless.setLocaleTranslation(
1538
- context.locale,
1592
+ normalizedLocale,
1539
1593
  target,
1540
- context.property,
1541
- context.translatedText,
1594
+ property,
1595
+ text,
1542
1596
  metadata === void 0 ? void 0 : { metadata }
1543
1597
  ),
1544
1598
  {
1545
- action: "setLocaleTranslation",
1546
- targetId: context.nodeId,
1547
- params: { locale: context.locale, kind: context.kind, property: context.property }
1599
+ action: "setManualTranslation",
1600
+ ...target.id === void 0 ? {} : { targetId: target.id },
1601
+ params: { locale: normalizedLocale, kind: target.kind, property }
1548
1602
  }
1549
1603
  );
1550
1604
  };
1605
+ const updateManualTranslation = (context) => {
1606
+ const target = context.kind === "form" ? { kind: "form" } : { kind: context.kind, id: context.nodeId };
1607
+ setManualTranslation(context.locale, target, context.property, context.translatedText);
1608
+ };
1551
1609
  const updateFormTranslation = (property, translatedText) => {
1552
1610
  if (editingLocale.length === 0) return;
1553
1611
  updateManualTranslation({
@@ -1656,6 +1714,7 @@ function FormBuilder({
1656
1714
  ...target.id === void 0 ? {} : { targetId: target.id },
1657
1715
  params: { locale: targetLocale, property }
1658
1716
  }),
1717
+ setManualTranslation,
1659
1718
  addLocale: (targetLocale) => executeAction(() => headless.addLocale(targetLocale), {
1660
1719
  action: "addLocale",
1661
1720
  params: { locale: targetLocale }
@@ -3051,6 +3110,8 @@ function ContextFormRenderer({
3051
3110
  autoSaveKey,
3052
3111
  beforeSubmit,
3053
3112
  onDraftSave,
3113
+ successRenderMode = "append",
3114
+ hideFormOnSuccess = false,
3054
3115
  submissionGuards = [],
3055
3116
  receiptStore,
3056
3117
  attemptStore,
@@ -3066,9 +3127,11 @@ function ContextFormRenderer({
3066
3127
  const [focusFieldId, setFocusFieldId] = useState4(null);
3067
3128
  const [confirmation, setConfirmation] = useState4(null);
3068
3129
  const [guardMessage, setGuardMessage] = useState4(null);
3130
+ const [guardsPending, setGuardsPending] = useState4(false);
3069
3131
  const [receipt, setReceipt] = useState4(null);
3070
3132
  const [receiptLoaded, setReceiptLoaded] = useState4(receiptStore === void 0);
3071
3133
  const rendererSubmissionInFlight = useRef2(false);
3134
+ const completionRef = useRef2(null);
3072
3135
  const pages = form.schema.pages;
3073
3136
  const visiblePageIndexes = useMemo4(
3074
3137
  () => pages === void 0 ? [] : pages.flatMap((page, index) => form.pageVisibility[page.id] === true ? [index] : []),
@@ -3078,8 +3141,9 @@ function ContextFormRenderer({
3078
3141
  const activeVisibleIndex = visiblePageIndexes.indexOf(currentPageIndex);
3079
3142
  const fieldIds = activePage === void 0 ? void 0 : new Set(activePage.questionIds);
3080
3143
  const visibleValues = useMemo4(() => selectVisibleAnswers2(form.schema, form.values), [form.schema, form.values]);
3081
- const submitState = confirmation === null ? form.submitStatus : "confirming";
3144
+ const submitState = confirmation === null && !guardsPending ? form.submitStatus : "confirming";
3082
3145
  const interactionLocked = submitState === "confirming" || submitState === "submitting";
3146
+ const isReplaceMode = successRenderMode === "replace" || hideFormOnSuccess;
3083
3147
  useEffect3(() => {
3084
3148
  let active = true;
3085
3149
  if (receiptStore === void 0) {
@@ -3119,6 +3183,10 @@ function ContextFormRenderer({
3119
3183
  setFocusFieldId(null);
3120
3184
  }
3121
3185
  }, [focusFieldId]);
3186
+ useEffect3(() => {
3187
+ if (!isReplaceMode || form.submitStatus !== "success") return;
3188
+ completionRef.current?.focus();
3189
+ }, [form.submitStatus, isReplaceMode]);
3122
3190
  useEffect3(() => {
3123
3191
  if (autoSaveKey === void 0 || typeof globalThis.localStorage === "undefined") return;
3124
3192
  const loadIdentity = `${autoSaveKey}:${form.schema.id}:${form.schema.version}`;
@@ -3184,13 +3252,14 @@ function ContextFormRenderer({
3184
3252
  };
3185
3253
  };
3186
3254
  const submitValues = async (guardsConfirmed = false) => {
3187
- if (rendererSubmissionInFlight.current || confirmation !== null && !guardsConfirmed) {
3255
+ if (rendererSubmissionInFlight.current || submitState === "submitting" || submitState === "success" || confirmation !== null && !guardsConfirmed) {
3188
3256
  return { status: "cancelled" };
3189
3257
  }
3190
3258
  const validation = validateAnswers2(form.schema, form.values);
3191
3259
  const firstInvalidFieldId = validation.issues[0]?.fieldId;
3192
3260
  if (validation.valid && !guardsConfirmed && submissionGuards.length > 0) {
3193
3261
  rendererSubmissionInFlight.current = true;
3262
+ setGuardsPending(true);
3194
3263
  try {
3195
3264
  const guardResult = await runSubmissionGuards(submissionGuards);
3196
3265
  if (guardResult.status === "block") {
@@ -3207,6 +3276,7 @@ function ContextFormRenderer({
3207
3276
  }
3208
3277
  } finally {
3209
3278
  rendererSubmissionInFlight.current = false;
3279
+ setGuardsPending(false);
3210
3280
  }
3211
3281
  }
3212
3282
  setGuardMessage(null);
@@ -3244,7 +3314,6 @@ function ContextFormRenderer({
3244
3314
  };
3245
3315
  try {
3246
3316
  await receiptStore.save(storedReceipt);
3247
- setReceipt(storedReceipt);
3248
3317
  } catch (cause) {
3249
3318
  const error = cause instanceof Error ? cause : new Error(String(cause));
3250
3319
  try {
@@ -3271,7 +3340,7 @@ function ContextFormRenderer({
3271
3340
  };
3272
3341
  const handleSubmit = (event) => {
3273
3342
  event.preventDefault();
3274
- if (interactionLocked) return;
3343
+ if (submitState === "submitting" || submitState === "confirming" || submitState === "success") return;
3275
3344
  void submitValues();
3276
3345
  };
3277
3346
  const confirmSubmission = () => {
@@ -3290,7 +3359,17 @@ function ContextFormRenderer({
3290
3359
  const validationIssues = Object.values(form.errors).filter((issue) => issue !== void 0);
3291
3360
  const canPrev = pages !== void 0 && activeVisibleIndex > 0;
3292
3361
  const canNext = pages !== void 0 && activeVisibleIndex < visiblePageIndexes.length - 1;
3293
- const renderSubmitButton = () => slots.renderSubmitButton?.({ isSubmitting: interactionLocked, onSubmit: () => void submitValues() }) ?? /* @__PURE__ */ jsx3("button", { className: "fe-submit", type: "submit", disabled: interactionLocked, children: form.translate(form.schema.submitLabelKey ?? "form.submit") });
3362
+ const renderSubmitButton = () => {
3363
+ const submitButtonProps = {
3364
+ isSubmitting: submitState === "submitting",
3365
+ submitStatus: submitState,
3366
+ disabled: interactionLocked || submitState === "success",
3367
+ onSubmit: () => void submitValues()
3368
+ };
3369
+ return slots.renderSubmitButton?.(submitButtonProps) ?? /* @__PURE__ */ jsx3("button", { className: "fe-submit", type: "submit", disabled: submitButtonProps.disabled, children: form.translate(form.schema.submitLabelKey ?? "form.submit") });
3370
+ };
3371
+ const completionMessage = form.schema.completionMessage ?? (successMessageKey === void 0 ? "Submitted." : form.translate(successMessageKey));
3372
+ const completionRegion = /* @__PURE__ */ jsx3("div", { ref: completionRef, className: "fe-completion", role: "status", "aria-live": "polite", tabIndex: -1, children: slots.renderCompletion?.({ message: completionMessage }) ?? /* @__PURE__ */ jsx3("div", { children: completionMessage }) });
3294
3373
  if (!receiptLoaded) return null;
3295
3374
  if (receipt !== null) {
3296
3375
  return /* @__PURE__ */ jsx3("div", { className: `fe-form fe-already-submitted ${className}`.trim(), children: slots.renderAlreadySubmitted?.({
@@ -3301,6 +3380,9 @@ function ContextFormRenderer({
3301
3380
  receiptStore === void 0 ? null : /* @__PURE__ */ jsx3("button", { type: "button", onClick: () => void resetReceipt(), children: form.translate("form.submitAnother") })
3302
3381
  ] }) });
3303
3382
  }
3383
+ if (form.submitStatus === "success" && isReplaceMode) {
3384
+ return /* @__PURE__ */ jsx3("div", { className: `fe-form ${className}`.trim(), children: completionRegion });
3385
+ }
3304
3386
  return /* @__PURE__ */ jsxs2("form", { ref: formRef, className: `fe-form ${className}`.trim(), noValidate: true, onSubmit: handleSubmit, children: [
3305
3387
  slots.renderHeader?.({
3306
3388
  title: form.schema.title,
@@ -3419,9 +3501,7 @@ function ContextFormRenderer({
3419
3501
  canNext ? null : renderSubmitButton()
3420
3502
  ] }),
3421
3503
  /* @__PURE__ */ jsxs2("div", { className: "fe-status", "aria-live": "polite", children: [
3422
- form.submitStatus === "success" ? slots.renderCompletion?.({
3423
- message: form.schema.completionMessage ?? (successMessageKey === void 0 ? "Submitted." : form.translate(successMessageKey))
3424
- }) ?? /* @__PURE__ */ jsx3("div", { role: "status", children: form.schema.completionMessage ?? (successMessageKey === void 0 ? "Submitted." : form.translate(successMessageKey)) }) : null,
3504
+ form.submitStatus === "success" ? completionRegion : null,
3425
3505
  form.submitStatus === "error" && form.submitError !== null ? slots.renderSubmitError?.({ error: form.submitError, onRetry: () => void submitValues() }) ?? (errorMessageKey === void 0 ? null : /* @__PURE__ */ jsx3("div", { role: "alert", children: form.translate(errorMessageKey) })) : null
3426
3506
  ] })
3427
3507
  ] });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@form-engine-ts/react",
3
- "version": "2.9.4",
3
+ "version": "2.9.5",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -42,8 +42,8 @@
42
42
  "typescript"
43
43
  ],
44
44
  "dependencies": {
45
- "@form-engine-ts/core": "2.9.4",
46
- "@form-engine-ts/privacy": "2.9.4"
45
+ "@form-engine-ts/core": "2.9.5",
46
+ "@form-engine-ts/privacy": "2.9.5"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "react": ">=18.2 <20",