Files
carmanagement/apps/homepage/src/components/forms/TextInput.tsx
T
root 057d41e1c2
Build & Push / Pipeline Tests (push) Failing after 1m5s
Build & Push / Build & Push Docker Image (push) Has been skipped
Test / Type Check (all packages) (push) Failing after 52s
Test / API Unit Tests (push) Has been skipped
Test / Homepage Unit Tests (push) Has been skipped
Test / Carplace Unit Tests (push) Has been skipped
Test / Admin Unit Tests (push) Has been skipped
Test / Dashboard Unit Tests (push) Has been skipped
Test / API Integration Tests (push) Has been skipped
add text input validation
2026-08-31 22:51:44 -04:00

98 lines
2.8 KiB
TypeScript

import { BidiText } from '@/components/foundation/BidiText';
import { classNames } from '@/lib/components/classNames';
import {
forwardRef,
type InputHTMLAttributes,
type SelectHTMLAttributes,
type TextareaHTMLAttributes,
} from 'react';
import styles from './TextInput.module.css';
export type TextInputType = 'text' | 'email' | 'tel' | 'url' | 'search';
interface TextInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'type'> {
type?: TextInputType;
invalid?: boolean;
describedBy?: string | undefined;
bidiValue?: boolean;
}
export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(function TextInput(
{ type = 'text', invalid = false, describedBy, bidiValue = false, className, ...props },
ref,
) {
const dir = bidiValue || type === 'email' || type === 'tel' || type === 'url' ? 'ltr' : undefined;
const maxLength = props.maxLength ?? (type === 'email' ? 254 : type === 'tel' ? 20 : undefined);
const pattern = props.pattern ?? (type === 'tel' ? '^(?:(?:\\+|00)212|0)\\s?[5-7](?:\\s?\\d){8}$' : undefined);
return (
<input
ref={ref}
type={type}
dir={dir}
maxLength={maxLength}
pattern={pattern}
className={classNames(styles.control, className)}
aria-invalid={invalid || undefined}
aria-describedby={describedBy}
{...props}
/>
);
});
interface TextAreaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
invalid?: boolean;
describedBy?: string | undefined;
characterCount?: { current: number; max: number; label: string };
}
export function TextArea({
invalid = false,
describedBy,
characterCount,
className,
...props
}: TextAreaProps) {
const countId = characterCount && props.id ? `${props.id}-count` : undefined;
const ids = [describedBy, countId].filter(Boolean).join(' ') || undefined;
return (
<div className={styles.textareaGroup}>
<textarea
className={classNames(styles.control, styles.textarea, className)}
aria-invalid={invalid || undefined}
aria-describedby={ids}
{...props}
/>
{characterCount ? (
<p id={countId} className={styles.count} aria-live="polite">
<BidiText>{`${characterCount.current}/${characterCount.max}`}</BidiText>{' '}
{characterCount.label}
</p>
) : null}
</div>
);
}
interface SelectProps extends SelectHTMLAttributes<HTMLSelectElement> {
invalid?: boolean;
describedBy?: string | undefined;
}
export function Select({
invalid = false,
describedBy,
className,
children,
...props
}: SelectProps) {
return (
<select
className={classNames(styles.control, styles.select, className)}
aria-invalid={invalid || undefined}
aria-describedby={describedBy}
{...props}
>
{children}
</select>
);
}