import React, { useId, memo, forwardRef, type ReactNode, type CSSProperties, type ComponentProps } from "react"; import { symToStr } from "tsafe/symToStr"; import { assert } from "tsafe/assert"; import type { Equals } from "tsafe"; import { cx } from "../tools/cx"; import { fr } from "../fr"; import { useAnalyticsId } from "../tools/useAnalyticsId"; export type FieldsetProps = FieldsetProps.Radio | FieldsetProps.Checkbox; export namespace FieldsetProps { export type Common = { className?: string; id?: string; classes?: Partial>; style?: CSSProperties; legend?: ReactNode; hintText?: ReactNode; options: { label: ReactNode; hintText?: ReactNode; nativeInputProps: ComponentProps<"input">; }[]; /** Default: "vertical" */ orientation?: "vertical" | "horizontal"; /** Default: "default" */ state?: "success" | "error" | "default"; /** * The message won't be displayed if state is "default". * If the state is "error" providing a message is mandatory **/ stateRelatedMessage?: ReactNode; /** Default: false */ disabled?: boolean; /** default: false */ small?: boolean; }; export type Radio = Omit & { type: "radio"; name?: string; options: (Common["options"][number] & { illustration?: ReactNode; })[]; }; export type Checkbox = Common & { type: "checkbox"; name?: never; }; } /** @see */ export const Fieldset = memo( forwardRef((props, ref) => { const { className, id: id_props, classes = {}, style, legend, hintText, options, orientation = "vertical", state = "default", stateRelatedMessage, disabled = false, type, name: name_props, small = false, ...rest } = props; const isRichRadio = type === "radio" && options.find(options => options.illustration !== undefined) !== undefined; assert>(); const id = useAnalyticsId({ "defaultIdPrefix": `fr-fieldset-${type}${ name_props === undefined ? "" : `-${name_props}` }`, "explicitlyProvidedId": id_props }); const getInputId = (i: number) => `${id}-${i}`; const legendId = `${id}-legend`; const errorDescId = `${id}-desc-error`; const successDescId = `${id}-desc-valid`; const messagesWrapperId = `${id}-messages`; const radioName = (function useClosure() { const id = useId(); return name_props ?? `radio-name-${id}`; })(); return (
{ switch (state) { case "default": return undefined; case "error": return "fr-fieldset--error"; case "success": return "fr-fieldset--valid"; } })() ), classes.root, className )} disabled={disabled} style={style} aria-labelledby={cx(legend !== undefined && legendId, messagesWrapperId)} role={state === "default" ? undefined : "group"} {...rest} ref={ref} > {legend !== undefined && ( {legend} {hintText !== undefined && ( {hintText} )} )}
{options.map(({ label, hintText, nativeInputProps, ...rest }, i) => (
{"illustration" in rest && (
{rest.illustration}
)}
))}
{stateRelatedMessage !== undefined && (

{ switch (state) { case "error": return errorDescId; case "success": return successDescId; } })()} className={fr.cx( "fr-message", (() => { switch (state) { case "error": return "fr-message--error"; case "success": return "fr-message--valid"; } })() )} > {stateRelatedMessage}

)}
); }) ); Fieldset.displayName = symToStr({ Fieldset }); export default Fieldset;