impl/typescript.tsx
3,269 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 { useId, useState, type JSX, type ReactNode } from "react";
import type { DateParts } from "./form_date_parts.ts"; ← from form.date-parts ^1.0.0 · built alongside by fune
import { Fieldset } from "./react_form_fieldset.ts"; ← from react.form.fieldset ^1.0.0 · built alongside by fune
import { FormField } from "./react_form_form_field.ts"; ← from react.form.form-field ^1.0.0 · built alongside by fune
import type { DateInputProps } from "./react_form_date_input_types.ts";
type Part = "day" | "month" | "year";
const BOXES: readonly { part: Part; label: string; width: string; autoComplete: string }[] = [
{ part: "day", label: "Day", width: "fune-input--width-2", autoComplete: "bday-day" },
{ part: "month", label: "Month", width: "fune-input--width-2", autoComplete: "bday-month" },
{ part: "year", label: "Year", width: "fune-input--width-4", autoComplete: "bday-year" },
];
function present(node: ReactNode): boolean {
return node !== null && node !== undefined && node !== false && node !== "";
}
/**
* The GOV.UK date input: a fieldset whose legend asks for the date, the hint
* and error message described to the whole group, and three text boxes with
* a numeric keyboard (not type="number", which GOV.UK advises against). With
* an error, the boxes named in errorFields are marked, or all three when it
* names none, as GOV.UK does for an error about the whole date.
*/
export function DateInput(props: DateInputProps): JSX.Element {
const auto = useId();
const { legend, hint, error, errorFields, value, defaultValue, onChange, onBlur, birthday, isPageHeading, disabled, className } = props;
const id = props.id ?? auto;
const name = props.name ?? id;
// The boxes' text as it stands, so onChange can hand over the whole date
// even when the input is uncontrolled.
const [own, setOwn] = useState<DateParts>(value ?? defaultValue ?? { day: "", month: "", year: "" });
const current = value ?? own;
const inError = present(error);
const marked = (part: Part) => inError && (!errorFields || errorFields.length === 0 || errorFields.includes(part));
function change(part: Part, text: string): void {
const next: DateParts = { ...current, [part]: text };
setOwn(next);
onChange?.(next);
}
return (
<Fieldset id={id} legend={legend} hint={hint} error={error} isPageHeading={isPageHeading} className={className}>
<div className="fune-date-input" id={id}>
{BOXES.map(({ part, label, width, autoComplete }) => (
<div className="fune-date-input-item" key={part}>
<FormField id={`${id}-${part}`} label={label}>
<input
className={["fune-input", marked(part) ? "fune-input--error" : null, width].filter(Boolean).join(" ")}
id={`${id}-${part}`}
name={`${name}-${part}`}
type="text"
inputMode="numeric"
autoComplete={birthday ? autoComplete : undefined}
disabled={disabled}
aria-invalid={marked(part) ? true : undefined}
value={value ? value[part] : undefined}
defaultValue={value ? undefined : defaultValue?.[part]}
onChange={(event) => change(part, event.target.value)}
onBlur={onBlur ? () => onBlur() : undefined}
/>
</FormField>
</div>
))}
</div>
</Fieldset>
);
}