135 lines
3.4 KiB
TypeScript
135 lines
3.4 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'
|
|
import { Script } from '@fastify/type-provider-typebox'
|
|
|
|
type Styles<C extends string, D extends string, S extends string> = {
|
|
base?: string
|
|
touched?: string
|
|
element?: string
|
|
label?: string
|
|
icon?: string
|
|
} & Record<C, string> &
|
|
Record<D, string> &
|
|
Record<S, string>
|
|
|
|
type Options<C extends string, D extends string, S extends string> = {
|
|
styles: Styles<C, D, S>
|
|
}
|
|
|
|
type Props<C extends string, D extends string, S extends string> = {
|
|
autoFocus?: boolean
|
|
className?: string
|
|
color?: C
|
|
defaultValue?: string
|
|
design?: D
|
|
disabled?: boolean
|
|
icon: ANY
|
|
label: string
|
|
name: string
|
|
noEmpty?: boolean
|
|
onChange?: ANY
|
|
options: string[] | number[] | { label: string; value: string | number }[]
|
|
placeholder?: string
|
|
required?: boolean
|
|
size: S
|
|
}
|
|
|
|
// designs / color / size
|
|
export default function selectFactory<C extends string = never, D extends string = never, S extends string = never>({
|
|
styles,
|
|
}: Options<C, D, S>) {
|
|
if (Array.isArray(styles)) {
|
|
styles = mergeStyles(styles)
|
|
}
|
|
|
|
const Select: FunctionComponent<Props<C, D, S>> = ({
|
|
autoFocus,
|
|
className,
|
|
color,
|
|
defaultValue,
|
|
design,
|
|
disabled,
|
|
icon,
|
|
name,
|
|
label,
|
|
noEmpty,
|
|
onChange,
|
|
placeholder,
|
|
required,
|
|
size,
|
|
options,
|
|
}) => {
|
|
const [touched, setTouched] = useState(false)
|
|
const selectRef = useRef<HTMLSelectElement>(null)
|
|
|
|
const onBlur = useCallback(() => setTouched(true), [])
|
|
|
|
useEffect(() => {
|
|
const form = selectRef.current!.form
|
|
const resetTouched = setTouched.bind(null, false)
|
|
|
|
form!.addEventListener('reset', resetTouched)
|
|
|
|
return () => form!.removeEventListener('reset', resetTouched)
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
if (autoFocus) selectRef.current!.focus()
|
|
}, [autoFocus])
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
styles.base,
|
|
design && styles[design],
|
|
color && styles[color],
|
|
size && styles[size],
|
|
touched && styles.touched,
|
|
className,
|
|
)}
|
|
>
|
|
{icon && <div className={cn(styles.icon)} style={{ maskImage: icon }} />}
|
|
|
|
<select
|
|
autoFocus={autoFocus}
|
|
className={styles.element}
|
|
disabled={disabled}
|
|
name={name}
|
|
onBlur={onBlur}
|
|
onChange={onChange}
|
|
ref={selectRef}
|
|
defaultValue={defaultValue}
|
|
required={required}
|
|
>
|
|
{!noEmpty && <option value=''>{placeholder || label}</option>}
|
|
|
|
{options &&
|
|
options.map((option) => (
|
|
<option
|
|
key={typeof option === 'object' ? option.value : option}
|
|
value={typeof option === 'object' ? option.value : option}
|
|
>
|
|
{typeof option === 'object' ? option.label : option}
|
|
</option>
|
|
))}
|
|
</select>
|
|
|
|
{/**
|
|
* <label> comes after <input> 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 <input> (see admin input.module.scss)
|
|
*/}
|
|
{label && (
|
|
<label className={styles.label} htmlFor={name}>
|
|
{label}
|
|
</label>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return Select
|
|
}
|