impl/typescript.tsx
3,236 bytes · the TypeScript implementation · view raw
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
"use client";
import { Fragment, useId, type JSX, type ReactNode } from "react";
import { Fieldset } from "./react_form_fieldset.ts"; ← from react.form.fieldset ^1.0.0 · built alongside by fune
import type { RadioGroupProps } from "./react_form_radio_group_types.ts";
function present(node: ReactNode): boolean {
return node !== null && node !== undefined && node !== false && node !== "";
}
/**
* GOV.UK radios: a fieldset whose legend asks the question, its hint and
* error describing the whole group, then one radio per option, all under one
* name. The first radio takes the id, so an error summary's link lands on
* it, and the rest are <id>-2, <id>-3...; the group's hint and error are
* <id>-hint and <id>-error. A radio has no state of its own to keep: the browser
* knows which is chosen, so value makes it controlled and defaultValue does
* not.
*/
export function RadioGroup(props: RadioGroupProps): JSX.Element {
const auto = useId();
const { legend, options, value, defaultValue, onChange, onBlur, hint, error, isPageHeading, inline, small, required, disabled, className } = props;
const id = props.id ?? auto;
const name = props.name ?? id;
if (options.length === 0) throw new Error("a radio group needs at least one option");
const seen = new Set<string>();
for (const o of options) {
if (seen.has(o.value)) throw new Error(`radio option values must be unique: "${o.value}" is there twice`);
seen.add(o.value);
}
const classes = ["fune-radios", inline ? "fune-radios--inline" : null, small ? "fune-radios--small" : null].filter(Boolean).join(" ");
return (
<Fieldset id={id} legend={legend} hint={hint} error={error} isPageHeading={isPageHeading} className={className}>
<div className={classes}>
{options.map((o, i) => {
const itemId = i === 0 ? id : `${id}-${i + 1}`;
const hintId = present(o.hint) ? `${itemId}-item-hint` : undefined;
return (
<Fragment key={o.value}>
{present(o.divider) ? <div className="fune-radios-divider">{o.divider}</div> : null}
<div className="fune-radio">
<input
className="fune-radio-input"
id={itemId}
name={name}
type="radio"
value={o.value}
checked={value === undefined ? undefined : value === o.value}
defaultChecked={value === undefined && defaultValue !== undefined ? defaultValue === o.value : undefined}
required={required}
disabled={disabled || o.disabled}
aria-describedby={hintId}
onBlur={onBlur ? () => onBlur() : undefined}
onChange={(event) => {
if (event.target.checked) onChange?.(o.value);
}}
/>
<label className="fune-label fune-radio-label" htmlFor={itemId}>
{o.label}
</label>
{hintId ? (
<div className="fune-hint fune-radio-hint" id={hintId}>
{o.hint}
</div>
) : null}
</div>
</Fragment>
);
})}
</div>
</Fieldset>
);
}