tech-interview-handbook/packages/ui/src/TextArea/TextArea.tsx

141 lines
3.1 KiB

import clsx from 'clsx';
import type {
ChangeEvent,
FocusEvent,
ForwardedRef,
TextareaHTMLAttributes,
} from 'react';
import React, { forwardRef, useId } from 'react';
type Attributes = Pick<
TextareaHTMLAttributes<HTMLTextAreaElement>,
| 'autoComplete'
| 'autoFocus'
| 'disabled'
| 'maxLength'
| 'minLength'
| 'name'
| 'onBlur'
| 'onFocus'
| 'placeholder'
| 'readOnly'
| 'required'
| 'rows'
>;
export type TextAreaResize = 'both' | 'horizontal' | 'none' | 'vertical';
type Props = Readonly<{
defaultValue?: string;
errorMessage?: React.ReactNode;
id?: string;
isLabelHidden?: boolean;
label: string;
onBlur?: (event: FocusEvent<HTMLTextAreaElement>) => void;
onChange?: (value: string, event: ChangeEvent<HTMLTextAreaElement>) => void;
resize?: TextAreaResize;
value?: string;
}> &
Readonly<Attributes>;
type State = 'error' | 'normal';
const stateClasses: Record<
State,
Readonly<{
textArea: string;
}>
> = {
error: {
textArea:
'border-danger-300 focus:ring-danger-500 focus:border-danger-500 text-danger-900 placeholder-danger-300',
},
normal: {
textArea:
'border-slate-300 focus:border-primary-500 focus:ring-primary-500 placeholder:text-slate-400',
},
};
const resizeClasses: Record<TextAreaResize, string> = {
both: 'resize',
horizontal: 'resize-x',
none: 'resize-none',
vertical: 'resize-y',
};
function TextArea(
{
defaultValue,
disabled,
errorMessage,
id: idParam,
isLabelHidden,
label,
resize = 'vertical',
required,
value,
onChange,
...props
}: Props,
ref: ForwardedRef<HTMLTextAreaElement>,
) {
const hasError = errorMessage != null;
const generatedId = useId();
const id = idParam ?? generatedId;
const errorId = useId();
const state: State = hasError ? 'error' : 'normal';
return (
<div>
<label
className={clsx(
isLabelHidden
? 'sr-only'
: 'mb-1 block text-sm font-medium text-gray-700',
)}
htmlFor={id}>
{label}
{required && (
<span aria-hidden="true" className="text-danger-500">
{' '}
*
</span>
)}
</label>
<div>
<textarea
ref={ref}
aria-describedby={hasError ? errorId : undefined}
aria-invalid={hasError ? true : undefined}
className={clsx(
'block w-full rounded-md text-sm disabled:bg-slate-50 disabled:text-slate-500',
stateClasses[state].textArea,
resizeClasses[resize],
)}
defaultValue={defaultValue}
disabled={disabled}
id={id}
name="comment"
required={required}
value={value != null ? value : undefined}
onChange={(event) => {
if (!onChange) {
return;
}
onChange(event.target.value, event);
}}
{...props}
/>
</div>
{errorMessage && (
<p className="text-danger-600 mt-2 text-sm" id={errorId}>
{errorMessage}
</p>
)}
</div>
);
}
export default forwardRef(TextArea);