99 lines
2.7 KiB
TypeScript
99 lines
2.7 KiB
TypeScript
// Formularelemente-Basis-Komponenten — SHL-01. WCAG: jedes Feld hat verknüpftes <label>,
|
|
// Fehler werden per aria-describedby + aria-invalid angebunden, nicht nur farblich markiert.
|
|
|
|
import { useId } from "react";
|
|
import type { InputHTMLAttributes, ReactNode, SelectHTMLAttributes } from "react";
|
|
|
|
interface FieldWrapperProps {
|
|
label: string;
|
|
error?: string;
|
|
hint?: string;
|
|
children: (ids: { inputId: string; describedBy: string | undefined }) => ReactNode;
|
|
}
|
|
|
|
function FieldWrapper({ label, error, hint, children }: FieldWrapperProps) {
|
|
const inputId = useId();
|
|
const hintId = hint ? `${inputId}-hint` : undefined;
|
|
const errorId = error ? `${inputId}-error` : undefined;
|
|
const describedBy = [hintId, errorId].filter(Boolean).join(" ") || undefined;
|
|
|
|
return (
|
|
<div className="shl-field">
|
|
<label htmlFor={inputId}>{label}</label>
|
|
{children({ inputId, describedBy })}
|
|
{hint && (
|
|
<p id={hintId} className="shl-field-hint">
|
|
{hint}
|
|
</p>
|
|
)}
|
|
{error && (
|
|
<p id={errorId} className="shl-field-error" role="alert">
|
|
{error}
|
|
</p>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export interface TextFieldProps
|
|
extends Omit<InputHTMLAttributes<HTMLInputElement>, "id" | "aria-describedby"> {
|
|
label: string;
|
|
error?: string;
|
|
hint?: string;
|
|
}
|
|
|
|
export function TextField({ label, error, hint, ...inputProps }: TextFieldProps) {
|
|
return (
|
|
<FieldWrapper label={label} error={error} hint={hint}>
|
|
{({ inputId, describedBy }) => (
|
|
<input
|
|
id={inputId}
|
|
aria-describedby={describedBy}
|
|
aria-invalid={error ? true : undefined}
|
|
{...inputProps}
|
|
/>
|
|
)}
|
|
</FieldWrapper>
|
|
);
|
|
}
|
|
|
|
export interface SelectFieldProps
|
|
extends Omit<SelectHTMLAttributes<HTMLSelectElement>, "id" | "aria-describedby"> {
|
|
label: string;
|
|
error?: string;
|
|
hint?: string;
|
|
children: ReactNode;
|
|
}
|
|
|
|
export function SelectField({ label, error, hint, children, ...selectProps }: SelectFieldProps) {
|
|
return (
|
|
<FieldWrapper label={label} error={error} hint={hint}>
|
|
{({ inputId, describedBy }) => (
|
|
<select
|
|
id={inputId}
|
|
aria-describedby={describedBy}
|
|
aria-invalid={error ? true : undefined}
|
|
{...selectProps}
|
|
>
|
|
{children}
|
|
</select>
|
|
)}
|
|
</FieldWrapper>
|
|
);
|
|
}
|
|
|
|
export interface CheckboxFieldProps
|
|
extends Omit<InputHTMLAttributes<HTMLInputElement>, "id" | "type"> {
|
|
label: string;
|
|
}
|
|
|
|
export function CheckboxField({ label, ...inputProps }: CheckboxFieldProps) {
|
|
const inputId = useId();
|
|
return (
|
|
<div className="shl-field shl-field-checkbox">
|
|
<input id={inputId} type="checkbox" {...inputProps} />
|
|
<label htmlFor={inputId}>{label}</label>
|
|
</div>
|
|
);
|
|
}
|