@form-engine-ts/react 2.9.5 → 2.9.6

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
@@ -152,3 +152,11 @@ preserved. Pass an SSR-safe `createLocalStorageSubmissionAttemptStore()` as `att
152
152
  before submission. Renderer injects it as `attemptId` and `submissionId`, retains it after a failed request, promotes it
153
153
  to the receipt after success, and then clears the attempt. Custom receipt stores may omit `getBatch`; the hook falls back
154
154
  to concurrent `get` calls.
155
+
156
+ After success, completion rendering receives a snapshot of `answers`, `schema`, the optional response, and
157
+ `submittedItems`. Each summary item includes the field title, raw value, formatted display value, visibility, and field
158
+ metadata. Use `renderSubmittedValues` for a typed summary slot; hidden fields are omitted by default and can be included
159
+ with `showHiddenFieldsInSummary`. Server validation can be returned by throwing `FormSubmissionError` with `fieldErrors`
160
+ and `formError`; field messages are mapped back to the form and the first invalid control is focused. Use
161
+ `submissionConfirmationRenderMode="replace"` or `"dialog"` for alternate confirmation presentations, and
162
+ `fieldsClassName` or `renderFields` to control the fields wrapper.
package/dist/index.cjs CHANGED
@@ -23,6 +23,7 @@ __export(index_exports, {
23
23
  FormBuilder: () => FormBuilder,
24
24
  FormProvider: () => FormProvider,
25
25
  FormRenderer: () => FormRenderer,
26
+ FormSubmissionError: () => FormSubmissionError,
26
27
  createLocalStorageSubmissionAttemptStore: () => createLocalStorageSubmissionAttemptStore,
27
28
  createLocalStorageSubmissionReceiptStore: () => createLocalStorageSubmissionReceiptStore,
28
29
  resolveInitialFieldType: () => resolveInitialFieldType,
@@ -2657,6 +2658,17 @@ function FormProvider({
2657
2658
  },
2658
2659
  [validSchema, validationPageIndex]
2659
2660
  );
2661
+ const setServerErrors = (0, import_react3.useCallback)((fieldErrors) => {
2662
+ setErrors(
2663
+ Object.fromEntries(
2664
+ Object.entries(fieldErrors).map(([fieldId, message]) => [
2665
+ fieldId,
2666
+ { fieldId, code: "invalid_type", messageKey: message, params: {} }
2667
+ ])
2668
+ )
2669
+ );
2670
+ setValidationPageIndex(null);
2671
+ }, []);
2660
2672
  const restoreValues = (0, import_react3.useCallback)(
2661
2673
  (restoredValues) => {
2662
2674
  const fieldIds = new Set(validSchema.fields.map((field) => field.id));
@@ -2741,6 +2753,7 @@ function FormProvider({
2741
2753
  submitError,
2742
2754
  isSubmitting: submitStatus === "submitting",
2743
2755
  setValue,
2756
+ setServerErrors,
2744
2757
  restoreValues,
2745
2758
  validatePage,
2746
2759
  reset,
@@ -2754,6 +2767,7 @@ function FormProvider({
2754
2767
  reset,
2755
2768
  restoreValues,
2756
2769
  setValue,
2770
+ setServerErrors,
2757
2771
  submit,
2758
2772
  submitError,
2759
2773
  submitStatus,
@@ -2900,6 +2914,18 @@ function useSubmissionReceipts(store, queries) {
2900
2914
  // src/renderer.tsx
2901
2915
  var import_core4 = require("@form-engine-ts/core");
2902
2916
  var import_react5 = require("react");
2917
+
2918
+ // src/types.ts
2919
+ var FormSubmissionError = class extends Error {
2920
+ payload;
2921
+ constructor(message, payload) {
2922
+ super(message);
2923
+ this.name = "FormSubmissionError";
2924
+ this.payload = payload ?? { formError: message };
2925
+ }
2926
+ };
2927
+
2928
+ // src/renderer.tsx
2903
2929
  var import_jsx_runtime3 = require("react/jsx-runtime");
2904
2930
  function describedBy(field, error, helpId, errorId) {
2905
2931
  const ids = [field.description === void 0 ? void 0 : helpId, error === void 0 ? void 0 : errorId].filter(
@@ -3096,6 +3122,30 @@ function isRecord(value) {
3096
3122
  function isFormValue(value) {
3097
3123
  return value === void 0 || typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value) || Array.isArray(value) && value.every((item) => typeof item === "string");
3098
3124
  }
3125
+ function displaySubmittedValue(field, value, translate) {
3126
+ if (value === void 0 || value === null) return "";
3127
+ if (field.type === "checkbox") return value === true ? translate("form.yes") : translate("form.no");
3128
+ if (field.type === "multi-select" && Array.isArray(value)) {
3129
+ const labels = new Map(field.options.map((option) => [option.id, option.label]));
3130
+ return value.map((item) => labels.get(item) ?? item).join(", ");
3131
+ }
3132
+ if ((field.type === "radio" || field.type === "select") && typeof value === "string") {
3133
+ return field.options.find((option) => option.id === value)?.label ?? value;
3134
+ }
3135
+ if (Array.isArray(value)) return value.join(", ");
3136
+ return String(value);
3137
+ }
3138
+ function buildSubmittedItems(schema, answers, visibility, translate, showHiddenFields) {
3139
+ return schema.fields.filter((field) => showHiddenFields || visibility[field.id] === true).map((field) => ({
3140
+ fieldId: field.id,
3141
+ title: field.title,
3142
+ type: field.type,
3143
+ rawValue: answers[field.id],
3144
+ displayValue: displaySubmittedValue(field, answers[field.id], translate),
3145
+ visible: visibility[field.id] === true,
3146
+ ...field.metadata === void 0 ? {} : { metadata: field.metadata }
3147
+ }));
3148
+ }
3099
3149
  function parseDraft(serialized) {
3100
3150
  try {
3101
3151
  const value = JSON.parse(serialized);
@@ -3123,6 +3173,9 @@ function ContextFormRenderer({
3123
3173
  beforeSubmit,
3124
3174
  onDraftSave,
3125
3175
  successRenderMode = "append",
3176
+ submissionConfirmationRenderMode = "inline",
3177
+ showHiddenFieldsInSummary = false,
3178
+ fieldsClassName,
3126
3179
  hideFormOnSuccess = false,
3127
3180
  submissionGuards = [],
3128
3181
  receiptStore,
@@ -3141,9 +3194,11 @@ function ContextFormRenderer({
3141
3194
  const [guardMessage, setGuardMessage] = (0, import_react5.useState)(null);
3142
3195
  const [guardsPending, setGuardsPending] = (0, import_react5.useState)(false);
3143
3196
  const [receipt, setReceipt] = (0, import_react5.useState)(null);
3197
+ const [completionData, setCompletionData] = (0, import_react5.useState)(null);
3144
3198
  const [receiptLoaded, setReceiptLoaded] = (0, import_react5.useState)(receiptStore === void 0);
3145
3199
  const rendererSubmissionInFlight = (0, import_react5.useRef)(false);
3146
3200
  const completionRef = (0, import_react5.useRef)(null);
3201
+ const confirmationRef = (0, import_react5.useRef)(null);
3147
3202
  const pages = form.schema.pages;
3148
3203
  const visiblePageIndexes = (0, import_react5.useMemo)(
3149
3204
  () => pages === void 0 ? [] : pages.flatMap((page, index) => form.pageVisibility[page.id] === true ? [index] : []),
@@ -3156,6 +3211,10 @@ function ContextFormRenderer({
3156
3211
  const submitState = confirmation === null && !guardsPending ? form.submitStatus : "confirming";
3157
3212
  const interactionLocked = submitState === "confirming" || submitState === "submitting";
3158
3213
  const isReplaceMode = successRenderMode === "replace" || hideFormOnSuccess;
3214
+ const focusSubmitButton = (0, import_react5.useCallback)(() => {
3215
+ const button = formRef.current?.querySelector(".fe-submit, button[type='submit'], button");
3216
+ button?.focus();
3217
+ }, []);
3159
3218
  (0, import_react5.useEffect)(() => {
3160
3219
  let active = true;
3161
3220
  if (receiptStore === void 0) {
@@ -3199,6 +3258,21 @@ function ContextFormRenderer({
3199
3258
  if (!isReplaceMode || form.submitStatus !== "success") return;
3200
3259
  completionRef.current?.focus();
3201
3260
  }, [form.submitStatus, isReplaceMode]);
3261
+ (0, import_react5.useEffect)(() => {
3262
+ if (confirmation === null) return;
3263
+ const confirmButton = confirmationRef.current?.querySelector("[data-fe-confirm], button");
3264
+ confirmButton?.focus();
3265
+ if (submissionConfirmationRenderMode !== "dialog") return;
3266
+ const onKeyDown = (event) => {
3267
+ if (event.key === "Escape") {
3268
+ event.preventDefault();
3269
+ setConfirmation(null);
3270
+ globalThis.setTimeout(focusSubmitButton, 0);
3271
+ }
3272
+ };
3273
+ globalThis.addEventListener("keydown", onKeyDown);
3274
+ return () => globalThis.removeEventListener("keydown", onKeyDown);
3275
+ }, [confirmation, focusSubmitButton, submissionConfirmationRenderMode]);
3202
3276
  (0, import_react5.useEffect)(() => {
3203
3277
  if (autoSaveKey === void 0 || typeof globalThis.localStorage === "undefined") return;
3204
3278
  const loadIdentity = `${autoSaveKey}:${form.schema.id}:${form.schema.version}`;
@@ -3314,7 +3388,33 @@ function ContextFormRenderer({
3314
3388
  focusFirstIssue(firstInvalidFieldId);
3315
3389
  return result;
3316
3390
  }
3391
+ if (result.status === "error") {
3392
+ if (result.error instanceof FormSubmissionError) {
3393
+ const fieldErrors = result.error.payload.fieldErrors ?? {};
3394
+ form.setServerErrors?.(fieldErrors);
3395
+ const firstServerFieldId = Object.keys(fieldErrors)[0];
3396
+ if (firstServerFieldId !== void 0) {
3397
+ const invalidPageIndex = pages?.findIndex((page) => page.questionIds.includes(firstServerFieldId));
3398
+ if (invalidPageIndex !== void 0 && invalidPageIndex >= 0) setCurrentPageIndex(invalidPageIndex);
3399
+ focusFirstIssue(firstServerFieldId);
3400
+ }
3401
+ }
3402
+ return result;
3403
+ }
3317
3404
  if (result.status !== "success") return result;
3405
+ const submittedAnswers = { ...form.values };
3406
+ const submittedItems = buildSubmittedItems(
3407
+ form.schema,
3408
+ submittedAnswers,
3409
+ form.visibility,
3410
+ (key) => form.translate(key),
3411
+ showHiddenFieldsInSummary
3412
+ );
3413
+ setCompletionData({
3414
+ answers: submittedAnswers,
3415
+ submittedItems,
3416
+ ...result.response === void 0 ? {} : { response: result.response }
3417
+ });
3318
3418
  if (receiptStore !== void 0) {
3319
3419
  const response = result.response;
3320
3420
  const submissionId = response?.submissionId ?? submissionAttempt?.attemptId;
@@ -3361,6 +3461,7 @@ function ContextFormRenderer({
3361
3461
  };
3362
3462
  const cancelSubmission = () => {
3363
3463
  setConfirmation(null);
3464
+ globalThis.setTimeout(focusSubmitButton, 0);
3364
3465
  };
3365
3466
  const resetReceipt = async () => {
3366
3467
  if (receiptStore === void 0) return;
@@ -3381,7 +3482,48 @@ function ContextFormRenderer({
3381
3482
  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
3483
  };
3383
3484
  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 }) });
3485
+ const activeCompletionData = completionData ?? {
3486
+ answers: { ...form.values },
3487
+ submittedItems: buildSubmittedItems(
3488
+ form.schema,
3489
+ form.values,
3490
+ form.visibility,
3491
+ (key) => form.translate(key),
3492
+ showHiddenFieldsInSummary
3493
+ )
3494
+ };
3495
+ const completionProps = {
3496
+ message: completionMessage,
3497
+ schema: form.schema,
3498
+ answers: activeCompletionData.answers,
3499
+ submittedItems: activeCompletionData.submittedItems,
3500
+ ...activeCompletionData.response === void 0 ? {} : { response: activeCompletionData.response },
3501
+ onReset: form.reset
3502
+ };
3503
+ const completionRegion = /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { ref: completionRef, className: "fe-completion", role: "status", "aria-live": "polite", tabIndex: -1, children: [
3504
+ slots.renderCompletion?.(completionProps) ?? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { children: completionMessage }),
3505
+ slots.renderSubmittedValues?.({ items: activeCompletionData.submittedItems, schema: form.schema })
3506
+ ] });
3507
+ const confirmationContent = /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3508
+ "div",
3509
+ {
3510
+ ref: confirmationRef,
3511
+ className: "fe-submission-confirmation",
3512
+ role: submissionConfirmationRenderMode === "dialog" ? void 0 : "dialog",
3513
+ children: slots.renderSubmissionConfirmation?.({
3514
+ findings: confirmation?.findings ?? [],
3515
+ message: confirmation?.message ?? form.translate("form.confirmSensitiveData"),
3516
+ schema: form.schema,
3517
+ visibleValues,
3518
+ onConfirm: confirmSubmission,
3519
+ onCancel: cancelSubmission
3520
+ }) ?? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
3521
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { children: confirmation?.message ?? form.translate("form.confirmSensitiveData") }),
3522
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", "data-fe-confirm": "true", onClick: confirmSubmission, children: form.translate("form.confirmSubmission") }),
3523
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", onClick: cancelSubmission, children: form.translate("form.cancelSubmission") })
3524
+ ] })
3525
+ }
3526
+ );
3385
3527
  if (!receiptLoaded) return null;
3386
3528
  if (receipt !== null) {
3387
3529
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: `fe-form fe-already-submitted ${className}`.trim(), children: slots.renderAlreadySubmitted?.({
@@ -3395,127 +3537,136 @@ function ContextFormRenderer({
3395
3537
  if (form.submitStatus === "success" && isReplaceMode) {
3396
3538
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: `fe-form ${className}`.trim(), children: completionRegion });
3397
3539
  }
3398
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("form", { ref: formRef, className: `fe-form ${className}`.trim(), noValidate: true, onSubmit: handleSubmit, children: [
3399
- slots.renderHeader?.({
3400
- title: form.schema.title,
3401
- ...form.schema.description === void 0 ? {} : { description: form.schema.description }
3402
- }) ?? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("header", { className: "fe-header", children: [
3403
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h1", { children: form.schema.title }),
3404
- form.schema.description === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { children: form.schema.description }),
3405
- pages === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "fe-progress", children: [
3406
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3407
- "div",
3408
- {
3409
- className: "form-progress-bar",
3410
- role: "progressbar",
3411
- "aria-valuemin": 1,
3412
- "aria-valuemax": visiblePageIndexes.length,
3413
- "aria-valuenow": activeVisibleIndex + 1,
3414
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3415
- "div",
3416
- {
3417
- className: "form-progress-fill",
3418
- style: { width: `${(activeVisibleIndex + 1) / visiblePageIndexes.length * 100}%` }
3540
+ if (confirmation !== null && submissionConfirmationRenderMode === "replace") {
3541
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: `fe-form ${className}`.trim(), children: confirmationContent });
3542
+ }
3543
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
3544
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3545
+ "form",
3546
+ {
3547
+ ref: formRef,
3548
+ className: `fe-form ${className}`.trim(),
3549
+ noValidate: true,
3550
+ onSubmit: handleSubmit,
3551
+ "aria-hidden": confirmation !== null && submissionConfirmationRenderMode === "dialog" ? true : void 0,
3552
+ children: [
3553
+ slots.renderHeader?.({
3554
+ title: form.schema.title,
3555
+ ...form.schema.description === void 0 ? {} : { description: form.schema.description }
3556
+ }) ?? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("header", { className: "fe-header", children: [
3557
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h1", { children: form.schema.title }),
3558
+ form.schema.description === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { children: form.schema.description }),
3559
+ pages === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "fe-progress", children: [
3560
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3561
+ "div",
3562
+ {
3563
+ className: "form-progress-bar",
3564
+ role: "progressbar",
3565
+ "aria-valuemin": 1,
3566
+ "aria-valuemax": visiblePageIndexes.length,
3567
+ "aria-valuenow": activeVisibleIndex + 1,
3568
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3569
+ "div",
3570
+ {
3571
+ className: "form-progress-fill",
3572
+ style: { width: `${(activeVisibleIndex + 1) / visiblePageIndexes.length * 100}%` }
3573
+ }
3574
+ )
3575
+ }
3576
+ ),
3577
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: form.translate("form.step", { current: activeVisibleIndex + 1, total: visiblePageIndexes.length }) })
3578
+ ] }),
3579
+ draftRestored ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "form-draft-badge", children: form.translate("form.draftRestored") }) : null
3580
+ ] }),
3581
+ activePage === void 0 ? null : slots.renderPageHeader?.({
3582
+ page: activePage,
3583
+ pageIndex: activeVisibleIndex,
3584
+ totalPages: visiblePageIndexes.length
3585
+ }) ?? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "fe-page-header", children: [
3586
+ activePage.title === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h2", { className: "fe-page-title", children: activePage.title }),
3587
+ activePage.description === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "fe-page-description", children: activePage.description })
3588
+ ] }),
3589
+ (() => {
3590
+ const fieldChildren = form.schema.fields.filter((field) => form.visibility[field.id] === true && (fieldIds === void 0 || fieldIds.has(field.id))).map((field) => {
3591
+ const error = form.errors[field.id];
3592
+ const props = {
3593
+ field,
3594
+ value: form.values[field.id],
3595
+ error,
3596
+ setValue: (value) => form.setValue(field.id, value),
3597
+ translate: form.translate,
3598
+ inputId: `${prefix}-${field.id}`,
3599
+ errorId: `${prefix}-${field.id}-error`,
3600
+ helpId: `${prefix}-${field.id}-help`,
3601
+ ...slots.renderCharacterCount === void 0 ? {} : { renderCharacterCount: slots.renderCharacterCount }
3602
+ };
3603
+ if (slots.renderField !== void 0) {
3604
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react5.Fragment, { children: slots.renderField({
3605
+ question: field,
3606
+ value: form.values[field.id],
3607
+ onChange: (value) => {
3608
+ if (isFormValue(value)) form.setValue(field.id, value);
3609
+ },
3610
+ ...error === void 0 ? {} : { error }
3611
+ }) }, field.id);
3419
3612
  }
3420
- )
3421
- }
3422
- ),
3423
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: form.translate("form.step", { current: activeVisibleIndex + 1, total: visiblePageIndexes.length }) })
3424
- ] }),
3425
- draftRestored ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "form-draft-badge", children: form.translate("form.draftRestored") }) : null
3426
- ] }),
3427
- activePage === void 0 ? null : slots.renderPageHeader?.({
3428
- page: activePage,
3429
- pageIndex: activeVisibleIndex,
3430
- totalPages: visiblePageIndexes.length
3431
- }) ?? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "fe-page-header", children: [
3432
- activePage.title === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h2", { className: "fe-page-title", children: activePage.title }),
3433
- activePage.description === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "fe-page-description", children: activePage.description })
3434
- ] }),
3435
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "fe-fields", children: form.schema.fields.filter((field) => form.visibility[field.id] === true && (fieldIds === void 0 || fieldIds.has(field.id))).map((field) => {
3436
- const error = form.errors[field.id];
3437
- const props = {
3438
- field,
3439
- value: form.values[field.id],
3440
- error,
3441
- setValue: (value) => form.setValue(field.id, value),
3442
- translate: form.translate,
3443
- inputId: `${prefix}-${field.id}`,
3444
- errorId: `${prefix}-${field.id}-error`,
3445
- helpId: `${prefix}-${field.id}-help`,
3446
- ...slots.renderCharacterCount === void 0 ? {} : { renderCharacterCount: slots.renderCharacterCount }
3447
- };
3448
- if (slots.renderField !== void 0) {
3449
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react5.Fragment, { children: slots.renderField({
3450
- question: field,
3451
- value: form.values[field.id],
3452
- onChange: (value) => {
3453
- if (isFormValue(value)) form.setValue(field.id, value);
3454
- },
3455
- ...error === void 0 ? {} : { error }
3456
- }) }, field.id);
3613
+ const Component = components[field.type];
3614
+ return Component === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(DefaultField, { ...props }, field.id) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Component, { ...props }, field.id);
3615
+ });
3616
+ const fieldClassName = `fe-fields${fieldsClassName === void 0 ? "" : ` ${fieldsClassName}`}`;
3617
+ return slots.renderFields?.({ children: fieldChildren, className: fieldClassName }) ?? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: fieldClassName, children: fieldChildren });
3618
+ })(),
3619
+ guardMessage === null ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { role: "alert", children: guardMessage }),
3620
+ confirmation !== null && submissionConfirmationRenderMode === "inline" ? confirmationContent : null,
3621
+ validationIssues.length === 0 ? null : slots.renderValidationSummary?.({ issues: validationIssues }) ?? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "fe-validation-summary", role: "alert", children: [
3622
+ validationIssues.length,
3623
+ " validation error",
3624
+ validationIssues.length === 1 ? "" : "s",
3625
+ "."
3626
+ ] }),
3627
+ pages === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
3628
+ slots.renderNavigation?.({
3629
+ currentPage: 0,
3630
+ totalPages: 1,
3631
+ canPrev: false,
3632
+ canNext: false,
3633
+ onPrev: () => void 0,
3634
+ onNext: () => void 0
3635
+ }),
3636
+ renderSubmitButton()
3637
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "form-step-navigation", children: [
3638
+ slots.renderNavigation?.({
3639
+ currentPage: activeVisibleIndex,
3640
+ totalPages: visiblePageIndexes.length,
3641
+ canPrev,
3642
+ canNext,
3643
+ onPrev: () => {
3644
+ if (!interactionLocked) setCurrentPageIndex(visiblePageIndexes[activeVisibleIndex - 1] ?? 0);
3645
+ },
3646
+ onNext: handleNext
3647
+ }) ?? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
3648
+ canPrev ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3649
+ "button",
3650
+ {
3651
+ className: "btn-prev",
3652
+ type: "button",
3653
+ disabled: interactionLocked,
3654
+ onClick: () => setCurrentPageIndex(visiblePageIndexes[activeVisibleIndex - 1] ?? 0),
3655
+ children: form.translate("form.back")
3656
+ }
3657
+ ) : null,
3658
+ canNext ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { className: "btn-next", type: "button", disabled: interactionLocked, onClick: handleNext, children: form.translate("form.next") }) : null
3659
+ ] }),
3660
+ canNext ? null : renderSubmitButton()
3661
+ ] }),
3662
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "fe-status", "aria-live": "polite", children: [
3663
+ form.submitStatus === "success" ? completionRegion : null,
3664
+ form.submitStatus === "error" && form.submitError !== null ? slots.renderSubmitError?.({ error: form.submitError, onRetry: () => void submitValues() }) ?? (form.submitError instanceof FormSubmissionError && form.submitError.payload.formError !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { role: "alert", children: form.submitError.payload.formError }) : errorMessageKey === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { role: "alert", children: form.translate(errorMessageKey) })) : null
3665
+ ] })
3666
+ ]
3457
3667
  }
3458
- const Component = components[field.type];
3459
- return Component === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(DefaultField, { ...props }, field.id) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Component, { ...props }, field.id);
3460
- }) }),
3461
- guardMessage === null ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { role: "alert", children: guardMessage }),
3462
- confirmation === null ? null : slots.renderSubmissionConfirmation?.({
3463
- findings: confirmation.findings,
3464
- message: confirmation.message ?? form.translate("form.confirmSensitiveData"),
3465
- schema: form.schema,
3466
- visibleValues,
3467
- onConfirm: confirmSubmission,
3468
- onCancel: cancelSubmission
3469
- }) ?? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "fe-submission-confirmation", role: "dialog", "aria-modal": "true", children: [
3470
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { children: confirmation.message ?? form.translate("form.confirmSensitiveData") }),
3471
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", onClick: confirmSubmission, children: form.translate("form.confirmSubmission") }),
3472
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", onClick: cancelSubmission, children: form.translate("form.cancelSubmission") })
3473
- ] }),
3474
- validationIssues.length === 0 ? null : slots.renderValidationSummary?.({ issues: validationIssues }) ?? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "fe-validation-summary", role: "alert", children: [
3475
- validationIssues.length,
3476
- " validation error",
3477
- validationIssues.length === 1 ? "" : "s",
3478
- "."
3479
- ] }),
3480
- pages === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
3481
- slots.renderNavigation?.({
3482
- currentPage: 0,
3483
- totalPages: 1,
3484
- canPrev: false,
3485
- canNext: false,
3486
- onPrev: () => void 0,
3487
- onNext: () => void 0
3488
- }),
3489
- renderSubmitButton()
3490
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "form-step-navigation", children: [
3491
- slots.renderNavigation?.({
3492
- currentPage: activeVisibleIndex,
3493
- totalPages: visiblePageIndexes.length,
3494
- canPrev,
3495
- canNext,
3496
- onPrev: () => {
3497
- if (!interactionLocked) setCurrentPageIndex(visiblePageIndexes[activeVisibleIndex - 1] ?? 0);
3498
- },
3499
- onNext: handleNext
3500
- }) ?? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
3501
- canPrev ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3502
- "button",
3503
- {
3504
- className: "btn-prev",
3505
- type: "button",
3506
- disabled: interactionLocked,
3507
- onClick: () => setCurrentPageIndex(visiblePageIndexes[activeVisibleIndex - 1] ?? 0),
3508
- children: form.translate("form.back")
3509
- }
3510
- ) : null,
3511
- canNext ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { className: "btn-next", type: "button", disabled: interactionLocked, onClick: handleNext, children: form.translate("form.next") }) : null
3512
- ] }),
3513
- canNext ? null : renderSubmitButton()
3514
- ] }),
3515
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "fe-status", "aria-live": "polite", children: [
3516
- form.submitStatus === "success" ? completionRegion : null,
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
3518
- ] })
3668
+ ),
3669
+ confirmation !== null && submissionConfirmationRenderMode === "dialog" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "fe-confirmation-dialog-backdrop", role: "dialog", "aria-modal": "true", children: confirmationContent }) : null
3519
3670
  ] });
3520
3671
  }
3521
3672
  var RENDERER_MESSAGES = {
@@ -3528,6 +3679,8 @@ var RENDERER_MESSAGES = {
3528
3679
  "form.confirmSensitiveData": "Sensitive data may be included. Confirm before submitting.",
3529
3680
  "form.confirmSubmission": "Confirm submission",
3530
3681
  "form.cancelSubmission": "Cancel",
3682
+ "form.yes": "Yes",
3683
+ "form.no": "No",
3531
3684
  "form.alreadySubmitted": "Already submitted.",
3532
3685
  "form.submitAnother": "Submit another response",
3533
3686
  "validation.required": "This field is required."
@@ -3569,6 +3722,7 @@ function FormRenderer(props) {
3569
3722
  FormBuilder,
3570
3723
  FormProvider,
3571
3724
  FormRenderer,
3725
+ FormSubmissionError,
3572
3726
  createLocalStorageSubmissionAttemptStore,
3573
3727
  createLocalStorageSubmissionReceiptStore,
3574
3728
  resolveInitialFieldType,
package/dist/index.d.cts CHANGED
@@ -338,6 +338,31 @@ interface SubmitResponse {
338
338
  readonly submissionId?: string;
339
339
  readonly submittedAt?: string;
340
340
  }
341
+ interface FormSubmittedAnswerItem {
342
+ readonly fieldId: string;
343
+ readonly title: string;
344
+ readonly type: QuestionType;
345
+ readonly rawValue: unknown;
346
+ readonly displayValue: string;
347
+ readonly visible: boolean;
348
+ readonly metadata?: Readonly<Record<string, JsonValue>>;
349
+ }
350
+ interface FormCompletionSlotProps {
351
+ readonly message?: string;
352
+ readonly schema: FormSchema;
353
+ readonly answers: Record<string, unknown>;
354
+ readonly submittedItems: readonly FormSubmittedAnswerItem[];
355
+ readonly response?: SubmitResponse;
356
+ readonly onReset?: () => void;
357
+ }
358
+ interface FormServerErrorPayload {
359
+ readonly fieldErrors?: Readonly<Record<string, string>>;
360
+ readonly formError?: string;
361
+ }
362
+ declare class FormSubmissionError extends Error {
363
+ readonly payload: FormServerErrorPayload;
364
+ constructor(message: string, payload?: FormServerErrorPayload);
365
+ }
341
366
  type FormSubmitHandler = (answers: FormValues) => SubmitResponse | void | Promise<SubmitResponse | undefined> | Promise<void>;
342
367
  type SubmissionGuardResult = {
343
368
  readonly status: "allow";
@@ -352,6 +377,7 @@ type SubmissionGuardResult = {
352
377
  };
353
378
  type SubmissionGuard = (schema: FormSchema, values: Record<string, unknown>) => SubmissionGuardResult | Promise<SubmissionGuardResult>;
354
379
  type FormSuccessRenderMode = "append" | "replace";
380
+ type SubmissionConfirmationRenderMode = "inline" | "replace" | "dialog";
355
381
  type FormSubmitStatus = "idle" | "submitting" | "confirming" | "success" | "error";
356
382
  /** @deprecated Use FormSubmitStatus instead. */
357
383
  type FormSubmitState = "idle" | "submitting" | "confirming" | "success" | "error";
@@ -369,6 +395,10 @@ interface SubmissionConfirmationSlotProps {
369
395
  readonly onConfirm: () => void;
370
396
  readonly onCancel: () => void;
371
397
  }
398
+ interface FormFieldsSlotProps {
399
+ readonly children: ReactNode;
400
+ readonly className?: string;
401
+ }
372
402
  interface FormRendererSlots {
373
403
  readonly renderHeader?: (props: {
374
404
  readonly title: string;
@@ -397,9 +427,15 @@ interface FormRendererSlots {
397
427
  readonly renderValidationSummary?: (props: {
398
428
  readonly issues: readonly ValidationError[];
399
429
  }) => ReactNode;
430
+ /** Additional completion data is supplied at runtime; use renderSubmittedValues for typed summary rendering. */
400
431
  readonly renderCompletion?: (props: {
401
432
  readonly message: string;
402
433
  }) => ReactNode;
434
+ readonly renderSubmittedValues?: (props: {
435
+ readonly items: readonly FormSubmittedAnswerItem[];
436
+ readonly schema: FormSchema;
437
+ }) => ReactNode;
438
+ readonly renderFields?: (props: FormFieldsSlotProps) => ReactNode;
403
439
  readonly renderSubmitError?: (props: {
404
440
  readonly error: Error;
405
441
  readonly onRetry?: () => void;
@@ -467,6 +503,7 @@ interface FormContextValue {
467
503
  readonly submitError: Error | null;
468
504
  readonly isSubmitting: boolean;
469
505
  readonly setValue: (fieldId: string, value: FormValue) => void;
506
+ readonly setServerErrors?: (fieldErrors: Readonly<Record<string, string>>) => void;
470
507
  readonly restoreValues: (values: FormValues) => void;
471
508
  readonly validatePage: (pageIndex: number) => AnswerValidationResult;
472
509
  readonly reset: () => void;
@@ -512,6 +549,9 @@ interface FormRendererPresentationProps extends SubmissionProtectionProps {
512
549
  * Defaults to "append" for backwards compatibility.
513
550
  */
514
551
  readonly successRenderMode?: FormSuccessRenderMode;
552
+ readonly submissionConfirmationRenderMode?: SubmissionConfirmationRenderMode;
553
+ readonly showHiddenFieldsInSummary?: boolean;
554
+ readonly fieldsClassName?: string;
515
555
  /** @deprecated Use successRenderMode="replace" instead. */
516
556
  readonly hideFormOnSuccess?: boolean;
517
557
  readonly successMessageKey?: string;
@@ -532,4 +572,4 @@ interface StandaloneFormRendererProps extends FormRendererPresentationProps {
532
572
  type FormRendererProps = FormRendererPresentationProps | StandaloneFormRendererProps;
533
573
  declare function FormRenderer(props: FormRendererProps): react.JSX.Element;
534
574
 
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 };
575
+ 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 FormCompletionSlotProps, type FormContextValue, type FormFieldsSlotProps, FormProvider, type FormProviderProps, FormRenderer, type FormRendererPresentationProps, type FormRendererProps, type FormRendererSlots, type FormServerErrorPayload, FormSubmissionError, type FormSubmitHandler, type FormSubmitState, type FormSubmitStatus, type FormSubmittedAnswerItem, type FormSuccessRenderMode, type IconButtonProps, type InputComponentProps, type ManualTranslationContext, type ManualTranslationTarget, type RenderSubmitButtonProps, type StandaloneFormRendererProps, type SubmissionAttempt, type SubmissionAttemptStore, type SubmissionConfirmationRenderMode, 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
@@ -338,6 +338,31 @@ interface SubmitResponse {
338
338
  readonly submissionId?: string;
339
339
  readonly submittedAt?: string;
340
340
  }
341
+ interface FormSubmittedAnswerItem {
342
+ readonly fieldId: string;
343
+ readonly title: string;
344
+ readonly type: QuestionType;
345
+ readonly rawValue: unknown;
346
+ readonly displayValue: string;
347
+ readonly visible: boolean;
348
+ readonly metadata?: Readonly<Record<string, JsonValue>>;
349
+ }
350
+ interface FormCompletionSlotProps {
351
+ readonly message?: string;
352
+ readonly schema: FormSchema;
353
+ readonly answers: Record<string, unknown>;
354
+ readonly submittedItems: readonly FormSubmittedAnswerItem[];
355
+ readonly response?: SubmitResponse;
356
+ readonly onReset?: () => void;
357
+ }
358
+ interface FormServerErrorPayload {
359
+ readonly fieldErrors?: Readonly<Record<string, string>>;
360
+ readonly formError?: string;
361
+ }
362
+ declare class FormSubmissionError extends Error {
363
+ readonly payload: FormServerErrorPayload;
364
+ constructor(message: string, payload?: FormServerErrorPayload);
365
+ }
341
366
  type FormSubmitHandler = (answers: FormValues) => SubmitResponse | void | Promise<SubmitResponse | undefined> | Promise<void>;
342
367
  type SubmissionGuardResult = {
343
368
  readonly status: "allow";
@@ -352,6 +377,7 @@ type SubmissionGuardResult = {
352
377
  };
353
378
  type SubmissionGuard = (schema: FormSchema, values: Record<string, unknown>) => SubmissionGuardResult | Promise<SubmissionGuardResult>;
354
379
  type FormSuccessRenderMode = "append" | "replace";
380
+ type SubmissionConfirmationRenderMode = "inline" | "replace" | "dialog";
355
381
  type FormSubmitStatus = "idle" | "submitting" | "confirming" | "success" | "error";
356
382
  /** @deprecated Use FormSubmitStatus instead. */
357
383
  type FormSubmitState = "idle" | "submitting" | "confirming" | "success" | "error";
@@ -369,6 +395,10 @@ interface SubmissionConfirmationSlotProps {
369
395
  readonly onConfirm: () => void;
370
396
  readonly onCancel: () => void;
371
397
  }
398
+ interface FormFieldsSlotProps {
399
+ readonly children: ReactNode;
400
+ readonly className?: string;
401
+ }
372
402
  interface FormRendererSlots {
373
403
  readonly renderHeader?: (props: {
374
404
  readonly title: string;
@@ -397,9 +427,15 @@ interface FormRendererSlots {
397
427
  readonly renderValidationSummary?: (props: {
398
428
  readonly issues: readonly ValidationError[];
399
429
  }) => ReactNode;
430
+ /** Additional completion data is supplied at runtime; use renderSubmittedValues for typed summary rendering. */
400
431
  readonly renderCompletion?: (props: {
401
432
  readonly message: string;
402
433
  }) => ReactNode;
434
+ readonly renderSubmittedValues?: (props: {
435
+ readonly items: readonly FormSubmittedAnswerItem[];
436
+ readonly schema: FormSchema;
437
+ }) => ReactNode;
438
+ readonly renderFields?: (props: FormFieldsSlotProps) => ReactNode;
403
439
  readonly renderSubmitError?: (props: {
404
440
  readonly error: Error;
405
441
  readonly onRetry?: () => void;
@@ -467,6 +503,7 @@ interface FormContextValue {
467
503
  readonly submitError: Error | null;
468
504
  readonly isSubmitting: boolean;
469
505
  readonly setValue: (fieldId: string, value: FormValue) => void;
506
+ readonly setServerErrors?: (fieldErrors: Readonly<Record<string, string>>) => void;
470
507
  readonly restoreValues: (values: FormValues) => void;
471
508
  readonly validatePage: (pageIndex: number) => AnswerValidationResult;
472
509
  readonly reset: () => void;
@@ -512,6 +549,9 @@ interface FormRendererPresentationProps extends SubmissionProtectionProps {
512
549
  * Defaults to "append" for backwards compatibility.
513
550
  */
514
551
  readonly successRenderMode?: FormSuccessRenderMode;
552
+ readonly submissionConfirmationRenderMode?: SubmissionConfirmationRenderMode;
553
+ readonly showHiddenFieldsInSummary?: boolean;
554
+ readonly fieldsClassName?: string;
515
555
  /** @deprecated Use successRenderMode="replace" instead. */
516
556
  readonly hideFormOnSuccess?: boolean;
517
557
  readonly successMessageKey?: string;
@@ -532,4 +572,4 @@ interface StandaloneFormRendererProps extends FormRendererPresentationProps {
532
572
  type FormRendererProps = FormRendererPresentationProps | StandaloneFormRendererProps;
533
573
  declare function FormRenderer(props: FormRendererProps): react.JSX.Element;
534
574
 
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 };
575
+ 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 FormCompletionSlotProps, type FormContextValue, type FormFieldsSlotProps, FormProvider, type FormProviderProps, FormRenderer, type FormRendererPresentationProps, type FormRendererProps, type FormRendererSlots, type FormServerErrorPayload, FormSubmissionError, type FormSubmitHandler, type FormSubmitState, type FormSubmitStatus, type FormSubmittedAnswerItem, type FormSuccessRenderMode, type IconButtonProps, type InputComponentProps, type ManualTranslationContext, type ManualTranslationTarget, type RenderSubmitButtonProps, type StandaloneFormRendererProps, type SubmissionAttempt, type SubmissionAttemptStore, type SubmissionConfirmationRenderMode, 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
@@ -2635,6 +2635,17 @@ function FormProvider({
2635
2635
  },
2636
2636
  [validSchema, validationPageIndex]
2637
2637
  );
2638
+ const setServerErrors = useCallback2((fieldErrors) => {
2639
+ setErrors(
2640
+ Object.fromEntries(
2641
+ Object.entries(fieldErrors).map(([fieldId, message]) => [
2642
+ fieldId,
2643
+ { fieldId, code: "invalid_type", messageKey: message, params: {} }
2644
+ ])
2645
+ )
2646
+ );
2647
+ setValidationPageIndex(null);
2648
+ }, []);
2638
2649
  const restoreValues = useCallback2(
2639
2650
  (restoredValues) => {
2640
2651
  const fieldIds = new Set(validSchema.fields.map((field) => field.id));
@@ -2719,6 +2730,7 @@ function FormProvider({
2719
2730
  submitError,
2720
2731
  isSubmitting: submitStatus === "submitting",
2721
2732
  setValue,
2733
+ setServerErrors,
2722
2734
  restoreValues,
2723
2735
  validatePage,
2724
2736
  reset,
@@ -2732,6 +2744,7 @@ function FormProvider({
2732
2744
  reset,
2733
2745
  restoreValues,
2734
2746
  setValue,
2747
+ setServerErrors,
2735
2748
  submit,
2736
2749
  submitError,
2737
2750
  submitStatus,
@@ -2882,12 +2895,25 @@ import {
2882
2895
  } from "@form-engine-ts/core";
2883
2896
  import {
2884
2897
  Fragment as Fragment2,
2898
+ useCallback as useCallback3,
2885
2899
  useEffect as useEffect3,
2886
2900
  useId,
2887
2901
  useMemo as useMemo4,
2888
2902
  useRef as useRef2,
2889
2903
  useState as useState4
2890
2904
  } from "react";
2905
+
2906
+ // src/types.ts
2907
+ var FormSubmissionError = class extends Error {
2908
+ payload;
2909
+ constructor(message, payload) {
2910
+ super(message);
2911
+ this.name = "FormSubmissionError";
2912
+ this.payload = payload ?? { formError: message };
2913
+ }
2914
+ };
2915
+
2916
+ // src/renderer.tsx
2891
2917
  import { Fragment as Fragment3, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
2892
2918
  function describedBy(field, error, helpId, errorId) {
2893
2919
  const ids = [field.description === void 0 ? void 0 : helpId, error === void 0 ? void 0 : errorId].filter(
@@ -3084,6 +3110,30 @@ function isRecord(value) {
3084
3110
  function isFormValue(value) {
3085
3111
  return value === void 0 || typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value) || Array.isArray(value) && value.every((item) => typeof item === "string");
3086
3112
  }
3113
+ function displaySubmittedValue(field, value, translate) {
3114
+ if (value === void 0 || value === null) return "";
3115
+ if (field.type === "checkbox") return value === true ? translate("form.yes") : translate("form.no");
3116
+ if (field.type === "multi-select" && Array.isArray(value)) {
3117
+ const labels = new Map(field.options.map((option) => [option.id, option.label]));
3118
+ return value.map((item) => labels.get(item) ?? item).join(", ");
3119
+ }
3120
+ if ((field.type === "radio" || field.type === "select") && typeof value === "string") {
3121
+ return field.options.find((option) => option.id === value)?.label ?? value;
3122
+ }
3123
+ if (Array.isArray(value)) return value.join(", ");
3124
+ return String(value);
3125
+ }
3126
+ function buildSubmittedItems(schema, answers, visibility, translate, showHiddenFields) {
3127
+ return schema.fields.filter((field) => showHiddenFields || visibility[field.id] === true).map((field) => ({
3128
+ fieldId: field.id,
3129
+ title: field.title,
3130
+ type: field.type,
3131
+ rawValue: answers[field.id],
3132
+ displayValue: displaySubmittedValue(field, answers[field.id], translate),
3133
+ visible: visibility[field.id] === true,
3134
+ ...field.metadata === void 0 ? {} : { metadata: field.metadata }
3135
+ }));
3136
+ }
3087
3137
  function parseDraft(serialized) {
3088
3138
  try {
3089
3139
  const value = JSON.parse(serialized);
@@ -3111,6 +3161,9 @@ function ContextFormRenderer({
3111
3161
  beforeSubmit,
3112
3162
  onDraftSave,
3113
3163
  successRenderMode = "append",
3164
+ submissionConfirmationRenderMode = "inline",
3165
+ showHiddenFieldsInSummary = false,
3166
+ fieldsClassName,
3114
3167
  hideFormOnSuccess = false,
3115
3168
  submissionGuards = [],
3116
3169
  receiptStore,
@@ -3129,9 +3182,11 @@ function ContextFormRenderer({
3129
3182
  const [guardMessage, setGuardMessage] = useState4(null);
3130
3183
  const [guardsPending, setGuardsPending] = useState4(false);
3131
3184
  const [receipt, setReceipt] = useState4(null);
3185
+ const [completionData, setCompletionData] = useState4(null);
3132
3186
  const [receiptLoaded, setReceiptLoaded] = useState4(receiptStore === void 0);
3133
3187
  const rendererSubmissionInFlight = useRef2(false);
3134
3188
  const completionRef = useRef2(null);
3189
+ const confirmationRef = useRef2(null);
3135
3190
  const pages = form.schema.pages;
3136
3191
  const visiblePageIndexes = useMemo4(
3137
3192
  () => pages === void 0 ? [] : pages.flatMap((page, index) => form.pageVisibility[page.id] === true ? [index] : []),
@@ -3144,6 +3199,10 @@ function ContextFormRenderer({
3144
3199
  const submitState = confirmation === null && !guardsPending ? form.submitStatus : "confirming";
3145
3200
  const interactionLocked = submitState === "confirming" || submitState === "submitting";
3146
3201
  const isReplaceMode = successRenderMode === "replace" || hideFormOnSuccess;
3202
+ const focusSubmitButton = useCallback3(() => {
3203
+ const button = formRef.current?.querySelector(".fe-submit, button[type='submit'], button");
3204
+ button?.focus();
3205
+ }, []);
3147
3206
  useEffect3(() => {
3148
3207
  let active = true;
3149
3208
  if (receiptStore === void 0) {
@@ -3187,6 +3246,21 @@ function ContextFormRenderer({
3187
3246
  if (!isReplaceMode || form.submitStatus !== "success") return;
3188
3247
  completionRef.current?.focus();
3189
3248
  }, [form.submitStatus, isReplaceMode]);
3249
+ useEffect3(() => {
3250
+ if (confirmation === null) return;
3251
+ const confirmButton = confirmationRef.current?.querySelector("[data-fe-confirm], button");
3252
+ confirmButton?.focus();
3253
+ if (submissionConfirmationRenderMode !== "dialog") return;
3254
+ const onKeyDown = (event) => {
3255
+ if (event.key === "Escape") {
3256
+ event.preventDefault();
3257
+ setConfirmation(null);
3258
+ globalThis.setTimeout(focusSubmitButton, 0);
3259
+ }
3260
+ };
3261
+ globalThis.addEventListener("keydown", onKeyDown);
3262
+ return () => globalThis.removeEventListener("keydown", onKeyDown);
3263
+ }, [confirmation, focusSubmitButton, submissionConfirmationRenderMode]);
3190
3264
  useEffect3(() => {
3191
3265
  if (autoSaveKey === void 0 || typeof globalThis.localStorage === "undefined") return;
3192
3266
  const loadIdentity = `${autoSaveKey}:${form.schema.id}:${form.schema.version}`;
@@ -3302,7 +3376,33 @@ function ContextFormRenderer({
3302
3376
  focusFirstIssue(firstInvalidFieldId);
3303
3377
  return result;
3304
3378
  }
3379
+ if (result.status === "error") {
3380
+ if (result.error instanceof FormSubmissionError) {
3381
+ const fieldErrors = result.error.payload.fieldErrors ?? {};
3382
+ form.setServerErrors?.(fieldErrors);
3383
+ const firstServerFieldId = Object.keys(fieldErrors)[0];
3384
+ if (firstServerFieldId !== void 0) {
3385
+ const invalidPageIndex = pages?.findIndex((page) => page.questionIds.includes(firstServerFieldId));
3386
+ if (invalidPageIndex !== void 0 && invalidPageIndex >= 0) setCurrentPageIndex(invalidPageIndex);
3387
+ focusFirstIssue(firstServerFieldId);
3388
+ }
3389
+ }
3390
+ return result;
3391
+ }
3305
3392
  if (result.status !== "success") return result;
3393
+ const submittedAnswers = { ...form.values };
3394
+ const submittedItems = buildSubmittedItems(
3395
+ form.schema,
3396
+ submittedAnswers,
3397
+ form.visibility,
3398
+ (key) => form.translate(key),
3399
+ showHiddenFieldsInSummary
3400
+ );
3401
+ setCompletionData({
3402
+ answers: submittedAnswers,
3403
+ submittedItems,
3404
+ ...result.response === void 0 ? {} : { response: result.response }
3405
+ });
3306
3406
  if (receiptStore !== void 0) {
3307
3407
  const response = result.response;
3308
3408
  const submissionId = response?.submissionId ?? submissionAttempt?.attemptId;
@@ -3349,6 +3449,7 @@ function ContextFormRenderer({
3349
3449
  };
3350
3450
  const cancelSubmission = () => {
3351
3451
  setConfirmation(null);
3452
+ globalThis.setTimeout(focusSubmitButton, 0);
3352
3453
  };
3353
3454
  const resetReceipt = async () => {
3354
3455
  if (receiptStore === void 0) return;
@@ -3369,7 +3470,48 @@ function ContextFormRenderer({
3369
3470
  return slots.renderSubmitButton?.(submitButtonProps) ?? /* @__PURE__ */ jsx3("button", { className: "fe-submit", type: "submit", disabled: submitButtonProps.disabled, children: form.translate(form.schema.submitLabelKey ?? "form.submit") });
3370
3471
  };
3371
3472
  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 }) });
3473
+ const activeCompletionData = completionData ?? {
3474
+ answers: { ...form.values },
3475
+ submittedItems: buildSubmittedItems(
3476
+ form.schema,
3477
+ form.values,
3478
+ form.visibility,
3479
+ (key) => form.translate(key),
3480
+ showHiddenFieldsInSummary
3481
+ )
3482
+ };
3483
+ const completionProps = {
3484
+ message: completionMessage,
3485
+ schema: form.schema,
3486
+ answers: activeCompletionData.answers,
3487
+ submittedItems: activeCompletionData.submittedItems,
3488
+ ...activeCompletionData.response === void 0 ? {} : { response: activeCompletionData.response },
3489
+ onReset: form.reset
3490
+ };
3491
+ const completionRegion = /* @__PURE__ */ jsxs2("div", { ref: completionRef, className: "fe-completion", role: "status", "aria-live": "polite", tabIndex: -1, children: [
3492
+ slots.renderCompletion?.(completionProps) ?? /* @__PURE__ */ jsx3("div", { children: completionMessage }),
3493
+ slots.renderSubmittedValues?.({ items: activeCompletionData.submittedItems, schema: form.schema })
3494
+ ] });
3495
+ const confirmationContent = /* @__PURE__ */ jsx3(
3496
+ "div",
3497
+ {
3498
+ ref: confirmationRef,
3499
+ className: "fe-submission-confirmation",
3500
+ role: submissionConfirmationRenderMode === "dialog" ? void 0 : "dialog",
3501
+ children: slots.renderSubmissionConfirmation?.({
3502
+ findings: confirmation?.findings ?? [],
3503
+ message: confirmation?.message ?? form.translate("form.confirmSensitiveData"),
3504
+ schema: form.schema,
3505
+ visibleValues,
3506
+ onConfirm: confirmSubmission,
3507
+ onCancel: cancelSubmission
3508
+ }) ?? /* @__PURE__ */ jsxs2(Fragment3, { children: [
3509
+ /* @__PURE__ */ jsx3("p", { children: confirmation?.message ?? form.translate("form.confirmSensitiveData") }),
3510
+ /* @__PURE__ */ jsx3("button", { type: "button", "data-fe-confirm": "true", onClick: confirmSubmission, children: form.translate("form.confirmSubmission") }),
3511
+ /* @__PURE__ */ jsx3("button", { type: "button", onClick: cancelSubmission, children: form.translate("form.cancelSubmission") })
3512
+ ] })
3513
+ }
3514
+ );
3373
3515
  if (!receiptLoaded) return null;
3374
3516
  if (receipt !== null) {
3375
3517
  return /* @__PURE__ */ jsx3("div", { className: `fe-form fe-already-submitted ${className}`.trim(), children: slots.renderAlreadySubmitted?.({
@@ -3383,127 +3525,136 @@ function ContextFormRenderer({
3383
3525
  if (form.submitStatus === "success" && isReplaceMode) {
3384
3526
  return /* @__PURE__ */ jsx3("div", { className: `fe-form ${className}`.trim(), children: completionRegion });
3385
3527
  }
3386
- return /* @__PURE__ */ jsxs2("form", { ref: formRef, className: `fe-form ${className}`.trim(), noValidate: true, onSubmit: handleSubmit, children: [
3387
- slots.renderHeader?.({
3388
- title: form.schema.title,
3389
- ...form.schema.description === void 0 ? {} : { description: form.schema.description }
3390
- }) ?? /* @__PURE__ */ jsxs2("header", { className: "fe-header", children: [
3391
- /* @__PURE__ */ jsx3("h1", { children: form.schema.title }),
3392
- form.schema.description === void 0 ? null : /* @__PURE__ */ jsx3("p", { children: form.schema.description }),
3393
- pages === void 0 ? null : /* @__PURE__ */ jsxs2("div", { className: "fe-progress", children: [
3394
- /* @__PURE__ */ jsx3(
3395
- "div",
3396
- {
3397
- className: "form-progress-bar",
3398
- role: "progressbar",
3399
- "aria-valuemin": 1,
3400
- "aria-valuemax": visiblePageIndexes.length,
3401
- "aria-valuenow": activeVisibleIndex + 1,
3402
- children: /* @__PURE__ */ jsx3(
3403
- "div",
3404
- {
3405
- className: "form-progress-fill",
3406
- style: { width: `${(activeVisibleIndex + 1) / visiblePageIndexes.length * 100}%` }
3528
+ if (confirmation !== null && submissionConfirmationRenderMode === "replace") {
3529
+ return /* @__PURE__ */ jsx3("div", { className: `fe-form ${className}`.trim(), children: confirmationContent });
3530
+ }
3531
+ return /* @__PURE__ */ jsxs2(Fragment3, { children: [
3532
+ /* @__PURE__ */ jsxs2(
3533
+ "form",
3534
+ {
3535
+ ref: formRef,
3536
+ className: `fe-form ${className}`.trim(),
3537
+ noValidate: true,
3538
+ onSubmit: handleSubmit,
3539
+ "aria-hidden": confirmation !== null && submissionConfirmationRenderMode === "dialog" ? true : void 0,
3540
+ children: [
3541
+ slots.renderHeader?.({
3542
+ title: form.schema.title,
3543
+ ...form.schema.description === void 0 ? {} : { description: form.schema.description }
3544
+ }) ?? /* @__PURE__ */ jsxs2("header", { className: "fe-header", children: [
3545
+ /* @__PURE__ */ jsx3("h1", { children: form.schema.title }),
3546
+ form.schema.description === void 0 ? null : /* @__PURE__ */ jsx3("p", { children: form.schema.description }),
3547
+ pages === void 0 ? null : /* @__PURE__ */ jsxs2("div", { className: "fe-progress", children: [
3548
+ /* @__PURE__ */ jsx3(
3549
+ "div",
3550
+ {
3551
+ className: "form-progress-bar",
3552
+ role: "progressbar",
3553
+ "aria-valuemin": 1,
3554
+ "aria-valuemax": visiblePageIndexes.length,
3555
+ "aria-valuenow": activeVisibleIndex + 1,
3556
+ children: /* @__PURE__ */ jsx3(
3557
+ "div",
3558
+ {
3559
+ className: "form-progress-fill",
3560
+ style: { width: `${(activeVisibleIndex + 1) / visiblePageIndexes.length * 100}%` }
3561
+ }
3562
+ )
3563
+ }
3564
+ ),
3565
+ /* @__PURE__ */ jsx3("span", { children: form.translate("form.step", { current: activeVisibleIndex + 1, total: visiblePageIndexes.length }) })
3566
+ ] }),
3567
+ draftRestored ? /* @__PURE__ */ jsx3("span", { className: "form-draft-badge", children: form.translate("form.draftRestored") }) : null
3568
+ ] }),
3569
+ activePage === void 0 ? null : slots.renderPageHeader?.({
3570
+ page: activePage,
3571
+ pageIndex: activeVisibleIndex,
3572
+ totalPages: visiblePageIndexes.length
3573
+ }) ?? /* @__PURE__ */ jsxs2("div", { className: "fe-page-header", children: [
3574
+ activePage.title === void 0 ? null : /* @__PURE__ */ jsx3("h2", { className: "fe-page-title", children: activePage.title }),
3575
+ activePage.description === void 0 ? null : /* @__PURE__ */ jsx3("p", { className: "fe-page-description", children: activePage.description })
3576
+ ] }),
3577
+ (() => {
3578
+ const fieldChildren = form.schema.fields.filter((field) => form.visibility[field.id] === true && (fieldIds === void 0 || fieldIds.has(field.id))).map((field) => {
3579
+ const error = form.errors[field.id];
3580
+ const props = {
3581
+ field,
3582
+ value: form.values[field.id],
3583
+ error,
3584
+ setValue: (value) => form.setValue(field.id, value),
3585
+ translate: form.translate,
3586
+ inputId: `${prefix}-${field.id}`,
3587
+ errorId: `${prefix}-${field.id}-error`,
3588
+ helpId: `${prefix}-${field.id}-help`,
3589
+ ...slots.renderCharacterCount === void 0 ? {} : { renderCharacterCount: slots.renderCharacterCount }
3590
+ };
3591
+ if (slots.renderField !== void 0) {
3592
+ return /* @__PURE__ */ jsx3(Fragment2, { children: slots.renderField({
3593
+ question: field,
3594
+ value: form.values[field.id],
3595
+ onChange: (value) => {
3596
+ if (isFormValue(value)) form.setValue(field.id, value);
3597
+ },
3598
+ ...error === void 0 ? {} : { error }
3599
+ }) }, field.id);
3407
3600
  }
3408
- )
3409
- }
3410
- ),
3411
- /* @__PURE__ */ jsx3("span", { children: form.translate("form.step", { current: activeVisibleIndex + 1, total: visiblePageIndexes.length }) })
3412
- ] }),
3413
- draftRestored ? /* @__PURE__ */ jsx3("span", { className: "form-draft-badge", children: form.translate("form.draftRestored") }) : null
3414
- ] }),
3415
- activePage === void 0 ? null : slots.renderPageHeader?.({
3416
- page: activePage,
3417
- pageIndex: activeVisibleIndex,
3418
- totalPages: visiblePageIndexes.length
3419
- }) ?? /* @__PURE__ */ jsxs2("div", { className: "fe-page-header", children: [
3420
- activePage.title === void 0 ? null : /* @__PURE__ */ jsx3("h2", { className: "fe-page-title", children: activePage.title }),
3421
- activePage.description === void 0 ? null : /* @__PURE__ */ jsx3("p", { className: "fe-page-description", children: activePage.description })
3422
- ] }),
3423
- /* @__PURE__ */ jsx3("div", { className: "fe-fields", children: form.schema.fields.filter((field) => form.visibility[field.id] === true && (fieldIds === void 0 || fieldIds.has(field.id))).map((field) => {
3424
- const error = form.errors[field.id];
3425
- const props = {
3426
- field,
3427
- value: form.values[field.id],
3428
- error,
3429
- setValue: (value) => form.setValue(field.id, value),
3430
- translate: form.translate,
3431
- inputId: `${prefix}-${field.id}`,
3432
- errorId: `${prefix}-${field.id}-error`,
3433
- helpId: `${prefix}-${field.id}-help`,
3434
- ...slots.renderCharacterCount === void 0 ? {} : { renderCharacterCount: slots.renderCharacterCount }
3435
- };
3436
- if (slots.renderField !== void 0) {
3437
- return /* @__PURE__ */ jsx3(Fragment2, { children: slots.renderField({
3438
- question: field,
3439
- value: form.values[field.id],
3440
- onChange: (value) => {
3441
- if (isFormValue(value)) form.setValue(field.id, value);
3442
- },
3443
- ...error === void 0 ? {} : { error }
3444
- }) }, field.id);
3601
+ const Component = components[field.type];
3602
+ return Component === void 0 ? /* @__PURE__ */ jsx3(DefaultField, { ...props }, field.id) : /* @__PURE__ */ jsx3(Component, { ...props }, field.id);
3603
+ });
3604
+ const fieldClassName = `fe-fields${fieldsClassName === void 0 ? "" : ` ${fieldsClassName}`}`;
3605
+ return slots.renderFields?.({ children: fieldChildren, className: fieldClassName }) ?? /* @__PURE__ */ jsx3("div", { className: fieldClassName, children: fieldChildren });
3606
+ })(),
3607
+ guardMessage === null ? null : /* @__PURE__ */ jsx3("div", { role: "alert", children: guardMessage }),
3608
+ confirmation !== null && submissionConfirmationRenderMode === "inline" ? confirmationContent : null,
3609
+ validationIssues.length === 0 ? null : slots.renderValidationSummary?.({ issues: validationIssues }) ?? /* @__PURE__ */ jsxs2("div", { className: "fe-validation-summary", role: "alert", children: [
3610
+ validationIssues.length,
3611
+ " validation error",
3612
+ validationIssues.length === 1 ? "" : "s",
3613
+ "."
3614
+ ] }),
3615
+ pages === void 0 ? /* @__PURE__ */ jsxs2(Fragment3, { children: [
3616
+ slots.renderNavigation?.({
3617
+ currentPage: 0,
3618
+ totalPages: 1,
3619
+ canPrev: false,
3620
+ canNext: false,
3621
+ onPrev: () => void 0,
3622
+ onNext: () => void 0
3623
+ }),
3624
+ renderSubmitButton()
3625
+ ] }) : /* @__PURE__ */ jsxs2("div", { className: "form-step-navigation", children: [
3626
+ slots.renderNavigation?.({
3627
+ currentPage: activeVisibleIndex,
3628
+ totalPages: visiblePageIndexes.length,
3629
+ canPrev,
3630
+ canNext,
3631
+ onPrev: () => {
3632
+ if (!interactionLocked) setCurrentPageIndex(visiblePageIndexes[activeVisibleIndex - 1] ?? 0);
3633
+ },
3634
+ onNext: handleNext
3635
+ }) ?? /* @__PURE__ */ jsxs2(Fragment3, { children: [
3636
+ canPrev ? /* @__PURE__ */ jsx3(
3637
+ "button",
3638
+ {
3639
+ className: "btn-prev",
3640
+ type: "button",
3641
+ disabled: interactionLocked,
3642
+ onClick: () => setCurrentPageIndex(visiblePageIndexes[activeVisibleIndex - 1] ?? 0),
3643
+ children: form.translate("form.back")
3644
+ }
3645
+ ) : null,
3646
+ canNext ? /* @__PURE__ */ jsx3("button", { className: "btn-next", type: "button", disabled: interactionLocked, onClick: handleNext, children: form.translate("form.next") }) : null
3647
+ ] }),
3648
+ canNext ? null : renderSubmitButton()
3649
+ ] }),
3650
+ /* @__PURE__ */ jsxs2("div", { className: "fe-status", "aria-live": "polite", children: [
3651
+ form.submitStatus === "success" ? completionRegion : null,
3652
+ form.submitStatus === "error" && form.submitError !== null ? slots.renderSubmitError?.({ error: form.submitError, onRetry: () => void submitValues() }) ?? (form.submitError instanceof FormSubmissionError && form.submitError.payload.formError !== void 0 ? /* @__PURE__ */ jsx3("div", { role: "alert", children: form.submitError.payload.formError }) : errorMessageKey === void 0 ? null : /* @__PURE__ */ jsx3("div", { role: "alert", children: form.translate(errorMessageKey) })) : null
3653
+ ] })
3654
+ ]
3445
3655
  }
3446
- const Component = components[field.type];
3447
- return Component === void 0 ? /* @__PURE__ */ jsx3(DefaultField, { ...props }, field.id) : /* @__PURE__ */ jsx3(Component, { ...props }, field.id);
3448
- }) }),
3449
- guardMessage === null ? null : /* @__PURE__ */ jsx3("div", { role: "alert", children: guardMessage }),
3450
- confirmation === null ? null : slots.renderSubmissionConfirmation?.({
3451
- findings: confirmation.findings,
3452
- message: confirmation.message ?? form.translate("form.confirmSensitiveData"),
3453
- schema: form.schema,
3454
- visibleValues,
3455
- onConfirm: confirmSubmission,
3456
- onCancel: cancelSubmission
3457
- }) ?? /* @__PURE__ */ jsxs2("div", { className: "fe-submission-confirmation", role: "dialog", "aria-modal": "true", children: [
3458
- /* @__PURE__ */ jsx3("p", { children: confirmation.message ?? form.translate("form.confirmSensitiveData") }),
3459
- /* @__PURE__ */ jsx3("button", { type: "button", onClick: confirmSubmission, children: form.translate("form.confirmSubmission") }),
3460
- /* @__PURE__ */ jsx3("button", { type: "button", onClick: cancelSubmission, children: form.translate("form.cancelSubmission") })
3461
- ] }),
3462
- validationIssues.length === 0 ? null : slots.renderValidationSummary?.({ issues: validationIssues }) ?? /* @__PURE__ */ jsxs2("div", { className: "fe-validation-summary", role: "alert", children: [
3463
- validationIssues.length,
3464
- " validation error",
3465
- validationIssues.length === 1 ? "" : "s",
3466
- "."
3467
- ] }),
3468
- pages === void 0 ? /* @__PURE__ */ jsxs2(Fragment3, { children: [
3469
- slots.renderNavigation?.({
3470
- currentPage: 0,
3471
- totalPages: 1,
3472
- canPrev: false,
3473
- canNext: false,
3474
- onPrev: () => void 0,
3475
- onNext: () => void 0
3476
- }),
3477
- renderSubmitButton()
3478
- ] }) : /* @__PURE__ */ jsxs2("div", { className: "form-step-navigation", children: [
3479
- slots.renderNavigation?.({
3480
- currentPage: activeVisibleIndex,
3481
- totalPages: visiblePageIndexes.length,
3482
- canPrev,
3483
- canNext,
3484
- onPrev: () => {
3485
- if (!interactionLocked) setCurrentPageIndex(visiblePageIndexes[activeVisibleIndex - 1] ?? 0);
3486
- },
3487
- onNext: handleNext
3488
- }) ?? /* @__PURE__ */ jsxs2(Fragment3, { children: [
3489
- canPrev ? /* @__PURE__ */ jsx3(
3490
- "button",
3491
- {
3492
- className: "btn-prev",
3493
- type: "button",
3494
- disabled: interactionLocked,
3495
- onClick: () => setCurrentPageIndex(visiblePageIndexes[activeVisibleIndex - 1] ?? 0),
3496
- children: form.translate("form.back")
3497
- }
3498
- ) : null,
3499
- canNext ? /* @__PURE__ */ jsx3("button", { className: "btn-next", type: "button", disabled: interactionLocked, onClick: handleNext, children: form.translate("form.next") }) : null
3500
- ] }),
3501
- canNext ? null : renderSubmitButton()
3502
- ] }),
3503
- /* @__PURE__ */ jsxs2("div", { className: "fe-status", "aria-live": "polite", children: [
3504
- form.submitStatus === "success" ? completionRegion : null,
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
3506
- ] })
3656
+ ),
3657
+ confirmation !== null && submissionConfirmationRenderMode === "dialog" ? /* @__PURE__ */ jsx3("div", { className: "fe-confirmation-dialog-backdrop", role: "dialog", "aria-modal": "true", children: confirmationContent }) : null
3507
3658
  ] });
3508
3659
  }
3509
3660
  var RENDERER_MESSAGES = {
@@ -3516,6 +3667,8 @@ var RENDERER_MESSAGES = {
3516
3667
  "form.confirmSensitiveData": "Sensitive data may be included. Confirm before submitting.",
3517
3668
  "form.confirmSubmission": "Confirm submission",
3518
3669
  "form.cancelSubmission": "Cancel",
3670
+ "form.yes": "Yes",
3671
+ "form.no": "No",
3519
3672
  "form.alreadySubmitted": "Already submitted.",
3520
3673
  "form.submitAnother": "Submit another response",
3521
3674
  "validation.required": "This field is required."
@@ -3556,6 +3709,7 @@ export {
3556
3709
  FormBuilder,
3557
3710
  FormProvider,
3558
3711
  FormRenderer,
3712
+ FormSubmissionError,
3559
3713
  createLocalStorageSubmissionAttemptStore,
3560
3714
  createLocalStorageSubmissionReceiptStore,
3561
3715
  resolveInitialFieldType,
package/dist/styles.css CHANGED
@@ -281,3 +281,20 @@
281
281
  .form-engine-builder__add {
282
282
  justify-self: start;
283
283
  }
284
+ .fe-confirmation-dialog-backdrop {
285
+ position: fixed;
286
+ inset: 0;
287
+ z-index: 1000;
288
+ display: grid;
289
+ place-items: center;
290
+ padding: 1rem;
291
+ background: rgb(0 0 0 / 45%);
292
+ }
293
+ .fe-confirmation-dialog-backdrop .fe-submission-confirmation {
294
+ max-width: 32rem;
295
+ padding: 1.5rem;
296
+ background: Canvas;
297
+ color: CanvasText;
298
+ border-radius: 0.5rem;
299
+ box-shadow: 0 1rem 3rem rgb(0 0 0 / 25%);
300
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@form-engine-ts/react",
3
- "version": "2.9.5",
3
+ "version": "2.9.6",
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.5",
46
- "@form-engine-ts/privacy": "2.9.5"
45
+ "@form-engine-ts/core": "2.9.6",
46
+ "@form-engine-ts/privacy": "2.9.6"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "react": ">=18.2 <20",