@form-engine-ts/react 2.9.6 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -20,6 +20,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ BUILDER_TRANSLATION_ALIASES: () => BUILDER_TRANSLATION_ALIASES,
24
+ BUILDER_TRANSLATION_KEYS: () => BUILDER_TRANSLATION_KEYS,
23
25
  FormBuilder: () => FormBuilder,
24
26
  FormProvider: () => FormProvider,
25
27
  FormRenderer: () => FormRenderer,
@@ -774,6 +776,33 @@ function useFormBuilder({
774
776
  };
775
777
  }
776
778
 
779
+ // src/i18n.ts
780
+ var BUILDER_TRANSLATION_KEYS = {
781
+ ADD_FIELD: "builder.actions.addField",
782
+ SELECT_FIELD_TYPE: "builder.fields.selectType",
783
+ SELECT_LOCALE_TO_ADD: "builder.localization.selectLocaleToAdd",
784
+ FIELD_TYPE_TEXT: "builder.fields.typeText",
785
+ FIELD_TYPE_TEXTAREA: "builder.fields.typeTextarea",
786
+ FIELD_TYPE_NUMBER: "builder.fields.typeNumber",
787
+ FIELD_TYPE_RATING: "builder.fields.typeRating",
788
+ FIELD_TYPE_RADIO: "builder.fields.typeRadio",
789
+ FIELD_TYPE_CHECKBOX: "builder.fields.typeCheckbox",
790
+ FIELD_TYPE_SELECT: "builder.fields.typeSelect",
791
+ FIELD_TYPE_MULTI_SELECT: "builder.fields.typeMultiSelect"
792
+ };
793
+ var BUILDER_TRANSLATION_ALIASES = {
794
+ "builder.actions.addField": "builder.addQuestion",
795
+ "builder.localization.selectLocaleToAdd": "builder.selectLocaleToAdd",
796
+ "builder.fields.typeText": "builder.fieldType.text",
797
+ "builder.fields.typeTextarea": "builder.fieldType.textarea",
798
+ "builder.fields.typeNumber": "builder.fieldType.number",
799
+ "builder.fields.typeRating": "builder.fieldType.rating",
800
+ "builder.fields.typeRadio": "builder.fieldType.radio",
801
+ "builder.fields.typeCheckbox": "builder.fieldType.checkbox",
802
+ "builder.fields.typeSelect": "builder.fieldType.select",
803
+ "builder.fields.typeMultiSelect": "builder.fieldType.multi-select"
804
+ };
805
+
777
806
  // src/builder.tsx
778
807
  var import_jsx_runtime = require("react/jsx-runtime");
779
808
  function DefaultButton({
@@ -844,6 +873,26 @@ function defaultIconFor(actionType) {
844
873
  return "\u6587";
845
874
  }
846
875
  }
876
+ function defaultFieldTypeIcon(type) {
877
+ switch (type) {
878
+ case "text":
879
+ return "T";
880
+ case "textarea":
881
+ return "\u2261";
882
+ case "number":
883
+ return "#";
884
+ case "rating":
885
+ return "\u2605";
886
+ case "select":
887
+ return "\u25BE";
888
+ case "multi-select":
889
+ return "\u2611";
890
+ case "checkbox":
891
+ return "\u25A1";
892
+ case "radio":
893
+ return "\u25C9";
894
+ }
895
+ }
847
896
  function DefaultTextInput({
848
897
  id,
849
898
  className,
@@ -956,8 +1005,15 @@ function DefaultSelect({
956
1005
  helperText,
957
1006
  value,
958
1007
  onChange,
959
- options
1008
+ onKeyDown,
1009
+ options,
1010
+ renderOption,
1011
+ renderValue
960
1012
  }) {
1013
+ const normalizedOptions = options.map(
1014
+ (option) => typeof option === "string" ? { value: option, label: option } : option
1015
+ );
1016
+ const selectedOption = normalizedOptions.find((option) => option.value === value);
961
1017
  const labelElement = label === void 0 ? null : id === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: label }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { htmlFor: id, children: label });
962
1018
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
963
1019
  labelElement,
@@ -975,9 +1031,11 @@ function DefaultSelect({
975
1031
  "aria-invalid": error === true ? true : void 0,
976
1032
  value,
977
1033
  onChange: (event) => onChange(event.currentTarget.value),
978
- children: options.map((option) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: option.value, disabled: option.disabled, children: option.label }, option.value))
1034
+ onKeyDown,
1035
+ children: normalizedOptions.map((option) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: option.value, disabled: option.disabled, children: renderOption === void 0 ? option.label : renderOption(option) }, option.value))
979
1036
  }
980
1037
  ),
1038
+ renderValue === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("output", { children: renderValue(selectedOption) }),
981
1039
  helperText === void 0 || helperText.length === 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("small", { id: ariaDescribedBy, children: helperText })
982
1040
  ] });
983
1041
  }
@@ -1065,7 +1123,8 @@ var DEFAULT_COMPONENTS = {
1065
1123
  Section: DefaultSection,
1066
1124
  Fieldset: DefaultFieldset,
1067
1125
  ErrorMessage: DefaultErrorMessage,
1068
- renderIcon: defaultIconFor
1126
+ renderIcon: defaultIconFor,
1127
+ renderFieldTypeIcon: defaultFieldTypeIcon
1069
1128
  };
1070
1129
  var BuilderPrimitiveContext = (0, import_react2.createContext)({
1071
1130
  components: DEFAULT_COMPONENTS,
@@ -1245,6 +1304,11 @@ var BUILDER_DEFAULTS = {
1245
1304
  "builder.translating": "Translating\u2026",
1246
1305
  "builder.translationLocale": "Translation locale",
1247
1306
  "builder.selectLocale": "Select a locale to edit translations.",
1307
+ "builder.localization.selectLocaleToAdd": "Select a locale to add",
1308
+ "builder.localization.noLocalesConfigured": "Translations not configured",
1309
+ "builder.localization.localesConfiguredSummary": "{{count}} locales configured",
1310
+ "builder.localization.allLocalesAdded": "\u3059\u3079\u3066\u306E\u5019\u88DC\u8A00\u8A9E\u304C\u8FFD\u52A0\u6E08\u307F\u3067\u3059",
1311
+ "builder.localization.maxLocalesReached": "\u767B\u9332\u53EF\u80FD\u306A\u6700\u5927\u8A00\u8A9E\u6570\uFF08{{max}}\uFF09\u306B\u9054\u3057\u307E\u3057\u305F",
1248
1312
  "builder.translation": "{{locale}} translation",
1249
1313
  "builder.translatedFormTitle": "Translated form title",
1250
1314
  "builder.translatedFormDescription": "Translated form description",
@@ -1261,6 +1325,16 @@ var BUILDER_DEFAULTS = {
1261
1325
  "builder.actions.close": "Close",
1262
1326
  "builder.actions.dragHandle": "Reorder",
1263
1327
  "builder.translationUnavailable": "Provide an async translation adapter to enable automatic translation.",
1328
+ "builder.fields.selectType": "Select field type",
1329
+ "builder.fields.typeText": "Text",
1330
+ "builder.fields.typeTextarea": "Textarea",
1331
+ "builder.fields.typeNumber": "Number",
1332
+ "builder.fields.typeRating": "Rating",
1333
+ "builder.fields.typeSelect": "Select",
1334
+ "builder.fields.typeMultiSelect": "Multi-select",
1335
+ "builder.fields.typeCheckbox": "Checkbox",
1336
+ "builder.fields.typeRadio": "Radio",
1337
+ "builder.actions.addField": "Add question",
1264
1338
  "builder.fieldType.text": "Text",
1265
1339
  "builder.fieldType.textarea": "Textarea",
1266
1340
  "builder.fieldType.number": "Number",
@@ -1297,7 +1371,7 @@ function OrderedBuilderSections({
1297
1371
  return groups.map((group, index) => ({ group, index, rank: ranks.get(sectionGroupName(group) ?? "questions") ?? order.length })).sort((left, right) => left.rank - right.rank || left.index - right.index).map(({ group }) => group);
1298
1372
  }
1299
1373
  function fieldTypeKey(type) {
1300
- return `builder.fieldType.${type}`;
1374
+ return import_core2.DEFAULT_FIELD_TYPE_DEFINITIONS.find((definition) => definition.type === type)?.labelKey ?? `builder.fieldType.${type}`;
1301
1375
  }
1302
1376
  function operatorKey(operator) {
1303
1377
  return `builder.operator.${operator}`;
@@ -1458,7 +1532,8 @@ function FormBuilder({
1458
1532
  const resolvedComponents = { ...DEFAULT_COMPONENTS, ...componentOverrides };
1459
1533
  const components = {
1460
1534
  ...GUARDED_COMPONENTS,
1461
- renderIcon: resolvedComponents.renderIcon
1535
+ renderIcon: resolvedComponents.renderIcon,
1536
+ renderFieldTypeIcon: resolvedComponents.renderFieldTypeIcon
1462
1537
  };
1463
1538
  const defaultStylesDisabled = disableDefaultStyles || unstyled;
1464
1539
  const builderClass = (value) => defaultStylesDisabled ? void 0 : value;
@@ -1489,6 +1564,12 @@ function FormBuilder({
1489
1564
  if (typeof value === "string" || typeof value === "number") translatorParams[name] = value;
1490
1565
  }
1491
1566
  const translated = translator?.translate(key, locale, translatorParams);
1567
+ if (translated !== void 0 && translated !== key) return translated;
1568
+ const alias = BUILDER_TRANSLATION_ALIASES[key];
1569
+ if (alias !== void 0) {
1570
+ const aliased = translator?.translate(alias, locale, translatorParams);
1571
+ if (aliased !== void 0 && aliased !== alias) return aliased;
1572
+ }
1492
1573
  return translated === void 0 ? interpolate(BUILDER_DEFAULTS[key] ?? key, params) : translated;
1493
1574
  };
1494
1575
  const pagesEnabled = features?.pages ?? true;
@@ -2224,6 +2305,7 @@ function FormBuilder({
2224
2305
  index,
2225
2306
  currentLocale: editingLocale,
2226
2307
  translate,
2308
+ ...slots === void 0 ? {} : { slots },
2227
2309
  ...policy === void 0 ? {} : { policy },
2228
2310
  ...features === void 0 ? {} : { features },
2229
2311
  readOnly,
@@ -2595,7 +2677,7 @@ function FormBuilder({
2595
2677
  action: "addField",
2596
2678
  disabled: initialFieldType === null || maxFieldsReached,
2597
2679
  onClick: addField,
2598
- children: translate("builder.addQuestion")
2680
+ children: translate(BUILDER_TRANSLATION_KEYS.ADD_FIELD)
2599
2681
  }
2600
2682
  ) })
2601
2683
  ] }) })
@@ -2608,6 +2690,18 @@ function FormBuilder({
2608
2690
  // src/context.tsx
2609
2691
  var import_core3 = require("@form-engine-ts/core");
2610
2692
  var import_react3 = require("react");
2693
+
2694
+ // src/types.ts
2695
+ var FormSubmissionError = class extends Error {
2696
+ payload;
2697
+ constructor(message, payload) {
2698
+ super(message);
2699
+ this.name = "FormSubmissionError";
2700
+ this.payload = payload ?? { formError: message };
2701
+ }
2702
+ };
2703
+
2704
+ // src/context.tsx
2611
2705
  var import_jsx_runtime2 = require("react/jsx-runtime");
2612
2706
  var FormContext = (0, import_react3.createContext)(null);
2613
2707
  function issuesByField(issues) {
@@ -2615,6 +2709,18 @@ function issuesByField(issues) {
2615
2709
  for (const issue of issues) result[issue.fieldId] ??= issue;
2616
2710
  return result;
2617
2711
  }
2712
+ function defaultAttemptId2() {
2713
+ const randomUuid = globalThis.crypto?.randomUUID;
2714
+ if (typeof randomUuid === "function") return randomUuid.call(globalThis.crypto);
2715
+ return `attempt-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
2716
+ }
2717
+ function isServerErrorPayload(value) {
2718
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
2719
+ const record = value;
2720
+ const fieldErrors = record.fieldErrors;
2721
+ const formError = record.formError;
2722
+ return (Object.hasOwn(record, "fieldErrors") || Object.hasOwn(record, "formError")) && (fieldErrors === void 0 || typeof fieldErrors === "object" && fieldErrors !== null && !Array.isArray(fieldErrors) && Object.values(fieldErrors).every((message) => typeof message === "string")) && (formError === void 0 || typeof formError === "string");
2723
+ }
2618
2724
  function FormProvider({
2619
2725
  schema,
2620
2726
  locale,
@@ -2699,7 +2805,7 @@ function FormProvider({
2699
2805
  setSubmitError(null);
2700
2806
  }, [initialValues]);
2701
2807
  const submit = (0, import_react3.useCallback)(
2702
- async (beforeSubmit, prepareSubmission) => {
2808
+ async (beforeSubmit, submitContext) => {
2703
2809
  if (submissionInFlight.current) return { status: "cancelled" };
2704
2810
  const validation = (0, import_core3.validateAnswers)(validSchema, values);
2705
2811
  if (!validation.valid) {
@@ -2720,13 +2826,22 @@ function FormProvider({
2720
2826
  setSubmitStatus("idle");
2721
2827
  return { status: "cancelled" };
2722
2828
  }
2723
- const submissionValues = prepareSubmission === void 0 ? visibleValues : await prepareSubmission(visibleValues);
2724
- const response = await onSubmit(submissionValues);
2829
+ const context = submitContext ?? {
2830
+ attemptId: defaultAttemptId2(),
2831
+ formId: validSchema.id,
2832
+ formVersion: validSchema.version,
2833
+ locale,
2834
+ submittedAt: (/* @__PURE__ */ new Date()).toISOString()
2835
+ };
2836
+ const response = await onSubmit({ ...visibleValues }, context);
2725
2837
  if (resetOnSuccess) setValues({ ...initialValues });
2726
2838
  setSubmitStatus("success");
2727
2839
  return response === void 0 ? { status: "success" } : { status: "success", response };
2728
2840
  } catch (cause) {
2729
- const error = cause instanceof Error ? cause : new Error(String(cause));
2841
+ const error = isServerErrorPayload(cause) ? new FormSubmissionError(
2842
+ cause.formError ?? (cause instanceof Error ? cause.message : "Form submission failed."),
2843
+ cause
2844
+ ) : cause instanceof Error ? cause : new Error(String(cause));
2730
2845
  setSubmitError(error);
2731
2846
  setSubmitStatus("error");
2732
2847
  return { status: "error", error };
@@ -2734,7 +2849,7 @@ function FormProvider({
2734
2849
  submissionInFlight.current = false;
2735
2850
  }
2736
2851
  },
2737
- [initialValues, onSubmit, resetOnSuccess, validSchema, values]
2852
+ [initialValues, locale, onSubmit, resetOnSuccess, validSchema, values]
2738
2853
  );
2739
2854
  const translate = (0, import_react3.useCallback)(
2740
2855
  (key, params) => translator.translate(key, locale, params),
@@ -2914,18 +3029,6 @@ function useSubmissionReceipts(store, queries) {
2914
3029
  // src/renderer.tsx
2915
3030
  var import_core4 = require("@form-engine-ts/core");
2916
3031
  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
2929
3032
  var import_jsx_runtime3 = require("react/jsx-runtime");
2930
3033
  function describedBy(field, error, helpId, errorId) {
2931
3034
  const ids = [field.description === void 0 ? void 0 : helpId, error === void 0 ? void 0 : errorId].filter(
@@ -3112,7 +3215,11 @@ function DefaultField(props) {
3112
3215
  fieldId: field.id,
3113
3216
  current: typeof value === "string" ? value.length : 0,
3114
3217
  max: field.maxLength
3115
- }) : null,
3218
+ }) ?? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "fe-character-count", "aria-live": "polite", children: [
3219
+ typeof value === "string" ? value.length : 0,
3220
+ " / ",
3221
+ field.maxLength
3222
+ ] }) : null,
3116
3223
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(FieldMessage, { props })
3117
3224
  ] });
3118
3225
  }
@@ -3164,11 +3271,58 @@ function parseDraft(serialized) {
3164
3271
  return null;
3165
3272
  }
3166
3273
  }
3274
+ var DEFAULT_RENDERER_MESSAGES = {
3275
+ en: {
3276
+ submitButton: "Submit",
3277
+ submittingButton: "Submitting...",
3278
+ retryButton: "Retry",
3279
+ requiredField: "This field is required.",
3280
+ alreadySubmittedTitle: "Already Submitted",
3281
+ alreadySubmittedMessage: "Already submitted.",
3282
+ serverErrorSummary: "Submission failed. Please check your answers and try again.",
3283
+ confirmSensitiveDataTitle: "Sensitive data may be included",
3284
+ confirmSensitiveDataMessage: "The following answers may contain personal information. Continue submitting?",
3285
+ confirmButton: "Proceed",
3286
+ cancelButton: "Cancel"
3287
+ },
3288
+ ja: {
3289
+ submitButton: "\u9001\u4FE1\u3059\u308B",
3290
+ submittingButton: "\u9001\u4FE1\u4E2D...",
3291
+ retryButton: "\u518D\u9001\u4FE1\u3059\u308B",
3292
+ requiredField: "\u3053\u306E\u9805\u76EE\u306F\u5FC5\u9808\u3067\u3059",
3293
+ alreadySubmittedTitle: "\u56DE\u7B54\u6E08\u307F\u3067\u3059",
3294
+ alreadySubmittedMessage: "\u3053\u306E\u30A2\u30F3\u30B1\u30FC\u30C8\u306B\u306F\u3059\u3067\u306B\u56DE\u7B54\u3057\u3066\u3044\u307E\u3059\u3002",
3295
+ serverErrorSummary: "\u9001\u4FE1\u306B\u5931\u6557\u3057\u307E\u3057\u305F\u3002\u5185\u5BB9\u3092\u3054\u78BA\u8A8D\u306E\u4E0A\u3001\u518D\u5EA6\u304A\u8A66\u3057\u304F\u3060\u3055\u3044\u3002",
3296
+ confirmSensitiveDataTitle: "\u500B\u4EBA\u60C5\u5831\u304C\u542B\u307E\u308C\u3066\u3044\u308B\u53EF\u80FD\u6027\u304C\u3042\u308A\u307E\u3059",
3297
+ confirmSensitiveDataMessage: "\u4EE5\u4E0B\u306E\u9805\u76EE\u306B\u500B\u4EBA\u60C5\u5831\u3068\u307F\u3089\u308C\u308B\u8A18\u8FF0\u304C\u3042\u308A\u307E\u3059\u3002\u3053\u306E\u307E\u307E\u9001\u4FE1\u3057\u3066\u3082\u3088\u308D\u3057\u3044\u3067\u3059\u304B\uFF1F",
3298
+ confirmButton: "\u3053\u306E\u307E\u307E\u9001\u4FE1",
3299
+ cancelButton: "\u4FEE\u6B63\u3059\u308B"
3300
+ }
3301
+ };
3302
+ function createRendererAttemptId() {
3303
+ const randomUuid = globalThis.crypto?.randomUUID;
3304
+ if (typeof randomUuid === "function") return randomUuid.call(globalThis.crypto);
3305
+ return `attempt-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
3306
+ }
3307
+ function maskSensitiveValue(finding) {
3308
+ if (finding.maskedText !== void 0) return finding.maskedText;
3309
+ const value = finding.matchedText;
3310
+ if (value === void 0) return void 0;
3311
+ if (finding.type === "email") {
3312
+ const separator = value.indexOf("@");
3313
+ if (separator > 0) return `${value.slice(0, Math.min(2, separator))}***${value.slice(separator)}`;
3314
+ }
3315
+ if (finding.type === "phone" || finding.type === "postal_code") return "***";
3316
+ return value.length <= 2 ? "***" : `${value.slice(0, 2)}***`;
3317
+ }
3167
3318
  function ContextFormRenderer({
3168
3319
  components = {},
3169
3320
  className = "",
3170
3321
  successMessageKey,
3171
3322
  errorMessageKey,
3323
+ attemptIdFactory,
3324
+ messages = {},
3325
+ messageResolver,
3172
3326
  autoSaveKey,
3173
3327
  beforeSubmit,
3174
3328
  onDraftSave,
@@ -3197,6 +3351,7 @@ function ContextFormRenderer({
3197
3351
  const [completionData, setCompletionData] = (0, import_react5.useState)(null);
3198
3352
  const [receiptLoaded, setReceiptLoaded] = (0, import_react5.useState)(receiptStore === void 0);
3199
3353
  const rendererSubmissionInFlight = (0, import_react5.useRef)(false);
3354
+ const fallbackAttemptId = (0, import_react5.useRef)(null);
3200
3355
  const completionRef = (0, import_react5.useRef)(null);
3201
3356
  const confirmationRef = (0, import_react5.useRef)(null);
3202
3357
  const pages = form.schema.pages;
@@ -3211,6 +3366,18 @@ function ContextFormRenderer({
3211
3366
  const submitState = confirmation === null && !guardsPending ? form.submitStatus : "confirming";
3212
3367
  const interactionLocked = submitState === "confirming" || submitState === "submitting";
3213
3368
  const isReplaceMode = successRenderMode === "replace" || hideFormOnSuccess;
3369
+ const resolveMessage = (0, import_react5.useCallback)(
3370
+ (key, fallback) => {
3371
+ const defaultText = fallback ?? DEFAULT_RENDERER_MESSAGES[form.locale.toLowerCase().startsWith("ja") ? "ja" : "en"][key] ?? key;
3372
+ const configured = messages[key];
3373
+ return messageResolver?.(key, configured ?? defaultText) ?? configured ?? defaultText;
3374
+ },
3375
+ [form.locale, messageResolver, messages]
3376
+ );
3377
+ const fieldTranslate = (0, import_react5.useCallback)(
3378
+ (key, params) => key === "validation.required" && (messages.requiredField !== void 0 || messageResolver !== void 0) ? resolveMessage("requiredField") : form.translate(key, params),
3379
+ [form.translate, messageResolver, messages.requiredField, resolveMessage]
3380
+ );
3214
3381
  const focusSubmitButton = (0, import_react5.useCallback)(() => {
3215
3382
  const button = formRef.current?.querySelector(".fe-submit, button[type='submit'], button");
3216
3383
  button?.focus();
@@ -3250,6 +3417,7 @@ function ContextFormRenderer({
3250
3417
  );
3251
3418
  const control = fieldContainer?.querySelector("input, select, textarea");
3252
3419
  if (control !== void 0 && control !== null) {
3420
+ control.scrollIntoView?.({ behavior: "smooth", block: "center" });
3253
3421
  control.focus();
3254
3422
  setFocusFieldId(null);
3255
3423
  }
@@ -3268,6 +3436,23 @@ function ContextFormRenderer({
3268
3436
  event.preventDefault();
3269
3437
  setConfirmation(null);
3270
3438
  globalThis.setTimeout(focusSubmitButton, 0);
3439
+ return;
3440
+ }
3441
+ if (event.key !== "Tab") return;
3442
+ const focusable = [
3443
+ ...confirmationRef.current?.querySelectorAll(
3444
+ "button, [href], input, select, textarea, [tabindex]:not([tabindex='-1'])"
3445
+ ) ?? []
3446
+ ].filter((element) => !element.hasAttribute("disabled"));
3447
+ if (focusable.length === 0) return;
3448
+ const first = focusable[0];
3449
+ const last = focusable[focusable.length - 1];
3450
+ if (event.shiftKey && document.activeElement === first) {
3451
+ event.preventDefault();
3452
+ last?.focus();
3453
+ } else if (!event.shiftKey && document.activeElement === last) {
3454
+ event.preventDefault();
3455
+ first?.focus();
3271
3456
  }
3272
3457
  };
3273
3458
  globalThis.addEventListener("keydown", onKeyDown);
@@ -3369,17 +3554,24 @@ function ContextFormRenderer({
3369
3554
  rendererSubmissionInFlight.current = true;
3370
3555
  try {
3371
3556
  let submissionAttempt;
3372
- const result = await form.submit(
3373
- beforeSubmit,
3374
- attemptStore === void 0 ? void 0 : async (values) => {
3375
- submissionAttempt = await attemptStore.getOrCreate(form.schema.id, form.schema.version);
3376
- return {
3377
- ...values,
3378
- attemptId: submissionAttempt.attemptId,
3379
- submissionId: submissionAttempt.attemptId
3380
- };
3381
- }
3382
- );
3557
+ let attemptId = fallbackAttemptId.current;
3558
+ if (attemptStore !== void 0) {
3559
+ submissionAttempt = await attemptStore.getOrCreate(form.schema.id, form.schema.version, attemptIdFactory);
3560
+ attemptId = submissionAttempt.attemptId;
3561
+ } else if (attemptId === null) {
3562
+ attemptId = attemptIdFactory?.() ?? createRendererAttemptId();
3563
+ fallbackAttemptId.current = attemptId;
3564
+ }
3565
+ if (attemptId === null) throw new Error("Unable to create a submission attempt id.");
3566
+ const submittedAt = (/* @__PURE__ */ new Date()).toISOString();
3567
+ const submitContext = {
3568
+ attemptId,
3569
+ formId: form.schema.id,
3570
+ formVersion: form.schema.version,
3571
+ locale: form.locale,
3572
+ submittedAt
3573
+ };
3574
+ const result = await form.submit(beforeSubmit, submitContext);
3383
3575
  if (result.status === "invalid") {
3384
3576
  const invalidPageIndex = pages?.findIndex(
3385
3577
  (page) => firstInvalidFieldId === void 0 ? false : page.questionIds.includes(firstInvalidFieldId)
@@ -3392,7 +3584,7 @@ function ContextFormRenderer({
3392
3584
  if (result.error instanceof FormSubmissionError) {
3393
3585
  const fieldErrors = result.error.payload.fieldErrors ?? {};
3394
3586
  form.setServerErrors?.(fieldErrors);
3395
- const firstServerFieldId = Object.keys(fieldErrors)[0];
3587
+ const firstServerFieldId = form.schema.fields.find((field) => Object.hasOwn(fieldErrors, field.id))?.id ?? Object.keys(fieldErrors)[0];
3396
3588
  if (firstServerFieldId !== void 0) {
3397
3589
  const invalidPageIndex = pages?.findIndex((page) => page.questionIds.includes(firstServerFieldId));
3398
3590
  if (invalidPageIndex !== void 0 && invalidPageIndex >= 0) setCurrentPageIndex(invalidPageIndex);
@@ -3417,11 +3609,11 @@ function ContextFormRenderer({
3417
3609
  });
3418
3610
  if (receiptStore !== void 0) {
3419
3611
  const response = result.response;
3420
- const submissionId = response?.submissionId ?? submissionAttempt?.attemptId;
3612
+ const submissionId = response?.submissionId ?? submissionAttempt?.attemptId ?? attemptId;
3421
3613
  const storedReceipt = {
3422
3614
  formId: form.schema.id,
3423
3615
  formVersion: form.schema.version,
3424
- submittedAt: response?.submittedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
3616
+ submittedAt: response?.submittedAt ?? submittedAt,
3425
3617
  ...submissionId === void 0 ? {} : { submissionId }
3426
3618
  };
3427
3619
  try {
@@ -3440,6 +3632,7 @@ function ContextFormRenderer({
3440
3632
  } catch {
3441
3633
  }
3442
3634
  }
3635
+ if (attemptStore === void 0) fallbackAttemptId.current = null;
3443
3636
  if (autoSaveKey !== void 0 && typeof globalThis.localStorage !== "undefined") {
3444
3637
  globalThis.localStorage.removeItem(autoSaveKey);
3445
3638
  setDraftRestored(false);
@@ -3479,7 +3672,10 @@ function ContextFormRenderer({
3479
3672
  disabled: interactionLocked || submitState === "success",
3480
3673
  onSubmit: () => void submitValues()
3481
3674
  };
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") });
3675
+ return slots.renderSubmitButton?.(submitButtonProps) ?? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("button", { className: "fe-submit", type: "submit", disabled: submitButtonProps.disabled, children: [
3676
+ submitState === "submitting" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "fe-spinner", "aria-hidden": "true" }) : null,
3677
+ submitState === "submitting" ? resolveMessage("submittingButton") : resolveMessage("submitButton", form.translate(form.schema.submitLabelKey ?? "form.submit"))
3678
+ ] });
3483
3679
  };
3484
3680
  const completionMessage = form.schema.completionMessage ?? (successMessageKey === void 0 ? "Submitted." : form.translate(successMessageKey));
3485
3681
  const activeCompletionData = completionData ?? {
@@ -3512,15 +3708,31 @@ function ContextFormRenderer({
3512
3708
  role: submissionConfirmationRenderMode === "dialog" ? void 0 : "dialog",
3513
3709
  children: slots.renderSubmissionConfirmation?.({
3514
3710
  findings: confirmation?.findings ?? [],
3515
- message: confirmation?.message ?? form.translate("form.confirmSensitiveData"),
3711
+ message: confirmation?.message ?? resolveMessage("confirmSensitiveDataMessage", form.translate("form.confirmSensitiveData")),
3516
3712
  schema: form.schema,
3517
3713
  visibleValues,
3518
3714
  onConfirm: confirmSubmission,
3519
3715
  onCancel: cancelSubmission
3520
3716
  }) ?? /* @__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") })
3717
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h2", { children: resolveMessage("confirmSensitiveDataTitle") }),
3718
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { children: confirmation?.message ?? resolveMessage("confirmSensitiveDataMessage", form.translate("form.confirmSensitiveData")) }),
3719
+ (confirmation?.findings ?? []).length === 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("ul", { children: (confirmation?.findings ?? []).map((finding, index) => {
3720
+ const field = form.schema.fields.find((candidate) => candidate.id === finding.fieldId);
3721
+ const typeLabels = form.locale.toLowerCase().startsWith("ja") ? { email: "\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9", phone: "\u96FB\u8A71\u756A\u53F7", url: "URL", postal_code: "\u90F5\u4FBF\u756A\u53F7" } : { email: "Email address", phone: "Phone number", url: "URL", postal_code: "Postal code" };
3722
+ const typeLabel = finding.typeLabel ?? typeLabels[finding.type] ?? finding.type;
3723
+ const value = maskSensitiveValue(finding);
3724
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("li", { children: [
3725
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: finding.fieldTitle ?? field?.title ?? finding.fieldId }),
3726
+ " ",
3727
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "fe-sensitive-type", children: typeLabel }),
3728
+ value === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "fe-sensitive-value", children: [
3729
+ " ",
3730
+ value
3731
+ ] })
3732
+ ] }, `${finding.fieldId}-${finding.type}-${finding.start ?? index}`);
3733
+ }) }),
3734
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", "data-fe-confirm": "true", onClick: confirmSubmission, children: resolveMessage("confirmButton", form.translate("form.confirmSubmission")) }),
3735
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", onClick: cancelSubmission, children: resolveMessage("cancelButton", form.translate("form.cancelSubmission")) })
3524
3736
  ] })
3525
3737
  }
3526
3738
  );
@@ -3530,8 +3742,9 @@ function ContextFormRenderer({
3530
3742
  receipt,
3531
3743
  ...receiptStore === void 0 ? {} : { onReset: () => void resetReceipt() }
3532
3744
  }) ?? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { role: "status", children: [
3533
- form.translate("form.alreadySubmitted"),
3534
- receiptStore === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", onClick: () => void resetReceipt(), children: form.translate("form.submitAnother") })
3745
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h2", { children: resolveMessage("alreadySubmittedTitle") }),
3746
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { children: resolveMessage("alreadySubmittedMessage", form.translate("form.alreadySubmitted")) }),
3747
+ receiptStore === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", onClick: () => void resetReceipt(), children: resolveMessage("submitButton", form.translate("form.submitAnother")) })
3535
3748
  ] }) });
3536
3749
  }
3537
3750
  if (form.submitStatus === "success" && isReplaceMode) {
@@ -3594,7 +3807,7 @@ function ContextFormRenderer({
3594
3807
  value: form.values[field.id],
3595
3808
  error,
3596
3809
  setValue: (value) => form.setValue(field.id, value),
3597
- translate: form.translate,
3810
+ translate: fieldTranslate,
3598
3811
  inputId: `${prefix}-${field.id}`,
3599
3812
  errorId: `${prefix}-${field.id}-error`,
3600
3813
  helpId: `${prefix}-${field.id}-help`,
@@ -3661,7 +3874,13 @@ function ContextFormRenderer({
3661
3874
  ] }),
3662
3875
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "fe-status", "aria-live": "polite", children: [
3663
3876
  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
3877
+ 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.jsxs)("div", { role: "alert", children: [
3878
+ form.submitError.payload.formError,
3879
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", onClick: () => void submitValues(), children: resolveMessage("retryButton") })
3880
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { role: "alert", children: [
3881
+ errorMessageKey === void 0 ? resolveMessage("serverErrorSummary") : form.translate(errorMessageKey),
3882
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", onClick: () => void submitValues(), children: resolveMessage("retryButton") })
3883
+ ] })) : null
3665
3884
  ] })
3666
3885
  ]
3667
3886
  }
@@ -3677,7 +3896,7 @@ var RENDERER_MESSAGES = {
3677
3896
  "form.draftRestored": "Draft restored",
3678
3897
  "form.submissionBlocked": "Submission blocked because sensitive data was detected.",
3679
3898
  "form.confirmSensitiveData": "Sensitive data may be included. Confirm before submitting.",
3680
- "form.confirmSubmission": "Confirm submission",
3899
+ "form.confirmSubmission": "Proceed",
3681
3900
  "form.cancelSubmission": "Cancel",
3682
3901
  "form.yes": "Yes",
3683
3902
  "form.no": "No",
@@ -3685,9 +3904,26 @@ var RENDERER_MESSAGES = {
3685
3904
  "form.submitAnother": "Submit another response",
3686
3905
  "validation.required": "This field is required."
3687
3906
  };
3907
+ var RENDERER_MESSAGES_JA = {
3908
+ "form.submit": "\u9001\u4FE1\u3059\u308B",
3909
+ "form.back": "\u623B\u308B",
3910
+ "form.next": "\u6B21\u3078",
3911
+ "form.step": "{{current}} / {{total}}",
3912
+ "form.draftRestored": "\u4E0B\u66F8\u304D\u3092\u5FA9\u5143\u3057\u307E\u3057\u305F",
3913
+ "form.submissionBlocked": "\u500B\u4EBA\u60C5\u5831\u304C\u691C\u51FA\u3055\u308C\u305F\u305F\u3081\u9001\u4FE1\u3067\u304D\u307E\u305B\u3093\u3002",
3914
+ "form.confirmSensitiveData": "\u500B\u4EBA\u60C5\u5831\u304C\u542B\u307E\u308C\u3066\u3044\u308B\u53EF\u80FD\u6027\u304C\u3042\u308A\u307E\u3059\u3002\u9001\u4FE1\u524D\u306B\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
3915
+ "form.confirmSubmission": "\u3053\u306E\u307E\u307E\u9001\u4FE1",
3916
+ "form.cancelSubmission": "\u4FEE\u6B63\u3059\u308B",
3917
+ "form.yes": "\u306F\u3044",
3918
+ "form.no": "\u3044\u3044\u3048",
3919
+ "form.alreadySubmitted": "\u56DE\u7B54\u6E08\u307F\u3067\u3059",
3920
+ "form.submitAnother": "\u5225\u306E\u56DE\u7B54\u3092\u9001\u4FE1",
3921
+ "validation.required": "\u3053\u306E\u9805\u76EE\u306F\u5FC5\u9808\u3067\u3059"
3922
+ };
3688
3923
  var defaultRendererTranslator = {
3689
- translate(key, _locale, params = {}) {
3690
- return (RENDERER_MESSAGES[key] ?? key).replace(
3924
+ translate(key, locale, params = {}) {
3925
+ const localizedMessages = locale.toLowerCase().startsWith("ja") ? RENDERER_MESSAGES_JA : RENDERER_MESSAGES;
3926
+ return (localizedMessages[key] ?? key).replace(
3691
3927
  /\{\{(\w+)\}\}/g,
3692
3928
  (token, name) => Object.hasOwn(params, name) ? String(params[name]) : token
3693
3929
  );
@@ -3719,6 +3955,8 @@ function FormRenderer(props) {
3719
3955
  }
3720
3956
  // Annotate the CommonJS export names for ESM import in node:
3721
3957
  0 && (module.exports = {
3958
+ BUILDER_TRANSLATION_ALIASES,
3959
+ BUILDER_TRANSLATION_KEYS,
3722
3960
  FormBuilder,
3723
3961
  FormProvider,
3724
3962
  FormRenderer,