Functional Weave
Code in Python

react.form.textarea@1.0.1

impl/typescript.tsx

3,128 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 { characterCount } from "./form_character_count.ts";  ← from form.character-count ^1.0.0 · built alongside by fune
import { FormField, fieldIds } from "./react_form_form_field.ts";  ← from react.form.form-field ^1.0.0 · built alongside by fune
import type { TextareaProps } from "./react_form_textarea_types.ts";

function present(node: ReactNode): boolean {
  return node !== null && node !== undefined && node !== false && node !== "";
}

/**
 * The GOV.UK textarea, and with maxLength or maxWords the GOV.UK character
 * count: a message after the box, in a polite live region that describes it,
 * saying how many characters or words are left. The box is never cut off at
 * the limit (no maxlength attribute), so a person can paste and then edit
 * down; it is marked in error while over.
 */
export function Textarea(props: TextareaProps): JSX.Element {
  const auto = useId();
  const { label, hint, error, value, defaultValue, onChange, onBlur, autoComplete, spellCheck, maxLength, maxWords, threshold, required, disabled, className } = props;
  // The live count needs the text even when the textarea is uncontrolled.
  const [typed, setTyped] = useState(defaultValue ?? "");
  const id = props.id ?? auto;
  if (maxLength !== undefined && maxWords !== undefined) {
    throw new Error("a textarea counts characters or words, not both: give maxLength or maxWords");
  }
  if (threshold !== undefined && maxLength === undefined && maxWords === undefined) {
    throw new Error("a threshold needs a limit: give maxLength or maxWords as well");
  }
  const limit = maxLength ?? maxWords;
  const count = limit === undefined ? null : characterCount(value ?? typed, limit, maxWords !== undefined ? "words" : "characters", threshold ?? null);
  const ids = fieldIds(id, present(hint), present(error));
  const info = `${id}-info`;
  const describedBy = count ? [ids.describedBy, info].filter(Boolean).join(" ") : ids.describedBy;
  const wrapper = [count ? "fune-character-count" : null, className || null].filter(Boolean).join(" ");
  return (
    <FormField id={id} label={label} hint={hint} error={error} className={wrapper || undefined}>
      <textarea
        className={ids.error || count?.over ? "fune-textarea fune-textarea--error" : "fune-textarea"}
        id={ids.control}
        name={props.name ?? id}
        rows={props.rows ?? 5}
        value={value}
        defaultValue={defaultValue}
        autoComplete={autoComplete}
        spellCheck={spellCheck}
        required={required}
        disabled={disabled}
        aria-describedby={describedBy ?? undefined}
        aria-invalid={ids.error ? true : undefined}
        onChange={(event) => {
          setTyped(event.target.value);
          onChange?.(event.target.value);
        }}
        onBlur={onBlur ? () => onBlur() : undefined}
      />
      {count ? (
        <div
          className={count.over ? "fune-character-count__message fune-character-count__message--over" : "fune-character-count__message"}
          id={info}
          hidden={!count.visible}
          aria-live="polite"
        >
          {count.message}
        </div>
      ) : null}
    </FormField>
  );
}