@form-engine-ts/react 4.3.2 → 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 +13 -2
- package/dist/index.cjs +169 -87
- package/dist/index.d.cts +35 -5
- package/dist/index.d.ts +35 -5
- package/dist/index.js +168 -87
- package/dist/styles.css +20 -9
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -41,13 +41,24 @@ Use any compatible `TranslationAdapter` in place of the mock translator.
|
|
|
41
41
|
|
|
42
42
|
## Choice field layout
|
|
43
43
|
|
|
44
|
-
Radio and
|
|
45
|
-
them as bordered, accessible `<fieldset>` groups with `<legend>` titles:
|
|
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
46
|
|
|
47
47
|
```tsx
|
|
48
48
|
<FormRenderer appearance={{ choiceField: "grouped" }} />
|
|
49
49
|
```
|
|
50
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
|
+
|
|
51
62
|
`groupedChoiceFields={true}` remains available as a deprecated compatibility alias.
|
|
52
63
|
|
|
53
64
|
Define `schema.pages` to enable Back/Next navigation, page validation, conditional page skipping, and an accessible
|
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,6 +3271,14 @@ 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
|
|
@@ -3297,51 +3306,98 @@ function GroupedChoiceDescription({ props }) {
|
|
|
3297
3306
|
function GroupedChoiceError({ props }) {
|
|
3298
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) });
|
|
3299
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
|
+
}
|
|
3300
3357
|
function DefaultField({
|
|
3301
3358
|
groupedChoiceFields,
|
|
3359
|
+
appearance,
|
|
3360
|
+
choiceGroupSlotProps,
|
|
3361
|
+
renderChoiceGroup,
|
|
3362
|
+
disabled,
|
|
3363
|
+
readOnly,
|
|
3302
3364
|
...props
|
|
3303
3365
|
}) {
|
|
3304
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(" ");
|
|
3305
3369
|
const ariaProps = {
|
|
3306
3370
|
"aria-describedby": describedBy(field, error, props.helpId, props.errorId),
|
|
3307
3371
|
"aria-invalid": error === void 0 ? void 0 : true
|
|
3308
3372
|
};
|
|
3309
3373
|
if (field.type === "checkbox") {
|
|
3310
|
-
if (
|
|
3311
|
-
return (
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
}
|
|
3338
|
-
),
|
|
3339
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: field.title })
|
|
3340
|
-
] }) }),
|
|
3341
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(GroupedChoiceError, { props })
|
|
3342
|
-
]
|
|
3343
|
-
}
|
|
3344
|
-
)
|
|
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
|
+
}
|
|
3345
3401
|
);
|
|
3346
3402
|
}
|
|
3347
3403
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "fe-field fe-field--checkbox", "data-field-id": field.id, children: [
|
|
@@ -3367,56 +3423,55 @@ function DefaultField({
|
|
|
3367
3423
|
}
|
|
3368
3424
|
if (field.type === "radio" || field.type === "multi-select") {
|
|
3369
3425
|
const selected = Array.isArray(value) ? value : [];
|
|
3370
|
-
if (
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
)
|
|
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
|
+
}
|
|
3420
3475
|
);
|
|
3421
3476
|
}
|
|
3422
3477
|
if (field.type === "radio") {
|
|
@@ -3553,6 +3608,7 @@ function DefaultField({
|
|
|
3553
3608
|
...ariaProps,
|
|
3554
3609
|
id: inputId,
|
|
3555
3610
|
name: field.id,
|
|
3611
|
+
disabled,
|
|
3556
3612
|
value: typeof value === "string" ? value : "",
|
|
3557
3613
|
onChange: (event) => setValue(event.currentTarget.value || void 0),
|
|
3558
3614
|
children: [
|
|
@@ -3576,6 +3632,20 @@ function DefaultField({
|
|
|
3576
3632
|
}
|
|
3577
3633
|
);
|
|
3578
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
|
+
}
|
|
3579
3649
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: `fe-field fe-field--${field.type}`, "data-field-id": field.id, children: [
|
|
3580
3650
|
label,
|
|
3581
3651
|
control,
|
|
@@ -3696,6 +3766,7 @@ function ContextFormRenderer({
|
|
|
3696
3766
|
onDraftSave,
|
|
3697
3767
|
successRenderMode = "append",
|
|
3698
3768
|
appearance,
|
|
3769
|
+
slotProps,
|
|
3699
3770
|
groupedChoiceFields = false,
|
|
3700
3771
|
submissionConfirmation,
|
|
3701
3772
|
submissionConfirmationRenderMode,
|
|
@@ -3709,7 +3780,6 @@ function ContextFormRenderer({
|
|
|
3709
3780
|
slots = {}
|
|
3710
3781
|
}) {
|
|
3711
3782
|
const form = useForm();
|
|
3712
|
-
const isGroupedMode = appearance?.choiceField === "grouped" || groupedChoiceFields;
|
|
3713
3783
|
const prefix = (0, import_react5.useId)().replace(/:/g, "");
|
|
3714
3784
|
const formRef = (0, import_react5.useRef)(null);
|
|
3715
3785
|
const loadedDraftKey = (0, import_react5.useRef)(null);
|
|
@@ -4216,7 +4286,18 @@ function ContextFormRenderer({
|
|
|
4216
4286
|
}) }, field.id);
|
|
4217
4287
|
}
|
|
4218
4288
|
const Component = components[field.type];
|
|
4219
|
-
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);
|
|
4220
4301
|
});
|
|
4221
4302
|
const fieldClassName = `fe-fields${fieldsClassName === void 0 ? "" : ` ${fieldsClassName}`}`;
|
|
4222
4303
|
return slots.renderFields?.({ children: fieldChildren, className: fieldClassName }) ?? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: fieldClassName, children: fieldChildren });
|
|
@@ -4356,6 +4437,7 @@ function FormRenderer(props) {
|
|
|
4356
4437
|
createLocalStorageSubmissionAttemptStore,
|
|
4357
4438
|
createLocalStorageSubmissionReceiptStore,
|
|
4358
4439
|
isTranslationUnresolved,
|
|
4440
|
+
resolveChoiceFieldLayout,
|
|
4359
4441
|
resolveFieldEditorControls,
|
|
4360
4442
|
resolveFieldTypeSelectOptions,
|
|
4361
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 {
|
|
@@ -478,13 +478,23 @@ type SubmissionGuardResult = {
|
|
|
478
478
|
type SubmissionGuard = (schema: FormSchema, values: Record<string, unknown>) => SubmissionGuardResult | Promise<SubmissionGuardResult>;
|
|
479
479
|
type FormSuccessRenderMode = "append" | "replace";
|
|
480
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
|
+
};
|
|
481
490
|
interface FormRendererAppearance {
|
|
482
491
|
/**
|
|
483
|
-
* Layout preset for radio and
|
|
492
|
+
* Layout preset for choice questions (radio, checkbox, and multi-select).
|
|
484
493
|
* - "default": keep the flat question layout.
|
|
485
494
|
* - "grouped": render a bordered fieldset and legend.
|
|
495
|
+
* - An object can configure each choice question type independently.
|
|
486
496
|
*/
|
|
487
|
-
readonly choiceField?: ChoiceFieldLayoutMode;
|
|
497
|
+
readonly choiceField?: ChoiceFieldLayoutMode | ChoiceFieldTypeLayoutMap;
|
|
488
498
|
}
|
|
489
499
|
type SubmissionConfirmationRenderMode = "inline" | "replace" | "dialog";
|
|
490
500
|
interface SubmissionConfirmationOptions {
|
|
@@ -513,6 +523,23 @@ interface FormFieldsSlotProps {
|
|
|
513
523
|
readonly children: ReactNode;
|
|
514
524
|
readonly className?: string;
|
|
515
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
|
+
}
|
|
516
543
|
interface FormRendererSlots {
|
|
517
544
|
readonly renderHeader?: (props: {
|
|
518
545
|
readonly title: string;
|
|
@@ -563,6 +590,7 @@ interface FormRendererSlots {
|
|
|
563
590
|
readonly current: number;
|
|
564
591
|
readonly max: number;
|
|
565
592
|
}) => ReactNode;
|
|
593
|
+
readonly renderChoiceGroup?: (props: ChoiceGroupSlotProps) => ReactNode;
|
|
566
594
|
}
|
|
567
595
|
interface SubmissionProtectionProps {
|
|
568
596
|
readonly submissionGuards?: readonly SubmissionGuard[];
|
|
@@ -665,6 +693,7 @@ declare const BUILDER_TRANSLATION_ALIASES: Readonly<Record<string, string>>;
|
|
|
665
693
|
declare function isTranslationUnresolved(result: unknown, key: string, aliases?: readonly string[]): boolean;
|
|
666
694
|
declare function resolveTranslation(key: string, aliases: readonly string[], adapter?: TranslationAdapter, defaultCatalog?: Readonly<Record<string, string>>, params?: Readonly<Record<string, unknown>>, locale?: string): string;
|
|
667
695
|
|
|
696
|
+
declare function resolveChoiceFieldLayout(type: FieldType, appearance?: FormRendererAppearance, groupedChoiceFieldsLegacy?: boolean): "default" | "grouped";
|
|
668
697
|
interface FieldComponentProps {
|
|
669
698
|
readonly field: FormField;
|
|
670
699
|
readonly value: FormValue;
|
|
@@ -681,6 +710,7 @@ interface FormRendererPresentationProps extends SubmissionProtectionProps {
|
|
|
681
710
|
readonly components?: FieldComponents;
|
|
682
711
|
readonly className?: string;
|
|
683
712
|
readonly appearance?: FormRendererAppearance;
|
|
713
|
+
readonly slotProps?: FormRendererSlotProps;
|
|
684
714
|
/** @deprecated Use appearance.choiceField="grouped" instead. */
|
|
685
715
|
readonly groupedChoiceFields?: boolean;
|
|
686
716
|
/**
|
|
@@ -716,4 +746,4 @@ interface StandaloneFormRendererProps extends FormRendererPresentationProps {
|
|
|
716
746
|
type FormRendererProps = FormRendererPresentationProps | StandaloneFormRendererProps;
|
|
717
747
|
declare function FormRenderer(props: FormRendererProps): react.JSX.Element;
|
|
718
748
|
|
|
719
|
-
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 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 FormRendererAppearance, 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 {
|
|
@@ -478,13 +478,23 @@ type SubmissionGuardResult = {
|
|
|
478
478
|
type SubmissionGuard = (schema: FormSchema, values: Record<string, unknown>) => SubmissionGuardResult | Promise<SubmissionGuardResult>;
|
|
479
479
|
type FormSuccessRenderMode = "append" | "replace";
|
|
480
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
|
+
};
|
|
481
490
|
interface FormRendererAppearance {
|
|
482
491
|
/**
|
|
483
|
-
* Layout preset for radio and
|
|
492
|
+
* Layout preset for choice questions (radio, checkbox, and multi-select).
|
|
484
493
|
* - "default": keep the flat question layout.
|
|
485
494
|
* - "grouped": render a bordered fieldset and legend.
|
|
495
|
+
* - An object can configure each choice question type independently.
|
|
486
496
|
*/
|
|
487
|
-
readonly choiceField?: ChoiceFieldLayoutMode;
|
|
497
|
+
readonly choiceField?: ChoiceFieldLayoutMode | ChoiceFieldTypeLayoutMap;
|
|
488
498
|
}
|
|
489
499
|
type SubmissionConfirmationRenderMode = "inline" | "replace" | "dialog";
|
|
490
500
|
interface SubmissionConfirmationOptions {
|
|
@@ -513,6 +523,23 @@ interface FormFieldsSlotProps {
|
|
|
513
523
|
readonly children: ReactNode;
|
|
514
524
|
readonly className?: string;
|
|
515
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
|
+
}
|
|
516
543
|
interface FormRendererSlots {
|
|
517
544
|
readonly renderHeader?: (props: {
|
|
518
545
|
readonly title: string;
|
|
@@ -563,6 +590,7 @@ interface FormRendererSlots {
|
|
|
563
590
|
readonly current: number;
|
|
564
591
|
readonly max: number;
|
|
565
592
|
}) => ReactNode;
|
|
593
|
+
readonly renderChoiceGroup?: (props: ChoiceGroupSlotProps) => ReactNode;
|
|
566
594
|
}
|
|
567
595
|
interface SubmissionProtectionProps {
|
|
568
596
|
readonly submissionGuards?: readonly SubmissionGuard[];
|
|
@@ -665,6 +693,7 @@ declare const BUILDER_TRANSLATION_ALIASES: Readonly<Record<string, string>>;
|
|
|
665
693
|
declare function isTranslationUnresolved(result: unknown, key: string, aliases?: readonly string[]): boolean;
|
|
666
694
|
declare function resolveTranslation(key: string, aliases: readonly string[], adapter?: TranslationAdapter, defaultCatalog?: Readonly<Record<string, string>>, params?: Readonly<Record<string, unknown>>, locale?: string): string;
|
|
667
695
|
|
|
696
|
+
declare function resolveChoiceFieldLayout(type: FieldType, appearance?: FormRendererAppearance, groupedChoiceFieldsLegacy?: boolean): "default" | "grouped";
|
|
668
697
|
interface FieldComponentProps {
|
|
669
698
|
readonly field: FormField;
|
|
670
699
|
readonly value: FormValue;
|
|
@@ -681,6 +710,7 @@ interface FormRendererPresentationProps extends SubmissionProtectionProps {
|
|
|
681
710
|
readonly components?: FieldComponents;
|
|
682
711
|
readonly className?: string;
|
|
683
712
|
readonly appearance?: FormRendererAppearance;
|
|
713
|
+
readonly slotProps?: FormRendererSlotProps;
|
|
684
714
|
/** @deprecated Use appearance.choiceField="grouped" instead. */
|
|
685
715
|
readonly groupedChoiceFields?: boolean;
|
|
686
716
|
/**
|
|
@@ -716,4 +746,4 @@ interface StandaloneFormRendererProps extends FormRendererPresentationProps {
|
|
|
716
746
|
type FormRendererProps = FormRendererPresentationProps | StandaloneFormRendererProps;
|
|
717
747
|
declare function FormRenderer(props: FormRendererProps): react.JSX.Element;
|
|
718
748
|
|
|
719
|
-
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 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 FormRendererAppearance, 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,6 +3253,14 @@ 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
|
|
@@ -3280,51 +3288,98 @@ function GroupedChoiceDescription({ props }) {
|
|
|
3280
3288
|
function GroupedChoiceError({ props }) {
|
|
3281
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) });
|
|
3282
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
|
+
}
|
|
3283
3339
|
function DefaultField({
|
|
3284
3340
|
groupedChoiceFields,
|
|
3341
|
+
appearance,
|
|
3342
|
+
choiceGroupSlotProps,
|
|
3343
|
+
renderChoiceGroup,
|
|
3344
|
+
disabled,
|
|
3345
|
+
readOnly,
|
|
3285
3346
|
...props
|
|
3286
3347
|
}) {
|
|
3287
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(" ");
|
|
3288
3351
|
const ariaProps = {
|
|
3289
3352
|
"aria-describedby": describedBy(field, error, props.helpId, props.errorId),
|
|
3290
3353
|
"aria-invalid": error === void 0 ? void 0 : true
|
|
3291
3354
|
};
|
|
3292
3355
|
if (field.type === "checkbox") {
|
|
3293
|
-
if (
|
|
3294
|
-
return (
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
}
|
|
3321
|
-
),
|
|
3322
|
-
/* @__PURE__ */ jsx3("span", { children: field.title })
|
|
3323
|
-
] }) }),
|
|
3324
|
-
/* @__PURE__ */ jsx3(GroupedChoiceError, { props })
|
|
3325
|
-
]
|
|
3326
|
-
}
|
|
3327
|
-
)
|
|
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
|
+
}
|
|
3328
3383
|
);
|
|
3329
3384
|
}
|
|
3330
3385
|
return /* @__PURE__ */ jsxs2("div", { className: "fe-field fe-field--checkbox", "data-field-id": field.id, children: [
|
|
@@ -3350,56 +3405,55 @@ function DefaultField({
|
|
|
3350
3405
|
}
|
|
3351
3406
|
if (field.type === "radio" || field.type === "multi-select") {
|
|
3352
3407
|
const selected = Array.isArray(value) ? value : [];
|
|
3353
|
-
if (
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
)
|
|
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
|
+
}
|
|
3403
3457
|
);
|
|
3404
3458
|
}
|
|
3405
3459
|
if (field.type === "radio") {
|
|
@@ -3536,6 +3590,7 @@ function DefaultField({
|
|
|
3536
3590
|
...ariaProps,
|
|
3537
3591
|
id: inputId,
|
|
3538
3592
|
name: field.id,
|
|
3593
|
+
disabled,
|
|
3539
3594
|
value: typeof value === "string" ? value : "",
|
|
3540
3595
|
onChange: (event) => setValue(event.currentTarget.value || void 0),
|
|
3541
3596
|
children: [
|
|
@@ -3559,6 +3614,20 @@ function DefaultField({
|
|
|
3559
3614
|
}
|
|
3560
3615
|
);
|
|
3561
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
|
+
}
|
|
3562
3631
|
return /* @__PURE__ */ jsxs2("div", { className: `fe-field fe-field--${field.type}`, "data-field-id": field.id, children: [
|
|
3563
3632
|
label,
|
|
3564
3633
|
control,
|
|
@@ -3679,6 +3748,7 @@ function ContextFormRenderer({
|
|
|
3679
3748
|
onDraftSave,
|
|
3680
3749
|
successRenderMode = "append",
|
|
3681
3750
|
appearance,
|
|
3751
|
+
slotProps,
|
|
3682
3752
|
groupedChoiceFields = false,
|
|
3683
3753
|
submissionConfirmation,
|
|
3684
3754
|
submissionConfirmationRenderMode,
|
|
@@ -3692,7 +3762,6 @@ function ContextFormRenderer({
|
|
|
3692
3762
|
slots = {}
|
|
3693
3763
|
}) {
|
|
3694
3764
|
const form = useForm();
|
|
3695
|
-
const isGroupedMode = appearance?.choiceField === "grouped" || groupedChoiceFields;
|
|
3696
3765
|
const prefix = useId().replace(/:/g, "");
|
|
3697
3766
|
const formRef = useRef2(null);
|
|
3698
3767
|
const loadedDraftKey = useRef2(null);
|
|
@@ -4199,7 +4268,18 @@ function ContextFormRenderer({
|
|
|
4199
4268
|
}) }, field.id);
|
|
4200
4269
|
}
|
|
4201
4270
|
const Component = components[field.type];
|
|
4202
|
-
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);
|
|
4203
4283
|
});
|
|
4204
4284
|
const fieldClassName = `fe-fields${fieldsClassName === void 0 ? "" : ` ${fieldsClassName}`}`;
|
|
4205
4285
|
return slots.renderFields?.({ children: fieldChildren, className: fieldClassName }) ?? /* @__PURE__ */ jsx3("div", { className: fieldClassName, children: fieldChildren });
|
|
@@ -4338,6 +4418,7 @@ export {
|
|
|
4338
4418
|
createLocalStorageSubmissionAttemptStore,
|
|
4339
4419
|
createLocalStorageSubmissionReceiptStore,
|
|
4340
4420
|
isTranslationUnresolved,
|
|
4421
|
+
resolveChoiceFieldLayout,
|
|
4341
4422
|
resolveFieldEditorControls,
|
|
4342
4423
|
resolveFieldTypeSelectOptions,
|
|
4343
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;
|
|
@@ -124,23 +135,23 @@
|
|
|
124
135
|
margin-top: 0.22rem;
|
|
125
136
|
}
|
|
126
137
|
.fe-choice-group {
|
|
127
|
-
background-color: var(--fe-bg
|
|
128
|
-
border:
|
|
129
|
-
border-radius: var(--fe-radius
|
|
138
|
+
background-color: var(--fe-choice-group-bg);
|
|
139
|
+
border: var(--fe-choice-group-border);
|
|
140
|
+
border-radius: var(--fe-choice-group-radius);
|
|
130
141
|
box-sizing: border-box;
|
|
131
|
-
margin: 0 0
|
|
132
|
-
padding:
|
|
142
|
+
margin: 0 0 var(--fe-choice-group-margin-bottom);
|
|
143
|
+
padding: var(--fe-choice-group-padding);
|
|
133
144
|
}
|
|
134
145
|
.fe-choice-legend {
|
|
135
|
-
color: var(--fe-
|
|
136
|
-
font-size:
|
|
137
|
-
font-weight:
|
|
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);
|
|
138
149
|
padding: 0 0.5rem;
|
|
139
150
|
}
|
|
140
151
|
.fe-choice-options {
|
|
141
152
|
display: flex;
|
|
142
153
|
flex-direction: column;
|
|
143
|
-
gap:
|
|
154
|
+
gap: var(--fe-choice-options-gap);
|
|
144
155
|
margin-top: 0.5rem;
|
|
145
156
|
}
|
|
146
157
|
.fe-choice-option {
|
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",
|