@form-engine-ts/react 4.3.1 → 4.4.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/README.md +22 -0
- package/dist/index.cjs +217 -4
- package/dist/index.d.cts +45 -3
- package/dist/index.d.ts +45 -3
- package/dist/index.js +216 -4
- package/dist/styles.css +46 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -39,6 +39,28 @@ export function ContactForm() {
|
|
|
39
39
|
|
|
40
40
|
Use any compatible `TranslationAdapter` in place of the mock translator.
|
|
41
41
|
|
|
42
|
+
## Choice field layout
|
|
43
|
+
|
|
44
|
+
Radio, checkbox, and multi-select questions use the flat layout by default. Set `appearance.choiceField` to `"grouped"`
|
|
45
|
+
to render them as bordered, accessible `<fieldset>` groups with `<legend>` titles:
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
<FormRenderer appearance={{ choiceField: "grouped" }} />
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Use an object to configure each choice type independently. Unspecified types remain flat:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
<FormRenderer appearance={{ choiceField: { radio: "grouped", checkbox: "default" } }} />
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Grouped wrappers can be replaced with `slots.renderChoiceGroup`; `slotProps.choiceGroup` accepts a class name and
|
|
58
|
+
inline style for the default wrapper. The exported `ChoiceGroupSlotProps` includes the field, translated error, and
|
|
59
|
+
option-list children. Group styling is controlled by the public `--fe-choice-group-*`, `--fe-choice-legend-*`, and
|
|
60
|
+
`--fe-choice-options-gap` CSS custom properties.
|
|
61
|
+
|
|
62
|
+
`groupedChoiceFields={true}` remains available as a deprecated compatibility alias.
|
|
63
|
+
|
|
42
64
|
Define `schema.pages` to enable Back/Next navigation, page validation, conditional page skipping, and an accessible
|
|
43
65
|
progress indicator. Pass `autoSaveKey` to persist a versioned draft in `localStorage` after a 500ms debounce and restore it
|
|
44
66
|
on the next mount:
|
package/dist/index.cjs
CHANGED
|
@@ -29,6 +29,7 @@ __export(index_exports, {
|
|
|
29
29
|
createLocalStorageSubmissionAttemptStore: () => createLocalStorageSubmissionAttemptStore,
|
|
30
30
|
createLocalStorageSubmissionReceiptStore: () => createLocalStorageSubmissionReceiptStore,
|
|
31
31
|
isTranslationUnresolved: () => isTranslationUnresolved,
|
|
32
|
+
resolveChoiceFieldLayout: () => resolveChoiceFieldLayout,
|
|
32
33
|
resolveFieldEditorControls: () => resolveFieldEditorControls,
|
|
33
34
|
resolveFieldTypeSelectOptions: () => resolveFieldTypeSelectOptions,
|
|
34
35
|
resolveInitialFieldType: () => resolveInitialFieldType,
|
|
@@ -3270,14 +3271,25 @@ function useSubmissionReceipts(store, queries) {
|
|
|
3270
3271
|
var import_core4 = require("@form-engine-ts/core");
|
|
3271
3272
|
var import_react5 = require("react");
|
|
3272
3273
|
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
3274
|
+
var isChoiceFieldType = (type) => type === "radio" || type === "checkbox" || type === "multi-select" || type === "select";
|
|
3275
|
+
function resolveChoiceFieldLayout(type, appearance, groupedChoiceFieldsLegacy = false) {
|
|
3276
|
+
if (groupedChoiceFieldsLegacy) return "grouped";
|
|
3277
|
+
const config = appearance?.choiceField;
|
|
3278
|
+
if (config === void 0) return "default";
|
|
3279
|
+
if (typeof config === "string") return config;
|
|
3280
|
+
return config[type] ?? "default";
|
|
3281
|
+
}
|
|
3273
3282
|
function describedBy(field, error, helpId, errorId) {
|
|
3274
3283
|
const ids = [field.description === void 0 ? void 0 : helpId, error === void 0 ? void 0 : errorId].filter(
|
|
3275
3284
|
Boolean
|
|
3276
3285
|
);
|
|
3277
3286
|
return ids.length === 0 ? void 0 : ids.join(" ");
|
|
3278
3287
|
}
|
|
3279
|
-
function RequiredMark({
|
|
3280
|
-
|
|
3288
|
+
function RequiredMark({
|
|
3289
|
+
required,
|
|
3290
|
+
className = "fe-required"
|
|
3291
|
+
}) {
|
|
3292
|
+
return required ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className, "aria-hidden": "true", children: [
|
|
3281
3293
|
" ",
|
|
3282
3294
|
"*"
|
|
3283
3295
|
] }) : null;
|
|
@@ -3288,13 +3300,106 @@ function FieldMessage({ props }) {
|
|
|
3288
3300
|
props.error === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { id: props.errorId, className: "fe-error", children: props.translate(props.error.messageKey, props.error.params) })
|
|
3289
3301
|
] });
|
|
3290
3302
|
}
|
|
3291
|
-
function
|
|
3303
|
+
function GroupedChoiceDescription({ props }) {
|
|
3304
|
+
return props.field.description === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { id: props.helpId, className: "fe-field-description", children: props.field.description });
|
|
3305
|
+
}
|
|
3306
|
+
function GroupedChoiceError({ props }) {
|
|
3307
|
+
return props.error === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { id: props.errorId, className: "fe-field-error", role: "alert", children: props.translate(props.error.messageKey, props.error.params) });
|
|
3308
|
+
}
|
|
3309
|
+
function ChoiceGroupFrame({
|
|
3310
|
+
props,
|
|
3311
|
+
children,
|
|
3312
|
+
className,
|
|
3313
|
+
slotProps,
|
|
3314
|
+
renderChoiceGroup,
|
|
3315
|
+
disabled,
|
|
3316
|
+
readOnly
|
|
3317
|
+
}) {
|
|
3318
|
+
const { field, error, translate } = props;
|
|
3319
|
+
const groupError = error === void 0 ? void 0 : { ...error, message: translate(error.messageKey, error.params) };
|
|
3320
|
+
const groupProps = {
|
|
3321
|
+
field,
|
|
3322
|
+
title: field.title,
|
|
3323
|
+
...field.description === void 0 ? {} : { description: field.description },
|
|
3324
|
+
...field.required === void 0 ? {} : { required: field.required },
|
|
3325
|
+
...groupError === void 0 ? {} : { error: groupError },
|
|
3326
|
+
...disabled === void 0 ? {} : { disabled },
|
|
3327
|
+
...readOnly === void 0 ? {} : { readOnly },
|
|
3328
|
+
children,
|
|
3329
|
+
className
|
|
3330
|
+
};
|
|
3331
|
+
if (renderChoiceGroup !== void 0) return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_jsx_runtime3.Fragment, { children: renderChoiceGroup(groupProps) });
|
|
3332
|
+
return (
|
|
3333
|
+
// biome-ignore lint/a11y/useAriaPropsSupportedByRole: The grouped fieldset exposes the required state for the complete choice question.
|
|
3334
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
3335
|
+
"fieldset",
|
|
3336
|
+
{
|
|
3337
|
+
className,
|
|
3338
|
+
"data-field-id": field.id,
|
|
3339
|
+
disabled,
|
|
3340
|
+
"aria-describedby": describedBy(field, error, props.helpId, props.errorId),
|
|
3341
|
+
"aria-invalid": Boolean(error),
|
|
3342
|
+
"aria-required": field.required,
|
|
3343
|
+
style: slotProps?.style,
|
|
3344
|
+
children: [
|
|
3345
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("legend", { className: "fe-choice-legend", children: [
|
|
3346
|
+
field.title,
|
|
3347
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(RequiredMark, { required: field.required, className: "fe-required-badge" })
|
|
3348
|
+
] }),
|
|
3349
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(GroupedChoiceDescription, { props }),
|
|
3350
|
+
children,
|
|
3351
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(GroupedChoiceError, { props })
|
|
3352
|
+
]
|
|
3353
|
+
}
|
|
3354
|
+
)
|
|
3355
|
+
);
|
|
3356
|
+
}
|
|
3357
|
+
function DefaultField({
|
|
3358
|
+
groupedChoiceFields,
|
|
3359
|
+
appearance,
|
|
3360
|
+
choiceGroupSlotProps,
|
|
3361
|
+
renderChoiceGroup,
|
|
3362
|
+
disabled,
|
|
3363
|
+
readOnly,
|
|
3364
|
+
...props
|
|
3365
|
+
}) {
|
|
3292
3366
|
const { field, value, setValue, inputId, error, translate } = props;
|
|
3367
|
+
const isGroupedChoiceField = isChoiceFieldType(field.type) && resolveChoiceFieldLayout(field.type, appearance, groupedChoiceFields) === "grouped";
|
|
3368
|
+
const choiceGroupClassName = ["fe-choice-group", `fe-field--${field.type}`, choiceGroupSlotProps?.className].filter((item) => item !== void 0 && item.length > 0).join(" ");
|
|
3293
3369
|
const ariaProps = {
|
|
3294
3370
|
"aria-describedby": describedBy(field, error, props.helpId, props.errorId),
|
|
3295
3371
|
"aria-invalid": error === void 0 ? void 0 : true
|
|
3296
3372
|
};
|
|
3297
3373
|
if (field.type === "checkbox") {
|
|
3374
|
+
if (isGroupedChoiceField) {
|
|
3375
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
3376
|
+
ChoiceGroupFrame,
|
|
3377
|
+
{
|
|
3378
|
+
props,
|
|
3379
|
+
className: choiceGroupClassName,
|
|
3380
|
+
slotProps: choiceGroupSlotProps,
|
|
3381
|
+
renderChoiceGroup,
|
|
3382
|
+
disabled,
|
|
3383
|
+
readOnly,
|
|
3384
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "fe-choice-options", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { className: "fe-choice-option", htmlFor: inputId, children: [
|
|
3385
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
3386
|
+
"input",
|
|
3387
|
+
{
|
|
3388
|
+
id: inputId,
|
|
3389
|
+
name: field.id,
|
|
3390
|
+
type: "checkbox",
|
|
3391
|
+
checked: value === true,
|
|
3392
|
+
"aria-label": field.title,
|
|
3393
|
+
disabled,
|
|
3394
|
+
readOnly,
|
|
3395
|
+
onChange: (event) => setValue(event.currentTarget.checked)
|
|
3396
|
+
}
|
|
3397
|
+
),
|
|
3398
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: field.title })
|
|
3399
|
+
] }) })
|
|
3400
|
+
}
|
|
3401
|
+
);
|
|
3402
|
+
}
|
|
3298
3403
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "fe-field fe-field--checkbox", "data-field-id": field.id, children: [
|
|
3299
3404
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { className: "fe-check-label", htmlFor: inputId, children: [
|
|
3300
3405
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
@@ -3318,6 +3423,84 @@ function DefaultField(props) {
|
|
|
3318
3423
|
}
|
|
3319
3424
|
if (field.type === "radio" || field.type === "multi-select") {
|
|
3320
3425
|
const selected = Array.isArray(value) ? value : [];
|
|
3426
|
+
if (isGroupedChoiceField) {
|
|
3427
|
+
const isRadio = field.type === "radio";
|
|
3428
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
3429
|
+
ChoiceGroupFrame,
|
|
3430
|
+
{
|
|
3431
|
+
props,
|
|
3432
|
+
className: choiceGroupClassName,
|
|
3433
|
+
slotProps: choiceGroupSlotProps,
|
|
3434
|
+
renderChoiceGroup,
|
|
3435
|
+
disabled,
|
|
3436
|
+
readOnly,
|
|
3437
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "fe-choice-options", children: field.options.map((option, index) => {
|
|
3438
|
+
const optionId = `${inputId}-${index}`;
|
|
3439
|
+
const checked = isRadio ? value === option.id : selected.includes(option.id);
|
|
3440
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { className: "fe-choice-option", htmlFor: optionId, children: [
|
|
3441
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
3442
|
+
"input",
|
|
3443
|
+
{
|
|
3444
|
+
id: optionId,
|
|
3445
|
+
name: field.id,
|
|
3446
|
+
type: isRadio ? "radio" : "checkbox",
|
|
3447
|
+
value: option.id,
|
|
3448
|
+
checked,
|
|
3449
|
+
disabled,
|
|
3450
|
+
readOnly,
|
|
3451
|
+
onKeyDown: isRadio ? (event) => {
|
|
3452
|
+
if (event.key !== "ArrowDown" && event.key !== "ArrowRight" && event.key !== "ArrowUp" && event.key !== "ArrowLeft")
|
|
3453
|
+
return;
|
|
3454
|
+
event.preventDefault();
|
|
3455
|
+
const offset = event.key === "ArrowDown" || event.key === "ArrowRight" ? 1 : -1;
|
|
3456
|
+
const nextIndex = (index + offset + field.options.length) % field.options.length;
|
|
3457
|
+
const nextOption = field.options[nextIndex];
|
|
3458
|
+
if (nextOption === void 0) return;
|
|
3459
|
+
setValue(nextOption.id);
|
|
3460
|
+
document.getElementById(`${inputId}-${nextIndex}`)?.focus();
|
|
3461
|
+
} : void 0,
|
|
3462
|
+
onChange: (event) => {
|
|
3463
|
+
if (isRadio) setValue(option.id);
|
|
3464
|
+
else
|
|
3465
|
+
setValue(
|
|
3466
|
+
event.currentTarget.checked ? [...selected, option.id] : selected.filter((item) => item !== option.id)
|
|
3467
|
+
);
|
|
3468
|
+
}
|
|
3469
|
+
}
|
|
3470
|
+
),
|
|
3471
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: option.label })
|
|
3472
|
+
] }, option.id);
|
|
3473
|
+
}) })
|
|
3474
|
+
}
|
|
3475
|
+
);
|
|
3476
|
+
}
|
|
3477
|
+
if (field.type === "radio") {
|
|
3478
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "fe-field fe-field--radio", "data-field-id": field.id, children: [
|
|
3479
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "fe-label", children: [
|
|
3480
|
+
field.title,
|
|
3481
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(RequiredMark, { required: field.required })
|
|
3482
|
+
] }),
|
|
3483
|
+
field.options.map((option, index) => {
|
|
3484
|
+
const optionId = `${inputId}-${index}`;
|
|
3485
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { className: "fe-check-label", htmlFor: optionId, children: [
|
|
3486
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
3487
|
+
"input",
|
|
3488
|
+
{
|
|
3489
|
+
...ariaProps,
|
|
3490
|
+
id: optionId,
|
|
3491
|
+
name: field.id,
|
|
3492
|
+
type: "radio",
|
|
3493
|
+
value: option.id,
|
|
3494
|
+
checked: value === option.id,
|
|
3495
|
+
onChange: () => setValue(option.id)
|
|
3496
|
+
}
|
|
3497
|
+
),
|
|
3498
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: option.label })
|
|
3499
|
+
] }, option.id);
|
|
3500
|
+
}),
|
|
3501
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(FieldMessage, { props })
|
|
3502
|
+
] });
|
|
3503
|
+
}
|
|
3321
3504
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("fieldset", { className: `fe-field fe-field--${field.type}`, "data-field-id": field.id, ...ariaProps, children: [
|
|
3322
3505
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("legend", { className: "fe-label", children: [
|
|
3323
3506
|
field.title,
|
|
@@ -3425,6 +3608,7 @@ function DefaultField(props) {
|
|
|
3425
3608
|
...ariaProps,
|
|
3426
3609
|
id: inputId,
|
|
3427
3610
|
name: field.id,
|
|
3611
|
+
disabled,
|
|
3428
3612
|
value: typeof value === "string" ? value : "",
|
|
3429
3613
|
onChange: (event) => setValue(event.currentTarget.value || void 0),
|
|
3430
3614
|
children: [
|
|
@@ -3448,6 +3632,20 @@ function DefaultField(props) {
|
|
|
3448
3632
|
}
|
|
3449
3633
|
);
|
|
3450
3634
|
}
|
|
3635
|
+
if (isGroupedChoiceField) {
|
|
3636
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
3637
|
+
ChoiceGroupFrame,
|
|
3638
|
+
{
|
|
3639
|
+
props,
|
|
3640
|
+
className: choiceGroupClassName,
|
|
3641
|
+
slotProps: choiceGroupSlotProps,
|
|
3642
|
+
renderChoiceGroup,
|
|
3643
|
+
disabled,
|
|
3644
|
+
readOnly,
|
|
3645
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "fe-choice-options", children: control })
|
|
3646
|
+
}
|
|
3647
|
+
);
|
|
3648
|
+
}
|
|
3451
3649
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: `fe-field fe-field--${field.type}`, "data-field-id": field.id, children: [
|
|
3452
3650
|
label,
|
|
3453
3651
|
control,
|
|
@@ -3567,6 +3765,9 @@ function ContextFormRenderer({
|
|
|
3567
3765
|
beforeSubmit,
|
|
3568
3766
|
onDraftSave,
|
|
3569
3767
|
successRenderMode = "append",
|
|
3768
|
+
appearance,
|
|
3769
|
+
slotProps,
|
|
3770
|
+
groupedChoiceFields = false,
|
|
3570
3771
|
submissionConfirmation,
|
|
3571
3772
|
submissionConfirmationRenderMode,
|
|
3572
3773
|
showHiddenFieldsInSummary = false,
|
|
@@ -4085,7 +4286,18 @@ function ContextFormRenderer({
|
|
|
4085
4286
|
}) }, field.id);
|
|
4086
4287
|
}
|
|
4087
4288
|
const Component = components[field.type];
|
|
4088
|
-
return Component === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
4289
|
+
return Component === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
4290
|
+
DefaultField,
|
|
4291
|
+
{
|
|
4292
|
+
...props,
|
|
4293
|
+
groupedChoiceFields,
|
|
4294
|
+
appearance,
|
|
4295
|
+
choiceGroupSlotProps: slotProps?.choiceGroup,
|
|
4296
|
+
renderChoiceGroup: slots.renderChoiceGroup,
|
|
4297
|
+
disabled: interactionLocked
|
|
4298
|
+
},
|
|
4299
|
+
field.id
|
|
4300
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Component, { ...props }, field.id);
|
|
4089
4301
|
});
|
|
4090
4302
|
const fieldClassName = `fe-fields${fieldsClassName === void 0 ? "" : ` ${fieldsClassName}`}`;
|
|
4091
4303
|
return slots.renderFields?.({ children: fieldChildren, className: fieldClassName }) ?? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: fieldClassName, children: fieldChildren });
|
|
@@ -4225,6 +4437,7 @@ function FormRenderer(props) {
|
|
|
4225
4437
|
createLocalStorageSubmissionAttemptStore,
|
|
4226
4438
|
createLocalStorageSubmissionReceiptStore,
|
|
4227
4439
|
isTranslationUnresolved,
|
|
4440
|
+
resolveChoiceFieldLayout,
|
|
4228
4441
|
resolveFieldEditorControls,
|
|
4229
4442
|
resolveFieldTypeSelectOptions,
|
|
4230
4443
|
resolveInitialFieldType,
|
package/dist/index.d.cts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { QuestionType, FormField, ChoiceOption, FormPage, FormSchema, DisplayCondition, JsonValue, SchemaIssue, FormPolicy, Question, FieldOption, TranslationReport, ValidationError, FormValues, TranslationAdapter, AsyncTranslationAdapter, PopulateTranslationOptions, FormValue,
|
|
1
|
+
import { QuestionType, FormField, ChoiceOption, FormPage, FormSchema, DisplayCondition, JsonValue, SchemaIssue, FormPolicy, Question, FieldOption, TranslationReport, ValidationIssue, ValidationError, FormValues, TranslationAdapter, AsyncTranslationAdapter, PopulateTranslationOptions, FormValue, AnswerValidationResult, FieldType } from '@form-engine-ts/core';
|
|
2
2
|
export { QuestionType } from '@form-engine-ts/core';
|
|
3
3
|
import * as react from 'react';
|
|
4
|
-
import { ReactNode, ComponentType, KeyboardEvent, MouseEvent } from 'react';
|
|
4
|
+
import { ReactNode, ComponentType, KeyboardEvent, MouseEvent, CSSProperties } from 'react';
|
|
5
5
|
import { SensitiveDataFinding } from '@form-engine-ts/privacy';
|
|
6
6
|
|
|
7
7
|
interface SubmissionAttempt {
|
|
@@ -477,6 +477,25 @@ type SubmissionGuardResult = {
|
|
|
477
477
|
};
|
|
478
478
|
type SubmissionGuard = (schema: FormSchema, values: Record<string, unknown>) => SubmissionGuardResult | Promise<SubmissionGuardResult>;
|
|
479
479
|
type FormSuccessRenderMode = "append" | "replace";
|
|
480
|
+
type ChoiceFieldLayoutMode = "default" | "grouped";
|
|
481
|
+
interface ChoiceFieldTypeLayoutMap {
|
|
482
|
+
readonly radio?: ChoiceFieldLayoutMode;
|
|
483
|
+
readonly checkbox?: ChoiceFieldLayoutMode;
|
|
484
|
+
readonly "multi-select"?: ChoiceFieldLayoutMode;
|
|
485
|
+
readonly select?: ChoiceFieldLayoutMode;
|
|
486
|
+
}
|
|
487
|
+
type FieldError = ValidationIssue & {
|
|
488
|
+
readonly message: string;
|
|
489
|
+
};
|
|
490
|
+
interface FormRendererAppearance {
|
|
491
|
+
/**
|
|
492
|
+
* Layout preset for choice questions (radio, checkbox, and multi-select).
|
|
493
|
+
* - "default": keep the flat question layout.
|
|
494
|
+
* - "grouped": render a bordered fieldset and legend.
|
|
495
|
+
* - An object can configure each choice question type independently.
|
|
496
|
+
*/
|
|
497
|
+
readonly choiceField?: ChoiceFieldLayoutMode | ChoiceFieldTypeLayoutMap;
|
|
498
|
+
}
|
|
480
499
|
type SubmissionConfirmationRenderMode = "inline" | "replace" | "dialog";
|
|
481
500
|
interface SubmissionConfirmationOptions {
|
|
482
501
|
readonly enabled?: boolean;
|
|
@@ -504,6 +523,23 @@ interface FormFieldsSlotProps {
|
|
|
504
523
|
readonly children: ReactNode;
|
|
505
524
|
readonly className?: string;
|
|
506
525
|
}
|
|
526
|
+
interface ChoiceGroupSlotProps {
|
|
527
|
+
readonly field: Question;
|
|
528
|
+
readonly title: string;
|
|
529
|
+
readonly description?: string;
|
|
530
|
+
readonly required?: boolean;
|
|
531
|
+
readonly error?: FieldError;
|
|
532
|
+
readonly disabled?: boolean;
|
|
533
|
+
readonly readOnly?: boolean;
|
|
534
|
+
readonly children: ReactNode;
|
|
535
|
+
readonly className?: string;
|
|
536
|
+
}
|
|
537
|
+
interface FormRendererSlotProps {
|
|
538
|
+
readonly choiceGroup?: {
|
|
539
|
+
readonly className?: string;
|
|
540
|
+
readonly style?: CSSProperties;
|
|
541
|
+
};
|
|
542
|
+
}
|
|
507
543
|
interface FormRendererSlots {
|
|
508
544
|
readonly renderHeader?: (props: {
|
|
509
545
|
readonly title: string;
|
|
@@ -554,6 +590,7 @@ interface FormRendererSlots {
|
|
|
554
590
|
readonly current: number;
|
|
555
591
|
readonly max: number;
|
|
556
592
|
}) => ReactNode;
|
|
593
|
+
readonly renderChoiceGroup?: (props: ChoiceGroupSlotProps) => ReactNode;
|
|
557
594
|
}
|
|
558
595
|
interface SubmissionProtectionProps {
|
|
559
596
|
readonly submissionGuards?: readonly SubmissionGuard[];
|
|
@@ -656,6 +693,7 @@ declare const BUILDER_TRANSLATION_ALIASES: Readonly<Record<string, string>>;
|
|
|
656
693
|
declare function isTranslationUnresolved(result: unknown, key: string, aliases?: readonly string[]): boolean;
|
|
657
694
|
declare function resolveTranslation(key: string, aliases: readonly string[], adapter?: TranslationAdapter, defaultCatalog?: Readonly<Record<string, string>>, params?: Readonly<Record<string, unknown>>, locale?: string): string;
|
|
658
695
|
|
|
696
|
+
declare function resolveChoiceFieldLayout(type: FieldType, appearance?: FormRendererAppearance, groupedChoiceFieldsLegacy?: boolean): "default" | "grouped";
|
|
659
697
|
interface FieldComponentProps {
|
|
660
698
|
readonly field: FormField;
|
|
661
699
|
readonly value: FormValue;
|
|
@@ -671,6 +709,10 @@ type FieldComponents = Partial<Record<FieldType, ComponentType<FieldComponentPro
|
|
|
671
709
|
interface FormRendererPresentationProps extends SubmissionProtectionProps {
|
|
672
710
|
readonly components?: FieldComponents;
|
|
673
711
|
readonly className?: string;
|
|
712
|
+
readonly appearance?: FormRendererAppearance;
|
|
713
|
+
readonly slotProps?: FormRendererSlotProps;
|
|
714
|
+
/** @deprecated Use appearance.choiceField="grouped" instead. */
|
|
715
|
+
readonly groupedChoiceFields?: boolean;
|
|
674
716
|
/**
|
|
675
717
|
* Controls where the completion message is rendered after a successful submission.
|
|
676
718
|
* Defaults to "append" for backwards compatibility.
|
|
@@ -704,4 +746,4 @@ interface StandaloneFormRendererProps extends FormRendererPresentationProps {
|
|
|
704
746
|
type FormRendererProps = FormRendererPresentationProps | StandaloneFormRendererProps;
|
|
705
747
|
declare function FormRenderer(props: FormRendererProps): react.JSX.Element;
|
|
706
748
|
|
|
707
|
-
export { BUILDER_TRANSLATION_ALIASES, BUILDER_TRANSLATION_KEYS, 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 BuilderTranslationKey, type ComponentBaseProps, type FieldComponentProps, type FieldComponents, type FieldEditorControlsConfig, type FieldEditorHeaderSlotProps, type FieldPropertyControlMode, type FieldState, type FieldTypeSelectOptionsConfig, type FieldTypeSelectOptionsContext, type FieldTypeSelectOptionsSorter, type FieldTypeSelectOptionsTransformer, type FieldTypeSelectSlotProps, 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 FormRendererMessages, type FormRendererPresentationProps, type FormRendererProps, type FormRendererSlots, type FormServerErrorPayload, FormSubmissionError, type FormSubmitHandler, type FormSubmitState, type FormSubmitStatus, type FormSubmittedAnswerItem, type FormSuccessRenderMode, type IconButtonProps, type InputComponentProps, type LocalizationSummaryContext, type ManualTranslationContext, type ManualTranslationTarget, type RenderSubmitButtonProps, type SelectComponentProps, type StandaloneFormRendererProps, type SubmissionAttempt, type SubmissionAttemptStore, type SubmissionConfirmationOptions, type SubmissionConfirmationRenderMode, type SubmissionConfirmationSlotProps, type SubmissionGuard, type SubmissionGuardResult, type SubmissionProtectionProps, type SubmissionReceipt, type SubmissionReceiptQuery, type SubmissionReceiptStore, type SubmitContext, type SubmitResponse, type SubmitResult, type SubmitStatus, type UseSubmissionReceiptsResult, createLocalStorageSubmissionAttemptStore, createLocalStorageSubmissionReceiptStore, isTranslationUnresolved, resolveFieldEditorControls, resolveFieldTypeSelectOptions, resolveInitialFieldType, resolveTranslation, submissionReceiptQueryKey, useField, useForm, useFormBuilder, useSubmissionReceipts };
|
|
749
|
+
export { BUILDER_TRANSLATION_ALIASES, BUILDER_TRANSLATION_KEYS, 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 BuilderTranslationKey, type ChoiceFieldLayoutMode, type ChoiceFieldTypeLayoutMap, type ChoiceGroupSlotProps, type ComponentBaseProps, type FieldComponentProps, type FieldComponents, type FieldEditorControlsConfig, type FieldEditorHeaderSlotProps, type FieldError, type FieldPropertyControlMode, type FieldState, type FieldTypeSelectOptionsConfig, type FieldTypeSelectOptionsContext, type FieldTypeSelectOptionsSorter, type FieldTypeSelectOptionsTransformer, type FieldTypeSelectSlotProps, 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 FormRendererAppearance, type FormRendererMessages, type FormRendererPresentationProps, type FormRendererProps, type FormRendererSlotProps, type FormRendererSlots, type FormServerErrorPayload, FormSubmissionError, type FormSubmitHandler, type FormSubmitState, type FormSubmitStatus, type FormSubmittedAnswerItem, type FormSuccessRenderMode, type IconButtonProps, type InputComponentProps, type LocalizationSummaryContext, type ManualTranslationContext, type ManualTranslationTarget, type RenderSubmitButtonProps, type SelectComponentProps, type StandaloneFormRendererProps, type SubmissionAttempt, type SubmissionAttemptStore, type SubmissionConfirmationOptions, type SubmissionConfirmationRenderMode, type SubmissionConfirmationSlotProps, type SubmissionGuard, type SubmissionGuardResult, type SubmissionProtectionProps, type SubmissionReceipt, type SubmissionReceiptQuery, type SubmissionReceiptStore, type SubmitContext, type SubmitResponse, type SubmitResult, type SubmitStatus, type UseSubmissionReceiptsResult, createLocalStorageSubmissionAttemptStore, createLocalStorageSubmissionReceiptStore, isTranslationUnresolved, resolveChoiceFieldLayout, resolveFieldEditorControls, resolveFieldTypeSelectOptions, resolveInitialFieldType, resolveTranslation, submissionReceiptQueryKey, useField, useForm, useFormBuilder, useSubmissionReceipts };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { QuestionType, FormField, ChoiceOption, FormPage, FormSchema, DisplayCondition, JsonValue, SchemaIssue, FormPolicy, Question, FieldOption, TranslationReport, ValidationError, FormValues, TranslationAdapter, AsyncTranslationAdapter, PopulateTranslationOptions, FormValue,
|
|
1
|
+
import { QuestionType, FormField, ChoiceOption, FormPage, FormSchema, DisplayCondition, JsonValue, SchemaIssue, FormPolicy, Question, FieldOption, TranslationReport, ValidationIssue, ValidationError, FormValues, TranslationAdapter, AsyncTranslationAdapter, PopulateTranslationOptions, FormValue, AnswerValidationResult, FieldType } from '@form-engine-ts/core';
|
|
2
2
|
export { QuestionType } from '@form-engine-ts/core';
|
|
3
3
|
import * as react from 'react';
|
|
4
|
-
import { ReactNode, ComponentType, KeyboardEvent, MouseEvent } from 'react';
|
|
4
|
+
import { ReactNode, ComponentType, KeyboardEvent, MouseEvent, CSSProperties } from 'react';
|
|
5
5
|
import { SensitiveDataFinding } from '@form-engine-ts/privacy';
|
|
6
6
|
|
|
7
7
|
interface SubmissionAttempt {
|
|
@@ -477,6 +477,25 @@ type SubmissionGuardResult = {
|
|
|
477
477
|
};
|
|
478
478
|
type SubmissionGuard = (schema: FormSchema, values: Record<string, unknown>) => SubmissionGuardResult | Promise<SubmissionGuardResult>;
|
|
479
479
|
type FormSuccessRenderMode = "append" | "replace";
|
|
480
|
+
type ChoiceFieldLayoutMode = "default" | "grouped";
|
|
481
|
+
interface ChoiceFieldTypeLayoutMap {
|
|
482
|
+
readonly radio?: ChoiceFieldLayoutMode;
|
|
483
|
+
readonly checkbox?: ChoiceFieldLayoutMode;
|
|
484
|
+
readonly "multi-select"?: ChoiceFieldLayoutMode;
|
|
485
|
+
readonly select?: ChoiceFieldLayoutMode;
|
|
486
|
+
}
|
|
487
|
+
type FieldError = ValidationIssue & {
|
|
488
|
+
readonly message: string;
|
|
489
|
+
};
|
|
490
|
+
interface FormRendererAppearance {
|
|
491
|
+
/**
|
|
492
|
+
* Layout preset for choice questions (radio, checkbox, and multi-select).
|
|
493
|
+
* - "default": keep the flat question layout.
|
|
494
|
+
* - "grouped": render a bordered fieldset and legend.
|
|
495
|
+
* - An object can configure each choice question type independently.
|
|
496
|
+
*/
|
|
497
|
+
readonly choiceField?: ChoiceFieldLayoutMode | ChoiceFieldTypeLayoutMap;
|
|
498
|
+
}
|
|
480
499
|
type SubmissionConfirmationRenderMode = "inline" | "replace" | "dialog";
|
|
481
500
|
interface SubmissionConfirmationOptions {
|
|
482
501
|
readonly enabled?: boolean;
|
|
@@ -504,6 +523,23 @@ interface FormFieldsSlotProps {
|
|
|
504
523
|
readonly children: ReactNode;
|
|
505
524
|
readonly className?: string;
|
|
506
525
|
}
|
|
526
|
+
interface ChoiceGroupSlotProps {
|
|
527
|
+
readonly field: Question;
|
|
528
|
+
readonly title: string;
|
|
529
|
+
readonly description?: string;
|
|
530
|
+
readonly required?: boolean;
|
|
531
|
+
readonly error?: FieldError;
|
|
532
|
+
readonly disabled?: boolean;
|
|
533
|
+
readonly readOnly?: boolean;
|
|
534
|
+
readonly children: ReactNode;
|
|
535
|
+
readonly className?: string;
|
|
536
|
+
}
|
|
537
|
+
interface FormRendererSlotProps {
|
|
538
|
+
readonly choiceGroup?: {
|
|
539
|
+
readonly className?: string;
|
|
540
|
+
readonly style?: CSSProperties;
|
|
541
|
+
};
|
|
542
|
+
}
|
|
507
543
|
interface FormRendererSlots {
|
|
508
544
|
readonly renderHeader?: (props: {
|
|
509
545
|
readonly title: string;
|
|
@@ -554,6 +590,7 @@ interface FormRendererSlots {
|
|
|
554
590
|
readonly current: number;
|
|
555
591
|
readonly max: number;
|
|
556
592
|
}) => ReactNode;
|
|
593
|
+
readonly renderChoiceGroup?: (props: ChoiceGroupSlotProps) => ReactNode;
|
|
557
594
|
}
|
|
558
595
|
interface SubmissionProtectionProps {
|
|
559
596
|
readonly submissionGuards?: readonly SubmissionGuard[];
|
|
@@ -656,6 +693,7 @@ declare const BUILDER_TRANSLATION_ALIASES: Readonly<Record<string, string>>;
|
|
|
656
693
|
declare function isTranslationUnresolved(result: unknown, key: string, aliases?: readonly string[]): boolean;
|
|
657
694
|
declare function resolveTranslation(key: string, aliases: readonly string[], adapter?: TranslationAdapter, defaultCatalog?: Readonly<Record<string, string>>, params?: Readonly<Record<string, unknown>>, locale?: string): string;
|
|
658
695
|
|
|
696
|
+
declare function resolveChoiceFieldLayout(type: FieldType, appearance?: FormRendererAppearance, groupedChoiceFieldsLegacy?: boolean): "default" | "grouped";
|
|
659
697
|
interface FieldComponentProps {
|
|
660
698
|
readonly field: FormField;
|
|
661
699
|
readonly value: FormValue;
|
|
@@ -671,6 +709,10 @@ type FieldComponents = Partial<Record<FieldType, ComponentType<FieldComponentPro
|
|
|
671
709
|
interface FormRendererPresentationProps extends SubmissionProtectionProps {
|
|
672
710
|
readonly components?: FieldComponents;
|
|
673
711
|
readonly className?: string;
|
|
712
|
+
readonly appearance?: FormRendererAppearance;
|
|
713
|
+
readonly slotProps?: FormRendererSlotProps;
|
|
714
|
+
/** @deprecated Use appearance.choiceField="grouped" instead. */
|
|
715
|
+
readonly groupedChoiceFields?: boolean;
|
|
674
716
|
/**
|
|
675
717
|
* Controls where the completion message is rendered after a successful submission.
|
|
676
718
|
* Defaults to "append" for backwards compatibility.
|
|
@@ -704,4 +746,4 @@ interface StandaloneFormRendererProps extends FormRendererPresentationProps {
|
|
|
704
746
|
type FormRendererProps = FormRendererPresentationProps | StandaloneFormRendererProps;
|
|
705
747
|
declare function FormRenderer(props: FormRendererProps): react.JSX.Element;
|
|
706
748
|
|
|
707
|
-
export { BUILDER_TRANSLATION_ALIASES, BUILDER_TRANSLATION_KEYS, 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 BuilderTranslationKey, type ComponentBaseProps, type FieldComponentProps, type FieldComponents, type FieldEditorControlsConfig, type FieldEditorHeaderSlotProps, type FieldPropertyControlMode, type FieldState, type FieldTypeSelectOptionsConfig, type FieldTypeSelectOptionsContext, type FieldTypeSelectOptionsSorter, type FieldTypeSelectOptionsTransformer, type FieldTypeSelectSlotProps, 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 FormRendererMessages, type FormRendererPresentationProps, type FormRendererProps, type FormRendererSlots, type FormServerErrorPayload, FormSubmissionError, type FormSubmitHandler, type FormSubmitState, type FormSubmitStatus, type FormSubmittedAnswerItem, type FormSuccessRenderMode, type IconButtonProps, type InputComponentProps, type LocalizationSummaryContext, type ManualTranslationContext, type ManualTranslationTarget, type RenderSubmitButtonProps, type SelectComponentProps, type StandaloneFormRendererProps, type SubmissionAttempt, type SubmissionAttemptStore, type SubmissionConfirmationOptions, type SubmissionConfirmationRenderMode, type SubmissionConfirmationSlotProps, type SubmissionGuard, type SubmissionGuardResult, type SubmissionProtectionProps, type SubmissionReceipt, type SubmissionReceiptQuery, type SubmissionReceiptStore, type SubmitContext, type SubmitResponse, type SubmitResult, type SubmitStatus, type UseSubmissionReceiptsResult, createLocalStorageSubmissionAttemptStore, createLocalStorageSubmissionReceiptStore, isTranslationUnresolved, resolveFieldEditorControls, resolveFieldTypeSelectOptions, resolveInitialFieldType, resolveTranslation, submissionReceiptQueryKey, useField, useForm, useFormBuilder, useSubmissionReceipts };
|
|
749
|
+
export { BUILDER_TRANSLATION_ALIASES, BUILDER_TRANSLATION_KEYS, 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 BuilderTranslationKey, type ChoiceFieldLayoutMode, type ChoiceFieldTypeLayoutMap, type ChoiceGroupSlotProps, type ComponentBaseProps, type FieldComponentProps, type FieldComponents, type FieldEditorControlsConfig, type FieldEditorHeaderSlotProps, type FieldError, type FieldPropertyControlMode, type FieldState, type FieldTypeSelectOptionsConfig, type FieldTypeSelectOptionsContext, type FieldTypeSelectOptionsSorter, type FieldTypeSelectOptionsTransformer, type FieldTypeSelectSlotProps, 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 FormRendererAppearance, type FormRendererMessages, type FormRendererPresentationProps, type FormRendererProps, type FormRendererSlotProps, type FormRendererSlots, type FormServerErrorPayload, FormSubmissionError, type FormSubmitHandler, type FormSubmitState, type FormSubmitStatus, type FormSubmittedAnswerItem, type FormSuccessRenderMode, type IconButtonProps, type InputComponentProps, type LocalizationSummaryContext, type ManualTranslationContext, type ManualTranslationTarget, type RenderSubmitButtonProps, type SelectComponentProps, type StandaloneFormRendererProps, type SubmissionAttempt, type SubmissionAttemptStore, type SubmissionConfirmationOptions, type SubmissionConfirmationRenderMode, type SubmissionConfirmationSlotProps, type SubmissionGuard, type SubmissionGuardResult, type SubmissionProtectionProps, type SubmissionReceipt, type SubmissionReceiptQuery, type SubmissionReceiptStore, type SubmitContext, type SubmitResponse, type SubmitResult, type SubmitStatus, type UseSubmissionReceiptsResult, createLocalStorageSubmissionAttemptStore, createLocalStorageSubmissionReceiptStore, isTranslationUnresolved, resolveChoiceFieldLayout, resolveFieldEditorControls, resolveFieldTypeSelectOptions, resolveInitialFieldType, resolveTranslation, submissionReceiptQueryKey, useField, useForm, useFormBuilder, useSubmissionReceipts };
|
package/dist/index.js
CHANGED
|
@@ -3253,14 +3253,25 @@ import {
|
|
|
3253
3253
|
useState as useState4
|
|
3254
3254
|
} from "react";
|
|
3255
3255
|
import { Fragment as Fragment3, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
3256
|
+
var isChoiceFieldType = (type) => type === "radio" || type === "checkbox" || type === "multi-select" || type === "select";
|
|
3257
|
+
function resolveChoiceFieldLayout(type, appearance, groupedChoiceFieldsLegacy = false) {
|
|
3258
|
+
if (groupedChoiceFieldsLegacy) return "grouped";
|
|
3259
|
+
const config = appearance?.choiceField;
|
|
3260
|
+
if (config === void 0) return "default";
|
|
3261
|
+
if (typeof config === "string") return config;
|
|
3262
|
+
return config[type] ?? "default";
|
|
3263
|
+
}
|
|
3256
3264
|
function describedBy(field, error, helpId, errorId) {
|
|
3257
3265
|
const ids = [field.description === void 0 ? void 0 : helpId, error === void 0 ? void 0 : errorId].filter(
|
|
3258
3266
|
Boolean
|
|
3259
3267
|
);
|
|
3260
3268
|
return ids.length === 0 ? void 0 : ids.join(" ");
|
|
3261
3269
|
}
|
|
3262
|
-
function RequiredMark({
|
|
3263
|
-
|
|
3270
|
+
function RequiredMark({
|
|
3271
|
+
required,
|
|
3272
|
+
className = "fe-required"
|
|
3273
|
+
}) {
|
|
3274
|
+
return required ? /* @__PURE__ */ jsxs2("span", { className, "aria-hidden": "true", children: [
|
|
3264
3275
|
" ",
|
|
3265
3276
|
"*"
|
|
3266
3277
|
] }) : null;
|
|
@@ -3271,13 +3282,106 @@ function FieldMessage({ props }) {
|
|
|
3271
3282
|
props.error === void 0 ? null : /* @__PURE__ */ jsx3("div", { id: props.errorId, className: "fe-error", children: props.translate(props.error.messageKey, props.error.params) })
|
|
3272
3283
|
] });
|
|
3273
3284
|
}
|
|
3274
|
-
function
|
|
3285
|
+
function GroupedChoiceDescription({ props }) {
|
|
3286
|
+
return props.field.description === void 0 ? null : /* @__PURE__ */ jsx3("div", { id: props.helpId, className: "fe-field-description", children: props.field.description });
|
|
3287
|
+
}
|
|
3288
|
+
function GroupedChoiceError({ props }) {
|
|
3289
|
+
return props.error === void 0 ? null : /* @__PURE__ */ jsx3("div", { id: props.errorId, className: "fe-field-error", role: "alert", children: props.translate(props.error.messageKey, props.error.params) });
|
|
3290
|
+
}
|
|
3291
|
+
function ChoiceGroupFrame({
|
|
3292
|
+
props,
|
|
3293
|
+
children,
|
|
3294
|
+
className,
|
|
3295
|
+
slotProps,
|
|
3296
|
+
renderChoiceGroup,
|
|
3297
|
+
disabled,
|
|
3298
|
+
readOnly
|
|
3299
|
+
}) {
|
|
3300
|
+
const { field, error, translate } = props;
|
|
3301
|
+
const groupError = error === void 0 ? void 0 : { ...error, message: translate(error.messageKey, error.params) };
|
|
3302
|
+
const groupProps = {
|
|
3303
|
+
field,
|
|
3304
|
+
title: field.title,
|
|
3305
|
+
...field.description === void 0 ? {} : { description: field.description },
|
|
3306
|
+
...field.required === void 0 ? {} : { required: field.required },
|
|
3307
|
+
...groupError === void 0 ? {} : { error: groupError },
|
|
3308
|
+
...disabled === void 0 ? {} : { disabled },
|
|
3309
|
+
...readOnly === void 0 ? {} : { readOnly },
|
|
3310
|
+
children,
|
|
3311
|
+
className
|
|
3312
|
+
};
|
|
3313
|
+
if (renderChoiceGroup !== void 0) return /* @__PURE__ */ jsx3(Fragment3, { children: renderChoiceGroup(groupProps) });
|
|
3314
|
+
return (
|
|
3315
|
+
// biome-ignore lint/a11y/useAriaPropsSupportedByRole: The grouped fieldset exposes the required state for the complete choice question.
|
|
3316
|
+
/* @__PURE__ */ jsxs2(
|
|
3317
|
+
"fieldset",
|
|
3318
|
+
{
|
|
3319
|
+
className,
|
|
3320
|
+
"data-field-id": field.id,
|
|
3321
|
+
disabled,
|
|
3322
|
+
"aria-describedby": describedBy(field, error, props.helpId, props.errorId),
|
|
3323
|
+
"aria-invalid": Boolean(error),
|
|
3324
|
+
"aria-required": field.required,
|
|
3325
|
+
style: slotProps?.style,
|
|
3326
|
+
children: [
|
|
3327
|
+
/* @__PURE__ */ jsxs2("legend", { className: "fe-choice-legend", children: [
|
|
3328
|
+
field.title,
|
|
3329
|
+
/* @__PURE__ */ jsx3(RequiredMark, { required: field.required, className: "fe-required-badge" })
|
|
3330
|
+
] }),
|
|
3331
|
+
/* @__PURE__ */ jsx3(GroupedChoiceDescription, { props }),
|
|
3332
|
+
children,
|
|
3333
|
+
/* @__PURE__ */ jsx3(GroupedChoiceError, { props })
|
|
3334
|
+
]
|
|
3335
|
+
}
|
|
3336
|
+
)
|
|
3337
|
+
);
|
|
3338
|
+
}
|
|
3339
|
+
function DefaultField({
|
|
3340
|
+
groupedChoiceFields,
|
|
3341
|
+
appearance,
|
|
3342
|
+
choiceGroupSlotProps,
|
|
3343
|
+
renderChoiceGroup,
|
|
3344
|
+
disabled,
|
|
3345
|
+
readOnly,
|
|
3346
|
+
...props
|
|
3347
|
+
}) {
|
|
3275
3348
|
const { field, value, setValue, inputId, error, translate } = props;
|
|
3349
|
+
const isGroupedChoiceField = isChoiceFieldType(field.type) && resolveChoiceFieldLayout(field.type, appearance, groupedChoiceFields) === "grouped";
|
|
3350
|
+
const choiceGroupClassName = ["fe-choice-group", `fe-field--${field.type}`, choiceGroupSlotProps?.className].filter((item) => item !== void 0 && item.length > 0).join(" ");
|
|
3276
3351
|
const ariaProps = {
|
|
3277
3352
|
"aria-describedby": describedBy(field, error, props.helpId, props.errorId),
|
|
3278
3353
|
"aria-invalid": error === void 0 ? void 0 : true
|
|
3279
3354
|
};
|
|
3280
3355
|
if (field.type === "checkbox") {
|
|
3356
|
+
if (isGroupedChoiceField) {
|
|
3357
|
+
return /* @__PURE__ */ jsx3(
|
|
3358
|
+
ChoiceGroupFrame,
|
|
3359
|
+
{
|
|
3360
|
+
props,
|
|
3361
|
+
className: choiceGroupClassName,
|
|
3362
|
+
slotProps: choiceGroupSlotProps,
|
|
3363
|
+
renderChoiceGroup,
|
|
3364
|
+
disabled,
|
|
3365
|
+
readOnly,
|
|
3366
|
+
children: /* @__PURE__ */ jsx3("div", { className: "fe-choice-options", children: /* @__PURE__ */ jsxs2("label", { className: "fe-choice-option", htmlFor: inputId, children: [
|
|
3367
|
+
/* @__PURE__ */ jsx3(
|
|
3368
|
+
"input",
|
|
3369
|
+
{
|
|
3370
|
+
id: inputId,
|
|
3371
|
+
name: field.id,
|
|
3372
|
+
type: "checkbox",
|
|
3373
|
+
checked: value === true,
|
|
3374
|
+
"aria-label": field.title,
|
|
3375
|
+
disabled,
|
|
3376
|
+
readOnly,
|
|
3377
|
+
onChange: (event) => setValue(event.currentTarget.checked)
|
|
3378
|
+
}
|
|
3379
|
+
),
|
|
3380
|
+
/* @__PURE__ */ jsx3("span", { children: field.title })
|
|
3381
|
+
] }) })
|
|
3382
|
+
}
|
|
3383
|
+
);
|
|
3384
|
+
}
|
|
3281
3385
|
return /* @__PURE__ */ jsxs2("div", { className: "fe-field fe-field--checkbox", "data-field-id": field.id, children: [
|
|
3282
3386
|
/* @__PURE__ */ jsxs2("label", { className: "fe-check-label", htmlFor: inputId, children: [
|
|
3283
3387
|
/* @__PURE__ */ jsx3(
|
|
@@ -3301,6 +3405,84 @@ function DefaultField(props) {
|
|
|
3301
3405
|
}
|
|
3302
3406
|
if (field.type === "radio" || field.type === "multi-select") {
|
|
3303
3407
|
const selected = Array.isArray(value) ? value : [];
|
|
3408
|
+
if (isGroupedChoiceField) {
|
|
3409
|
+
const isRadio = field.type === "radio";
|
|
3410
|
+
return /* @__PURE__ */ jsx3(
|
|
3411
|
+
ChoiceGroupFrame,
|
|
3412
|
+
{
|
|
3413
|
+
props,
|
|
3414
|
+
className: choiceGroupClassName,
|
|
3415
|
+
slotProps: choiceGroupSlotProps,
|
|
3416
|
+
renderChoiceGroup,
|
|
3417
|
+
disabled,
|
|
3418
|
+
readOnly,
|
|
3419
|
+
children: /* @__PURE__ */ jsx3("div", { className: "fe-choice-options", children: field.options.map((option, index) => {
|
|
3420
|
+
const optionId = `${inputId}-${index}`;
|
|
3421
|
+
const checked = isRadio ? value === option.id : selected.includes(option.id);
|
|
3422
|
+
return /* @__PURE__ */ jsxs2("label", { className: "fe-choice-option", htmlFor: optionId, children: [
|
|
3423
|
+
/* @__PURE__ */ jsx3(
|
|
3424
|
+
"input",
|
|
3425
|
+
{
|
|
3426
|
+
id: optionId,
|
|
3427
|
+
name: field.id,
|
|
3428
|
+
type: isRadio ? "radio" : "checkbox",
|
|
3429
|
+
value: option.id,
|
|
3430
|
+
checked,
|
|
3431
|
+
disabled,
|
|
3432
|
+
readOnly,
|
|
3433
|
+
onKeyDown: isRadio ? (event) => {
|
|
3434
|
+
if (event.key !== "ArrowDown" && event.key !== "ArrowRight" && event.key !== "ArrowUp" && event.key !== "ArrowLeft")
|
|
3435
|
+
return;
|
|
3436
|
+
event.preventDefault();
|
|
3437
|
+
const offset = event.key === "ArrowDown" || event.key === "ArrowRight" ? 1 : -1;
|
|
3438
|
+
const nextIndex = (index + offset + field.options.length) % field.options.length;
|
|
3439
|
+
const nextOption = field.options[nextIndex];
|
|
3440
|
+
if (nextOption === void 0) return;
|
|
3441
|
+
setValue(nextOption.id);
|
|
3442
|
+
document.getElementById(`${inputId}-${nextIndex}`)?.focus();
|
|
3443
|
+
} : void 0,
|
|
3444
|
+
onChange: (event) => {
|
|
3445
|
+
if (isRadio) setValue(option.id);
|
|
3446
|
+
else
|
|
3447
|
+
setValue(
|
|
3448
|
+
event.currentTarget.checked ? [...selected, option.id] : selected.filter((item) => item !== option.id)
|
|
3449
|
+
);
|
|
3450
|
+
}
|
|
3451
|
+
}
|
|
3452
|
+
),
|
|
3453
|
+
/* @__PURE__ */ jsx3("span", { children: option.label })
|
|
3454
|
+
] }, option.id);
|
|
3455
|
+
}) })
|
|
3456
|
+
}
|
|
3457
|
+
);
|
|
3458
|
+
}
|
|
3459
|
+
if (field.type === "radio") {
|
|
3460
|
+
return /* @__PURE__ */ jsxs2("div", { className: "fe-field fe-field--radio", "data-field-id": field.id, children: [
|
|
3461
|
+
/* @__PURE__ */ jsxs2("div", { className: "fe-label", children: [
|
|
3462
|
+
field.title,
|
|
3463
|
+
/* @__PURE__ */ jsx3(RequiredMark, { required: field.required })
|
|
3464
|
+
] }),
|
|
3465
|
+
field.options.map((option, index) => {
|
|
3466
|
+
const optionId = `${inputId}-${index}`;
|
|
3467
|
+
return /* @__PURE__ */ jsxs2("label", { className: "fe-check-label", htmlFor: optionId, children: [
|
|
3468
|
+
/* @__PURE__ */ jsx3(
|
|
3469
|
+
"input",
|
|
3470
|
+
{
|
|
3471
|
+
...ariaProps,
|
|
3472
|
+
id: optionId,
|
|
3473
|
+
name: field.id,
|
|
3474
|
+
type: "radio",
|
|
3475
|
+
value: option.id,
|
|
3476
|
+
checked: value === option.id,
|
|
3477
|
+
onChange: () => setValue(option.id)
|
|
3478
|
+
}
|
|
3479
|
+
),
|
|
3480
|
+
/* @__PURE__ */ jsx3("span", { children: option.label })
|
|
3481
|
+
] }, option.id);
|
|
3482
|
+
}),
|
|
3483
|
+
/* @__PURE__ */ jsx3(FieldMessage, { props })
|
|
3484
|
+
] });
|
|
3485
|
+
}
|
|
3304
3486
|
return /* @__PURE__ */ jsxs2("fieldset", { className: `fe-field fe-field--${field.type}`, "data-field-id": field.id, ...ariaProps, children: [
|
|
3305
3487
|
/* @__PURE__ */ jsxs2("legend", { className: "fe-label", children: [
|
|
3306
3488
|
field.title,
|
|
@@ -3408,6 +3590,7 @@ function DefaultField(props) {
|
|
|
3408
3590
|
...ariaProps,
|
|
3409
3591
|
id: inputId,
|
|
3410
3592
|
name: field.id,
|
|
3593
|
+
disabled,
|
|
3411
3594
|
value: typeof value === "string" ? value : "",
|
|
3412
3595
|
onChange: (event) => setValue(event.currentTarget.value || void 0),
|
|
3413
3596
|
children: [
|
|
@@ -3431,6 +3614,20 @@ function DefaultField(props) {
|
|
|
3431
3614
|
}
|
|
3432
3615
|
);
|
|
3433
3616
|
}
|
|
3617
|
+
if (isGroupedChoiceField) {
|
|
3618
|
+
return /* @__PURE__ */ jsx3(
|
|
3619
|
+
ChoiceGroupFrame,
|
|
3620
|
+
{
|
|
3621
|
+
props,
|
|
3622
|
+
className: choiceGroupClassName,
|
|
3623
|
+
slotProps: choiceGroupSlotProps,
|
|
3624
|
+
renderChoiceGroup,
|
|
3625
|
+
disabled,
|
|
3626
|
+
readOnly,
|
|
3627
|
+
children: /* @__PURE__ */ jsx3("div", { className: "fe-choice-options", children: control })
|
|
3628
|
+
}
|
|
3629
|
+
);
|
|
3630
|
+
}
|
|
3434
3631
|
return /* @__PURE__ */ jsxs2("div", { className: `fe-field fe-field--${field.type}`, "data-field-id": field.id, children: [
|
|
3435
3632
|
label,
|
|
3436
3633
|
control,
|
|
@@ -3550,6 +3747,9 @@ function ContextFormRenderer({
|
|
|
3550
3747
|
beforeSubmit,
|
|
3551
3748
|
onDraftSave,
|
|
3552
3749
|
successRenderMode = "append",
|
|
3750
|
+
appearance,
|
|
3751
|
+
slotProps,
|
|
3752
|
+
groupedChoiceFields = false,
|
|
3553
3753
|
submissionConfirmation,
|
|
3554
3754
|
submissionConfirmationRenderMode,
|
|
3555
3755
|
showHiddenFieldsInSummary = false,
|
|
@@ -4068,7 +4268,18 @@ function ContextFormRenderer({
|
|
|
4068
4268
|
}) }, field.id);
|
|
4069
4269
|
}
|
|
4070
4270
|
const Component = components[field.type];
|
|
4071
|
-
return Component === void 0 ? /* @__PURE__ */ jsx3(
|
|
4271
|
+
return Component === void 0 ? /* @__PURE__ */ jsx3(
|
|
4272
|
+
DefaultField,
|
|
4273
|
+
{
|
|
4274
|
+
...props,
|
|
4275
|
+
groupedChoiceFields,
|
|
4276
|
+
appearance,
|
|
4277
|
+
choiceGroupSlotProps: slotProps?.choiceGroup,
|
|
4278
|
+
renderChoiceGroup: slots.renderChoiceGroup,
|
|
4279
|
+
disabled: interactionLocked
|
|
4280
|
+
},
|
|
4281
|
+
field.id
|
|
4282
|
+
) : /* @__PURE__ */ jsx3(Component, { ...props }, field.id);
|
|
4072
4283
|
});
|
|
4073
4284
|
const fieldClassName = `fe-fields${fieldsClassName === void 0 ? "" : ` ${fieldsClassName}`}`;
|
|
4074
4285
|
return slots.renderFields?.({ children: fieldChildren, className: fieldClassName }) ?? /* @__PURE__ */ jsx3("div", { className: fieldClassName, children: fieldChildren });
|
|
@@ -4207,6 +4418,7 @@ export {
|
|
|
4207
4418
|
createLocalStorageSubmissionAttemptStore,
|
|
4208
4419
|
createLocalStorageSubmissionReceiptStore,
|
|
4209
4420
|
isTranslationUnresolved,
|
|
4421
|
+
resolveChoiceFieldLayout,
|
|
4210
4422
|
resolveFieldEditorControls,
|
|
4211
4423
|
resolveFieldTypeSelectOptions,
|
|
4212
4424
|
resolveInitialFieldType,
|
package/dist/styles.css
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
/* src/styles.css */
|
|
2
|
+
:root {
|
|
3
|
+
--fe-choice-group-border: 1px solid var(--fe-border-color, #e2e8f0);
|
|
4
|
+
--fe-choice-group-radius: var(--fe-radius, 8px);
|
|
5
|
+
--fe-choice-group-padding: 1rem 1.25rem;
|
|
6
|
+
--fe-choice-group-margin-bottom: 1.25rem;
|
|
7
|
+
--fe-choice-group-bg: var(--fe-bg-color, transparent);
|
|
8
|
+
--fe-choice-legend-font-weight: 600;
|
|
9
|
+
--fe-choice-legend-font-size: 0.95rem;
|
|
10
|
+
--fe-choice-legend-color: var(--fe-text-color, inherit);
|
|
11
|
+
--fe-choice-options-gap: 0.625rem;
|
|
12
|
+
}
|
|
2
13
|
.fe-form {
|
|
3
14
|
color: #17202a;
|
|
4
15
|
display: grid;
|
|
@@ -123,6 +134,41 @@
|
|
|
123
134
|
.fe-check-label input {
|
|
124
135
|
margin-top: 0.22rem;
|
|
125
136
|
}
|
|
137
|
+
.fe-choice-group {
|
|
138
|
+
background-color: var(--fe-choice-group-bg);
|
|
139
|
+
border: var(--fe-choice-group-border);
|
|
140
|
+
border-radius: var(--fe-choice-group-radius);
|
|
141
|
+
box-sizing: border-box;
|
|
142
|
+
margin: 0 0 var(--fe-choice-group-margin-bottom);
|
|
143
|
+
padding: var(--fe-choice-group-padding);
|
|
144
|
+
}
|
|
145
|
+
.fe-choice-legend {
|
|
146
|
+
color: var(--fe-choice-legend-color);
|
|
147
|
+
font-size: var(--fe-choice-legend-font-size);
|
|
148
|
+
font-weight: var(--fe-choice-legend-font-weight);
|
|
149
|
+
padding: 0 0.5rem;
|
|
150
|
+
}
|
|
151
|
+
.fe-choice-options {
|
|
152
|
+
display: flex;
|
|
153
|
+
flex-direction: column;
|
|
154
|
+
gap: var(--fe-choice-options-gap);
|
|
155
|
+
margin-top: 0.5rem;
|
|
156
|
+
}
|
|
157
|
+
.fe-choice-option {
|
|
158
|
+
align-items: center;
|
|
159
|
+
cursor: pointer;
|
|
160
|
+
display: flex;
|
|
161
|
+
gap: 0.5rem;
|
|
162
|
+
}
|
|
163
|
+
.fe-field-description {
|
|
164
|
+
color: #667085;
|
|
165
|
+
font-size: 0.875rem;
|
|
166
|
+
}
|
|
167
|
+
.fe-field-error {
|
|
168
|
+
color: #b42318;
|
|
169
|
+
font-size: 0.875rem;
|
|
170
|
+
font-weight: 600;
|
|
171
|
+
}
|
|
126
172
|
.fe-rating-options {
|
|
127
173
|
display: flex;
|
|
128
174
|
flex-wrap: wrap;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@form-engine-ts/react",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.4.0",
|
|
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": "4.
|
|
46
|
-
"@form-engine-ts/privacy": "4.
|
|
45
|
+
"@form-engine-ts/core": "4.4.0",
|
|
46
|
+
"@form-engine-ts/privacy": "4.4.0"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
49
|
"react": ">=18.2 <20",
|