nsfwapp/frontend/src/components/ui/TextInput.tsx
2026-04-27 14:40:16 +02:00

107 lines
2.2 KiB
TypeScript

// frontend\src\components\ui\TextInput.tsx
'use client'
import {
useRef,
useCallback,
forwardRef,
type InputHTMLAttributes,
type Ref,
} from 'react'
type TextInputSize = 'sm' | 'md' | 'lg'
export type TextInputProps = Omit<
InputHTMLAttributes<HTMLInputElement>,
'size'
> & {
size?: TextInputSize
selectAllOnFocus?: boolean
selectAllOnMouseDown?: boolean
}
const baseClassName = `
block w-full min-w-0
bg-white text-gray-900 shadow-sm ring-1 ring-gray-200
focus:outline-none focus:ring-2 focus:ring-indigo-500
dark:bg-white/10 dark:text-white dark:ring-white/10 dark:[color-scheme:dark]
`
const sizeClassName: Record<TextInputSize, string> = {
sm: 'rounded-md px-3 py-2 text-sm',
md: 'rounded-lg px-3 py-2.5 text-sm',
lg: 'rounded-lg px-4 py-3 text-base',
}
function assignRef<T>(ref: Ref<T> | undefined, value: T | null) {
if (!ref) return
if (typeof ref === 'function') {
ref(value)
return
}
;(ref as { current: T | null }).current = value
}
const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
(
{
className = '',
type = 'text',
size = 'md',
selectAllOnFocus = false,
selectAllOnMouseDown = false,
onFocus,
onMouseDown,
...props
},
forwardedRef
) => {
const innerRef = useRef<HTMLInputElement | null>(null)
const setRefs = useCallback(
(node: HTMLInputElement | null) => {
innerRef.current = node
assignRef(forwardedRef, node)
},
[forwardedRef]
)
const selectAll = useCallback(() => {
const el = innerRef.current
if (!el) return
el.focus()
requestAnimationFrame(() => el.select())
}, [])
return (
<input
{...props}
ref={setRefs}
type={type}
className={[baseClassName, sizeClassName[size], className].join(' ').trim()}
onFocus={(e) => {
onFocus?.(e)
if (selectAllOnFocus) {
selectAll()
}
}}
onMouseDown={(e) => {
onMouseDown?.(e)
if (!selectAllOnMouseDown) return
if (e.button !== 0) return
e.preventDefault()
selectAll()
}}
/>
)
}
)
TextInput.displayName = 'TextInput'
export default TextInput