brf/client/shared/components/textarea_factory.tsx
2025-12-13 21:12:08 +01:00

91 lines
2.2 KiB
TypeScript

import { h, type FunctionComponent } from 'preact'
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
import cn from 'classnames'
import mergeStyles from '../utils/merge_styles.ts'
type Props = {
autoFocus?: boolean
className?: string
cols?: number
defaultValue?: string
disabled?: boolean
label: string
name: string
placeholder?: string
required?: boolean
rows: number
showValidity?: boolean
}
export default function textareaFactory(styles: {
base?: string
touched?: string
element?: string
label?: string
statusIcon?: string
}): FunctionComponent<Props> {
if (Array.isArray(styles)) {
styles = mergeStyles(styles)
}
const Textarea = ({
autoFocus,
className,
cols,
defaultValue,
disabled,
label,
name,
placeholder,
required,
rows,
showValidity = true,
}: Props) => {
const [touched, setTouched] = useState(false)
const textareaRef = useRef<HTMLTextAreaElement>(null)
const onBlur = useCallback(() => setTouched(true), [])
useEffect(() => {
const form = textareaRef.current!.form!
const resetTouched = setTouched.bind(null, false)
form.addEventListener('reset', resetTouched)
return () => form.removeEventListener('reset', resetTouched)
}, [])
return (
<div className={cn(styles.base, touched && styles.touched, className)}>
<textarea
autoFocus={autoFocus}
className={styles.element}
cols={cols}
disabled={disabled}
name={name}
onBlur={onBlur}
placeholder={placeholder ?? label}
ref={textareaRef}
defaultValue={defaultValue}
required={required}
rows={rows}
/>
{showValidity && <i className={styles.statusIcon} />}
{/**
* <label> comes after <textarea> to be able to use next sibling CSS selector
* (see public input.module.scss). Use `flexbox` and `order` if <label> needs to be
* displayed before <textarea> (see admin input.module.scss)
*/}
{label && (
<label className={styles.label} htmlFor={name}>
{label}
</label>
)}
</div>
)
}
return Textarea
}