@form-engine-ts/react 4.3.1 → 4.3.2
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 +11 -0
- package/dist/index.cjs +135 -4
- package/dist/index.d.cts +13 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.js +135 -4
- package/dist/styles.css +35 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -39,6 +39,17 @@ 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 and checkbox questions use the flat layout by default. Set `appearance.choiceField` to `"grouped"` to render
|
|
45
|
+
them as bordered, accessible `<fieldset>` groups with `<legend>` titles:
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
<FormRenderer appearance={{ choiceField: "grouped" }} />
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`groupedChoiceFields={true}` remains available as a deprecated compatibility alias.
|
|
52
|
+
|
|
42
53
|
Define `schema.pages` to enable Back/Next navigation, page validation, conditional page skipping, and an accessible
|
|
43
54
|
progress indicator. Pass `autoSaveKey` to persist a versioned draft in `localStorage` after a 500ms debounce and restore it
|
|
44
55
|
on the next mount:
|
package/dist/index.cjs
CHANGED
|
@@ -3276,8 +3276,11 @@ function describedBy(field, error, helpId, errorId) {
|
|
|
3276
3276
|
);
|
|
3277
3277
|
return ids.length === 0 ? void 0 : ids.join(" ");
|
|
3278
3278
|
}
|
|
3279
|
-
function RequiredMark({
|
|
3280
|
-
|
|
3279
|
+
function RequiredMark({
|
|
3280
|
+
required,
|
|
3281
|
+
className = "fe-required"
|
|
3282
|
+
}) {
|
|
3283
|
+
return required ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className, "aria-hidden": "true", children: [
|
|
3281
3284
|
" ",
|
|
3282
3285
|
"*"
|
|
3283
3286
|
] }) : null;
|
|
@@ -3288,13 +3291,59 @@ function FieldMessage({ props }) {
|
|
|
3288
3291
|
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
3292
|
] });
|
|
3290
3293
|
}
|
|
3291
|
-
function
|
|
3294
|
+
function GroupedChoiceDescription({ props }) {
|
|
3295
|
+
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 });
|
|
3296
|
+
}
|
|
3297
|
+
function GroupedChoiceError({ props }) {
|
|
3298
|
+
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
|
+
}
|
|
3300
|
+
function DefaultField({
|
|
3301
|
+
groupedChoiceFields,
|
|
3302
|
+
...props
|
|
3303
|
+
}) {
|
|
3292
3304
|
const { field, value, setValue, inputId, error, translate } = props;
|
|
3293
3305
|
const ariaProps = {
|
|
3294
3306
|
"aria-describedby": describedBy(field, error, props.helpId, props.errorId),
|
|
3295
3307
|
"aria-invalid": error === void 0 ? void 0 : true
|
|
3296
3308
|
};
|
|
3297
3309
|
if (field.type === "checkbox") {
|
|
3310
|
+
if (groupedChoiceFields) {
|
|
3311
|
+
return (
|
|
3312
|
+
// biome-ignore lint/a11y/useAriaPropsSupportedByRole: The grouped fieldset exposes the required state for the complete choice question.
|
|
3313
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
3314
|
+
"fieldset",
|
|
3315
|
+
{
|
|
3316
|
+
className: "fe-choice-group fe-field--checkbox",
|
|
3317
|
+
"data-field-id": field.id,
|
|
3318
|
+
"aria-describedby": describedBy(field, error, props.helpId, props.errorId),
|
|
3319
|
+
"aria-invalid": Boolean(error),
|
|
3320
|
+
"aria-required": field.required,
|
|
3321
|
+
children: [
|
|
3322
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("legend", { className: "fe-choice-legend", children: [
|
|
3323
|
+
field.title,
|
|
3324
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(RequiredMark, { required: field.required, className: "fe-required-badge" })
|
|
3325
|
+
] }),
|
|
3326
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(GroupedChoiceDescription, { props }),
|
|
3327
|
+
/* @__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: [
|
|
3328
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
3329
|
+
"input",
|
|
3330
|
+
{
|
|
3331
|
+
id: inputId,
|
|
3332
|
+
name: field.id,
|
|
3333
|
+
type: "checkbox",
|
|
3334
|
+
checked: value === true,
|
|
3335
|
+
"aria-label": field.title,
|
|
3336
|
+
onChange: (event) => setValue(event.currentTarget.checked)
|
|
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
|
+
)
|
|
3345
|
+
);
|
|
3346
|
+
}
|
|
3298
3347
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "fe-field fe-field--checkbox", "data-field-id": field.id, children: [
|
|
3299
3348
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { className: "fe-check-label", htmlFor: inputId, children: [
|
|
3300
3349
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
@@ -3318,6 +3367,85 @@ function DefaultField(props) {
|
|
|
3318
3367
|
}
|
|
3319
3368
|
if (field.type === "radio" || field.type === "multi-select") {
|
|
3320
3369
|
const selected = Array.isArray(value) ? value : [];
|
|
3370
|
+
if (field.type === "radio" && groupedChoiceFields) {
|
|
3371
|
+
return (
|
|
3372
|
+
// biome-ignore lint/a11y/useAriaPropsSupportedByRole: The grouped fieldset exposes the required state for the complete choice question.
|
|
3373
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
3374
|
+
"fieldset",
|
|
3375
|
+
{
|
|
3376
|
+
className: "fe-choice-group fe-field--radio",
|
|
3377
|
+
"data-field-id": field.id,
|
|
3378
|
+
"aria-describedby": describedBy(field, error, props.helpId, props.errorId),
|
|
3379
|
+
"aria-invalid": Boolean(error),
|
|
3380
|
+
"aria-required": field.required,
|
|
3381
|
+
children: [
|
|
3382
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("legend", { className: "fe-choice-legend", children: [
|
|
3383
|
+
field.title,
|
|
3384
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(RequiredMark, { required: field.required, className: "fe-required-badge" })
|
|
3385
|
+
] }),
|
|
3386
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(GroupedChoiceDescription, { props }),
|
|
3387
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "fe-choice-options", children: field.options.map((option, index) => {
|
|
3388
|
+
const optionId = `${inputId}-${index}`;
|
|
3389
|
+
const checked = value === option.id;
|
|
3390
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { className: "fe-choice-option", htmlFor: optionId, children: [
|
|
3391
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
3392
|
+
"input",
|
|
3393
|
+
{
|
|
3394
|
+
id: optionId,
|
|
3395
|
+
name: field.id,
|
|
3396
|
+
type: "radio",
|
|
3397
|
+
value: option.id,
|
|
3398
|
+
checked,
|
|
3399
|
+
onKeyDown: (event) => {
|
|
3400
|
+
if (event.key !== "ArrowDown" && event.key !== "ArrowRight" && event.key !== "ArrowUp" && event.key !== "ArrowLeft")
|
|
3401
|
+
return;
|
|
3402
|
+
event.preventDefault();
|
|
3403
|
+
const offset = event.key === "ArrowDown" || event.key === "ArrowRight" ? 1 : -1;
|
|
3404
|
+
const nextIndex = (index + offset + field.options.length) % field.options.length;
|
|
3405
|
+
const nextOption = field.options[nextIndex];
|
|
3406
|
+
if (nextOption === void 0) return;
|
|
3407
|
+
setValue(nextOption.id);
|
|
3408
|
+
document.getElementById(`${inputId}-${nextIndex}`)?.focus();
|
|
3409
|
+
},
|
|
3410
|
+
onChange: () => setValue(option.id)
|
|
3411
|
+
}
|
|
3412
|
+
),
|
|
3413
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: option.label })
|
|
3414
|
+
] }, option.id);
|
|
3415
|
+
}) }),
|
|
3416
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(GroupedChoiceError, { props })
|
|
3417
|
+
]
|
|
3418
|
+
}
|
|
3419
|
+
)
|
|
3420
|
+
);
|
|
3421
|
+
}
|
|
3422
|
+
if (field.type === "radio") {
|
|
3423
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "fe-field fe-field--radio", "data-field-id": field.id, children: [
|
|
3424
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "fe-label", children: [
|
|
3425
|
+
field.title,
|
|
3426
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(RequiredMark, { required: field.required })
|
|
3427
|
+
] }),
|
|
3428
|
+
field.options.map((option, index) => {
|
|
3429
|
+
const optionId = `${inputId}-${index}`;
|
|
3430
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { className: "fe-check-label", htmlFor: optionId, children: [
|
|
3431
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
3432
|
+
"input",
|
|
3433
|
+
{
|
|
3434
|
+
...ariaProps,
|
|
3435
|
+
id: optionId,
|
|
3436
|
+
name: field.id,
|
|
3437
|
+
type: "radio",
|
|
3438
|
+
value: option.id,
|
|
3439
|
+
checked: value === option.id,
|
|
3440
|
+
onChange: () => setValue(option.id)
|
|
3441
|
+
}
|
|
3442
|
+
),
|
|
3443
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: option.label })
|
|
3444
|
+
] }, option.id);
|
|
3445
|
+
}),
|
|
3446
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(FieldMessage, { props })
|
|
3447
|
+
] });
|
|
3448
|
+
}
|
|
3321
3449
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("fieldset", { className: `fe-field fe-field--${field.type}`, "data-field-id": field.id, ...ariaProps, children: [
|
|
3322
3450
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("legend", { className: "fe-label", children: [
|
|
3323
3451
|
field.title,
|
|
@@ -3567,6 +3695,8 @@ function ContextFormRenderer({
|
|
|
3567
3695
|
beforeSubmit,
|
|
3568
3696
|
onDraftSave,
|
|
3569
3697
|
successRenderMode = "append",
|
|
3698
|
+
appearance,
|
|
3699
|
+
groupedChoiceFields = false,
|
|
3570
3700
|
submissionConfirmation,
|
|
3571
3701
|
submissionConfirmationRenderMode,
|
|
3572
3702
|
showHiddenFieldsInSummary = false,
|
|
@@ -3579,6 +3709,7 @@ function ContextFormRenderer({
|
|
|
3579
3709
|
slots = {}
|
|
3580
3710
|
}) {
|
|
3581
3711
|
const form = useForm();
|
|
3712
|
+
const isGroupedMode = appearance?.choiceField === "grouped" || groupedChoiceFields;
|
|
3582
3713
|
const prefix = (0, import_react5.useId)().replace(/:/g, "");
|
|
3583
3714
|
const formRef = (0, import_react5.useRef)(null);
|
|
3584
3715
|
const loadedDraftKey = (0, import_react5.useRef)(null);
|
|
@@ -4085,7 +4216,7 @@ function ContextFormRenderer({
|
|
|
4085
4216
|
}) }, field.id);
|
|
4086
4217
|
}
|
|
4087
4218
|
const Component = components[field.type];
|
|
4088
|
-
return Component === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(DefaultField, { ...props }, field.id) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Component, { ...props }, field.id);
|
|
4219
|
+
return Component === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(DefaultField, { ...props, groupedChoiceFields: isGroupedMode }, field.id) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Component, { ...props }, field.id);
|
|
4089
4220
|
});
|
|
4090
4221
|
const fieldClassName = `fe-fields${fieldsClassName === void 0 ? "" : ` ${fieldsClassName}`}`;
|
|
4091
4222
|
return slots.renderFields?.({ children: fieldChildren, className: fieldClassName }) ?? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: fieldClassName, children: fieldChildren });
|
package/dist/index.d.cts
CHANGED
|
@@ -477,6 +477,15 @@ 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 FormRendererAppearance {
|
|
482
|
+
/**
|
|
483
|
+
* Layout preset for radio and checkbox questions.
|
|
484
|
+
* - "default": keep the flat question layout.
|
|
485
|
+
* - "grouped": render a bordered fieldset and legend.
|
|
486
|
+
*/
|
|
487
|
+
readonly choiceField?: ChoiceFieldLayoutMode;
|
|
488
|
+
}
|
|
480
489
|
type SubmissionConfirmationRenderMode = "inline" | "replace" | "dialog";
|
|
481
490
|
interface SubmissionConfirmationOptions {
|
|
482
491
|
readonly enabled?: boolean;
|
|
@@ -671,6 +680,9 @@ type FieldComponents = Partial<Record<FieldType, ComponentType<FieldComponentPro
|
|
|
671
680
|
interface FormRendererPresentationProps extends SubmissionProtectionProps {
|
|
672
681
|
readonly components?: FieldComponents;
|
|
673
682
|
readonly className?: string;
|
|
683
|
+
readonly appearance?: FormRendererAppearance;
|
|
684
|
+
/** @deprecated Use appearance.choiceField="grouped" instead. */
|
|
685
|
+
readonly groupedChoiceFields?: boolean;
|
|
674
686
|
/**
|
|
675
687
|
* Controls where the completion message is rendered after a successful submission.
|
|
676
688
|
* Defaults to "append" for backwards compatibility.
|
|
@@ -704,4 +716,4 @@ interface StandaloneFormRendererProps extends FormRendererPresentationProps {
|
|
|
704
716
|
type FormRendererProps = FormRendererPresentationProps | StandaloneFormRendererProps;
|
|
705
717
|
declare function FormRenderer(props: FormRendererProps): react.JSX.Element;
|
|
706
718
|
|
|
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 };
|
|
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 };
|
package/dist/index.d.ts
CHANGED
|
@@ -477,6 +477,15 @@ 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 FormRendererAppearance {
|
|
482
|
+
/**
|
|
483
|
+
* Layout preset for radio and checkbox questions.
|
|
484
|
+
* - "default": keep the flat question layout.
|
|
485
|
+
* - "grouped": render a bordered fieldset and legend.
|
|
486
|
+
*/
|
|
487
|
+
readonly choiceField?: ChoiceFieldLayoutMode;
|
|
488
|
+
}
|
|
480
489
|
type SubmissionConfirmationRenderMode = "inline" | "replace" | "dialog";
|
|
481
490
|
interface SubmissionConfirmationOptions {
|
|
482
491
|
readonly enabled?: boolean;
|
|
@@ -671,6 +680,9 @@ type FieldComponents = Partial<Record<FieldType, ComponentType<FieldComponentPro
|
|
|
671
680
|
interface FormRendererPresentationProps extends SubmissionProtectionProps {
|
|
672
681
|
readonly components?: FieldComponents;
|
|
673
682
|
readonly className?: string;
|
|
683
|
+
readonly appearance?: FormRendererAppearance;
|
|
684
|
+
/** @deprecated Use appearance.choiceField="grouped" instead. */
|
|
685
|
+
readonly groupedChoiceFields?: boolean;
|
|
674
686
|
/**
|
|
675
687
|
* Controls where the completion message is rendered after a successful submission.
|
|
676
688
|
* Defaults to "append" for backwards compatibility.
|
|
@@ -704,4 +716,4 @@ interface StandaloneFormRendererProps extends FormRendererPresentationProps {
|
|
|
704
716
|
type FormRendererProps = FormRendererPresentationProps | StandaloneFormRendererProps;
|
|
705
717
|
declare function FormRenderer(props: FormRendererProps): react.JSX.Element;
|
|
706
718
|
|
|
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 };
|
|
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 };
|
package/dist/index.js
CHANGED
|
@@ -3259,8 +3259,11 @@ function describedBy(field, error, helpId, errorId) {
|
|
|
3259
3259
|
);
|
|
3260
3260
|
return ids.length === 0 ? void 0 : ids.join(" ");
|
|
3261
3261
|
}
|
|
3262
|
-
function RequiredMark({
|
|
3263
|
-
|
|
3262
|
+
function RequiredMark({
|
|
3263
|
+
required,
|
|
3264
|
+
className = "fe-required"
|
|
3265
|
+
}) {
|
|
3266
|
+
return required ? /* @__PURE__ */ jsxs2("span", { className, "aria-hidden": "true", children: [
|
|
3264
3267
|
" ",
|
|
3265
3268
|
"*"
|
|
3266
3269
|
] }) : null;
|
|
@@ -3271,13 +3274,59 @@ function FieldMessage({ props }) {
|
|
|
3271
3274
|
props.error === void 0 ? null : /* @__PURE__ */ jsx3("div", { id: props.errorId, className: "fe-error", children: props.translate(props.error.messageKey, props.error.params) })
|
|
3272
3275
|
] });
|
|
3273
3276
|
}
|
|
3274
|
-
function
|
|
3277
|
+
function GroupedChoiceDescription({ props }) {
|
|
3278
|
+
return props.field.description === void 0 ? null : /* @__PURE__ */ jsx3("div", { id: props.helpId, className: "fe-field-description", children: props.field.description });
|
|
3279
|
+
}
|
|
3280
|
+
function GroupedChoiceError({ props }) {
|
|
3281
|
+
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
|
+
}
|
|
3283
|
+
function DefaultField({
|
|
3284
|
+
groupedChoiceFields,
|
|
3285
|
+
...props
|
|
3286
|
+
}) {
|
|
3275
3287
|
const { field, value, setValue, inputId, error, translate } = props;
|
|
3276
3288
|
const ariaProps = {
|
|
3277
3289
|
"aria-describedby": describedBy(field, error, props.helpId, props.errorId),
|
|
3278
3290
|
"aria-invalid": error === void 0 ? void 0 : true
|
|
3279
3291
|
};
|
|
3280
3292
|
if (field.type === "checkbox") {
|
|
3293
|
+
if (groupedChoiceFields) {
|
|
3294
|
+
return (
|
|
3295
|
+
// biome-ignore lint/a11y/useAriaPropsSupportedByRole: The grouped fieldset exposes the required state for the complete choice question.
|
|
3296
|
+
/* @__PURE__ */ jsxs2(
|
|
3297
|
+
"fieldset",
|
|
3298
|
+
{
|
|
3299
|
+
className: "fe-choice-group fe-field--checkbox",
|
|
3300
|
+
"data-field-id": field.id,
|
|
3301
|
+
"aria-describedby": describedBy(field, error, props.helpId, props.errorId),
|
|
3302
|
+
"aria-invalid": Boolean(error),
|
|
3303
|
+
"aria-required": field.required,
|
|
3304
|
+
children: [
|
|
3305
|
+
/* @__PURE__ */ jsxs2("legend", { className: "fe-choice-legend", children: [
|
|
3306
|
+
field.title,
|
|
3307
|
+
/* @__PURE__ */ jsx3(RequiredMark, { required: field.required, className: "fe-required-badge" })
|
|
3308
|
+
] }),
|
|
3309
|
+
/* @__PURE__ */ jsx3(GroupedChoiceDescription, { props }),
|
|
3310
|
+
/* @__PURE__ */ jsx3("div", { className: "fe-choice-options", children: /* @__PURE__ */ jsxs2("label", { className: "fe-choice-option", htmlFor: inputId, children: [
|
|
3311
|
+
/* @__PURE__ */ jsx3(
|
|
3312
|
+
"input",
|
|
3313
|
+
{
|
|
3314
|
+
id: inputId,
|
|
3315
|
+
name: field.id,
|
|
3316
|
+
type: "checkbox",
|
|
3317
|
+
checked: value === true,
|
|
3318
|
+
"aria-label": field.title,
|
|
3319
|
+
onChange: (event) => setValue(event.currentTarget.checked)
|
|
3320
|
+
}
|
|
3321
|
+
),
|
|
3322
|
+
/* @__PURE__ */ jsx3("span", { children: field.title })
|
|
3323
|
+
] }) }),
|
|
3324
|
+
/* @__PURE__ */ jsx3(GroupedChoiceError, { props })
|
|
3325
|
+
]
|
|
3326
|
+
}
|
|
3327
|
+
)
|
|
3328
|
+
);
|
|
3329
|
+
}
|
|
3281
3330
|
return /* @__PURE__ */ jsxs2("div", { className: "fe-field fe-field--checkbox", "data-field-id": field.id, children: [
|
|
3282
3331
|
/* @__PURE__ */ jsxs2("label", { className: "fe-check-label", htmlFor: inputId, children: [
|
|
3283
3332
|
/* @__PURE__ */ jsx3(
|
|
@@ -3301,6 +3350,85 @@ function DefaultField(props) {
|
|
|
3301
3350
|
}
|
|
3302
3351
|
if (field.type === "radio" || field.type === "multi-select") {
|
|
3303
3352
|
const selected = Array.isArray(value) ? value : [];
|
|
3353
|
+
if (field.type === "radio" && groupedChoiceFields) {
|
|
3354
|
+
return (
|
|
3355
|
+
// biome-ignore lint/a11y/useAriaPropsSupportedByRole: The grouped fieldset exposes the required state for the complete choice question.
|
|
3356
|
+
/* @__PURE__ */ jsxs2(
|
|
3357
|
+
"fieldset",
|
|
3358
|
+
{
|
|
3359
|
+
className: "fe-choice-group fe-field--radio",
|
|
3360
|
+
"data-field-id": field.id,
|
|
3361
|
+
"aria-describedby": describedBy(field, error, props.helpId, props.errorId),
|
|
3362
|
+
"aria-invalid": Boolean(error),
|
|
3363
|
+
"aria-required": field.required,
|
|
3364
|
+
children: [
|
|
3365
|
+
/* @__PURE__ */ jsxs2("legend", { className: "fe-choice-legend", children: [
|
|
3366
|
+
field.title,
|
|
3367
|
+
/* @__PURE__ */ jsx3(RequiredMark, { required: field.required, className: "fe-required-badge" })
|
|
3368
|
+
] }),
|
|
3369
|
+
/* @__PURE__ */ jsx3(GroupedChoiceDescription, { props }),
|
|
3370
|
+
/* @__PURE__ */ jsx3("div", { className: "fe-choice-options", children: field.options.map((option, index) => {
|
|
3371
|
+
const optionId = `${inputId}-${index}`;
|
|
3372
|
+
const checked = value === option.id;
|
|
3373
|
+
return /* @__PURE__ */ jsxs2("label", { className: "fe-choice-option", htmlFor: optionId, children: [
|
|
3374
|
+
/* @__PURE__ */ jsx3(
|
|
3375
|
+
"input",
|
|
3376
|
+
{
|
|
3377
|
+
id: optionId,
|
|
3378
|
+
name: field.id,
|
|
3379
|
+
type: "radio",
|
|
3380
|
+
value: option.id,
|
|
3381
|
+
checked,
|
|
3382
|
+
onKeyDown: (event) => {
|
|
3383
|
+
if (event.key !== "ArrowDown" && event.key !== "ArrowRight" && event.key !== "ArrowUp" && event.key !== "ArrowLeft")
|
|
3384
|
+
return;
|
|
3385
|
+
event.preventDefault();
|
|
3386
|
+
const offset = event.key === "ArrowDown" || event.key === "ArrowRight" ? 1 : -1;
|
|
3387
|
+
const nextIndex = (index + offset + field.options.length) % field.options.length;
|
|
3388
|
+
const nextOption = field.options[nextIndex];
|
|
3389
|
+
if (nextOption === void 0) return;
|
|
3390
|
+
setValue(nextOption.id);
|
|
3391
|
+
document.getElementById(`${inputId}-${nextIndex}`)?.focus();
|
|
3392
|
+
},
|
|
3393
|
+
onChange: () => setValue(option.id)
|
|
3394
|
+
}
|
|
3395
|
+
),
|
|
3396
|
+
/* @__PURE__ */ jsx3("span", { children: option.label })
|
|
3397
|
+
] }, option.id);
|
|
3398
|
+
}) }),
|
|
3399
|
+
/* @__PURE__ */ jsx3(GroupedChoiceError, { props })
|
|
3400
|
+
]
|
|
3401
|
+
}
|
|
3402
|
+
)
|
|
3403
|
+
);
|
|
3404
|
+
}
|
|
3405
|
+
if (field.type === "radio") {
|
|
3406
|
+
return /* @__PURE__ */ jsxs2("div", { className: "fe-field fe-field--radio", "data-field-id": field.id, children: [
|
|
3407
|
+
/* @__PURE__ */ jsxs2("div", { className: "fe-label", children: [
|
|
3408
|
+
field.title,
|
|
3409
|
+
/* @__PURE__ */ jsx3(RequiredMark, { required: field.required })
|
|
3410
|
+
] }),
|
|
3411
|
+
field.options.map((option, index) => {
|
|
3412
|
+
const optionId = `${inputId}-${index}`;
|
|
3413
|
+
return /* @__PURE__ */ jsxs2("label", { className: "fe-check-label", htmlFor: optionId, children: [
|
|
3414
|
+
/* @__PURE__ */ jsx3(
|
|
3415
|
+
"input",
|
|
3416
|
+
{
|
|
3417
|
+
...ariaProps,
|
|
3418
|
+
id: optionId,
|
|
3419
|
+
name: field.id,
|
|
3420
|
+
type: "radio",
|
|
3421
|
+
value: option.id,
|
|
3422
|
+
checked: value === option.id,
|
|
3423
|
+
onChange: () => setValue(option.id)
|
|
3424
|
+
}
|
|
3425
|
+
),
|
|
3426
|
+
/* @__PURE__ */ jsx3("span", { children: option.label })
|
|
3427
|
+
] }, option.id);
|
|
3428
|
+
}),
|
|
3429
|
+
/* @__PURE__ */ jsx3(FieldMessage, { props })
|
|
3430
|
+
] });
|
|
3431
|
+
}
|
|
3304
3432
|
return /* @__PURE__ */ jsxs2("fieldset", { className: `fe-field fe-field--${field.type}`, "data-field-id": field.id, ...ariaProps, children: [
|
|
3305
3433
|
/* @__PURE__ */ jsxs2("legend", { className: "fe-label", children: [
|
|
3306
3434
|
field.title,
|
|
@@ -3550,6 +3678,8 @@ function ContextFormRenderer({
|
|
|
3550
3678
|
beforeSubmit,
|
|
3551
3679
|
onDraftSave,
|
|
3552
3680
|
successRenderMode = "append",
|
|
3681
|
+
appearance,
|
|
3682
|
+
groupedChoiceFields = false,
|
|
3553
3683
|
submissionConfirmation,
|
|
3554
3684
|
submissionConfirmationRenderMode,
|
|
3555
3685
|
showHiddenFieldsInSummary = false,
|
|
@@ -3562,6 +3692,7 @@ function ContextFormRenderer({
|
|
|
3562
3692
|
slots = {}
|
|
3563
3693
|
}) {
|
|
3564
3694
|
const form = useForm();
|
|
3695
|
+
const isGroupedMode = appearance?.choiceField === "grouped" || groupedChoiceFields;
|
|
3565
3696
|
const prefix = useId().replace(/:/g, "");
|
|
3566
3697
|
const formRef = useRef2(null);
|
|
3567
3698
|
const loadedDraftKey = useRef2(null);
|
|
@@ -4068,7 +4199,7 @@ function ContextFormRenderer({
|
|
|
4068
4199
|
}) }, field.id);
|
|
4069
4200
|
}
|
|
4070
4201
|
const Component = components[field.type];
|
|
4071
|
-
return Component === void 0 ? /* @__PURE__ */ jsx3(DefaultField, { ...props }, field.id) : /* @__PURE__ */ jsx3(Component, { ...props }, field.id);
|
|
4202
|
+
return Component === void 0 ? /* @__PURE__ */ jsx3(DefaultField, { ...props, groupedChoiceFields: isGroupedMode }, field.id) : /* @__PURE__ */ jsx3(Component, { ...props }, field.id);
|
|
4072
4203
|
});
|
|
4073
4204
|
const fieldClassName = `fe-fields${fieldsClassName === void 0 ? "" : ` ${fieldsClassName}`}`;
|
|
4074
4205
|
return slots.renderFields?.({ children: fieldChildren, className: fieldClassName }) ?? /* @__PURE__ */ jsx3("div", { className: fieldClassName, children: fieldChildren });
|
package/dist/styles.css
CHANGED
|
@@ -123,6 +123,41 @@
|
|
|
123
123
|
.fe-check-label input {
|
|
124
124
|
margin-top: 0.22rem;
|
|
125
125
|
}
|
|
126
|
+
.fe-choice-group {
|
|
127
|
+
background-color: var(--fe-bg-color, #ffffff);
|
|
128
|
+
border: 1px solid var(--fe-border-color, #e2e8f0);
|
|
129
|
+
border-radius: var(--fe-radius, 8px);
|
|
130
|
+
box-sizing: border-box;
|
|
131
|
+
margin: 0 0 1.25rem;
|
|
132
|
+
padding: 1rem 1.25rem;
|
|
133
|
+
}
|
|
134
|
+
.fe-choice-legend {
|
|
135
|
+
color: var(--fe-text-color, #1a202c);
|
|
136
|
+
font-size: 0.95rem;
|
|
137
|
+
font-weight: 600;
|
|
138
|
+
padding: 0 0.5rem;
|
|
139
|
+
}
|
|
140
|
+
.fe-choice-options {
|
|
141
|
+
display: flex;
|
|
142
|
+
flex-direction: column;
|
|
143
|
+
gap: 0.625rem;
|
|
144
|
+
margin-top: 0.5rem;
|
|
145
|
+
}
|
|
146
|
+
.fe-choice-option {
|
|
147
|
+
align-items: center;
|
|
148
|
+
cursor: pointer;
|
|
149
|
+
display: flex;
|
|
150
|
+
gap: 0.5rem;
|
|
151
|
+
}
|
|
152
|
+
.fe-field-description {
|
|
153
|
+
color: #667085;
|
|
154
|
+
font-size: 0.875rem;
|
|
155
|
+
}
|
|
156
|
+
.fe-field-error {
|
|
157
|
+
color: #b42318;
|
|
158
|
+
font-size: 0.875rem;
|
|
159
|
+
font-weight: 600;
|
|
160
|
+
}
|
|
126
161
|
.fe-rating-options {
|
|
127
162
|
display: flex;
|
|
128
163
|
flex-wrap: wrap;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@form-engine-ts/react",
|
|
3
|
-
"version": "4.3.
|
|
3
|
+
"version": "4.3.2",
|
|
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.3.
|
|
46
|
-
"@form-engine-ts/privacy": "4.3.
|
|
45
|
+
"@form-engine-ts/core": "4.3.2",
|
|
46
|
+
"@form-engine-ts/privacy": "4.3.2"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
49
|
"react": ">=18.2 <20",
|