Functional Weave
Code in Python

react.form.date-input@1.0.1

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>
  );
}