All files / src/components/ui textarea.tsx

100% Statements 5/5
100% Branches 0/0
100% Functions 1/1
100% Lines 5/5

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89                                    72x                                                                   72x                         2291x   2291x                                 72x        
/**
 * Textarea — the Job Book's ruled multi-line field.
 *
 * `.textarea` in `.context/mockups/_base.css`: the same rule, ground and
 * radius as `.input`, a taller minimum, and a vertical resize handle.
 *
 * `CharCountTextarea` composes this (it is this control plus a counter), so
 * the field chrome lives in exactly one place.
 *
 * Error *messages* are not this component's job: wrap it in a `Field` and add
 * an `ErrorText`, which sets `aria-invalid` here automatically.
 */
 
import { cva, type VariantProps } from "class-variance-authority";
import { forwardRef, type TextareaHTMLAttributes } from "react";
import { cn } from "../../lib/utils";
import { useFieldControl } from "./field";
 
const textareaVariants = cva(
	"block min-h-21 w-full rounded-md border bg-background-base px-2.5 py-2 text-sm leading-relaxed text-foreground-default transition-colors placeholder:text-foreground-subtle focus-visible:border-foreground-default focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-foreground-default disabled:cursor-not-allowed disabled:opacity-50",
	{
		variants: {
			resize: {
				vertical: "resize-y",
				none: "resize-none",
			},
			invalid: {
				// Colour never travels alone: the ErrorText under the control is the
				// message, this is only the ink on the rule.
				true: "border-error ring-1 ring-error",
				// The portal's fields resolve their stroke through --border-strong,
				// 3:1 against both grounds (WCAG 1.4.11, UI component boundary).
				// --border-strong is 1.67:1 light / 1.91:1 dark and is NOT a legal
				// field stroke; it was a placeholder while the Earthy override
				// still shadowed the package tokens.
				false: "border-input",
			},
		},
		defaultVariants: {
			resize: "vertical",
			invalid: false,
		},
	},
);
 
interface TextareaProps
	extends TextareaHTMLAttributes<HTMLTextAreaElement>,
		Omit<VariantProps<typeof textareaVariants>, "invalid"> {
	/** Marks the control invalid. A `Field` with an `ErrorText` sets this too. */
	invalid?: boolean;
}
 
const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
	(
		{
			className,
			resize,
			invalid,
			id,
			required,
			"aria-describedby": describedBy,
			...props
		},
		ref,
	) => {
		const field = useFieldControl({ id, describedBy, invalid, required });
 
		return (
			<textarea
				ref={ref}
				id={field.id}
				required={field.required}
				aria-describedby={field["aria-describedby"]}
				aria-invalid={field["aria-invalid"]}
				className={cn(
					textareaVariants({ resize, invalid: field.invalid }),
					className,
				)}
				{...props}
			/>
		);
	},
);
 
Textarea.displayName = "Textarea";
 
export type { TextareaProps };
export { Textarea, textareaVariants };