SHL-01: ui-shell-design-system-zentral (tokens, theming, i18n-rahmen, basis-komponenten)
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
// 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user