Improve NameInput

This commit is contained in:
Francisco Ignácio Fontoura Solla 2026-03-20 15:44:11 -03:00
parent e13b55dadb
commit c72cd6a86e
2 changed files with 63 additions and 22 deletions

View file

@ -1,13 +1,13 @@
'use client'
import { ChangeEventHandler, type ComponentProps } from 'react'
import type {
CompositionEventHandler,
FocusEventHandler,
InputEventHandler,
ComponentProps,
} from 'react'
import { Input } from './ui/input'
import {
type FieldPath,
type FieldValues,
type RegisterOptions,
useFormContext,
} from 'react-hook-form'
import { type FieldPath, type FieldValues, useFormContext } from 'react-hook-form'
interface FormInpuProps<
TFieldValues extends FieldValues,
@ -25,22 +25,50 @@ export const FormInput = <
name,
format,
sanitize,
onChange,
...props
}: FormInpuProps<TFieldValues, TFieldName>) => {
const { onChange: onValueChange, ...registerResult } = useFormContext<TFieldValues>().register(
const { onChange, onBlur, ...registerResult } = useFormContext<TFieldValues>().register(
name,
sanitize ? { setValueAs: sanitize } : undefined,
)
const handleChange: ChangeEventHandler<HTMLInputElement> = (e) => {
if (format) {
e.target.value = format(e.target.value)
}
onValueChange(e)
onChange?.(e)
const handleUpdate = (e: EventTarget & HTMLInputElement, value: string) => {
const selectionStart = e.selectionStart
e.value = value
e.setSelectionRange(selectionStart, selectionStart)
}
return <Input {...props} {...registerResult} onChange={handleChange} />
const handleInput: InputEventHandler<HTMLInputElement> = (e) => {
if (e.nativeEvent.isComposing) {
return
}
if (format) {
handleUpdate(e.currentTarget, format(e.currentTarget.value))
}
onChange(e)
}
const handleCompositionEnd: CompositionEventHandler<HTMLInputElement> | undefined = format
? (e) => handleUpdate(e.currentTarget, format(e.currentTarget.value))
: undefined
const handleBlur: FocusEventHandler<HTMLInputElement> = (e) => {
handleUpdate(
e.currentTarget,
format?.(sanitize?.(e.currentTarget.value) ?? e.currentTarget.value) ?? e.currentTarget.value,
)
onBlur(e)
}
return (
<Input
{...props}
{...registerResult}
onInput={handleInput}
onCompositionEnd={handleCompositionEnd}
onBlur={handleBlur}
/>
)
}

View file

@ -10,18 +10,31 @@ export const NameInput = (props: ComponentProps<'input'>) => (
autoComplete="name"
maxLength={120}
minLength={2}
format={sanitize}
format={format}
sanitize={sanitize}
required
{...props}
/>
)
const sanitize = (value: string) =>
const format = (value: string) =>
value
.normalize('NFC')
.replace(/[^\p{L}\p{M}\s-]+/gu, '')
.replace(/-+/g, '-')
.trimStart()
.replace(/\s+/g, ' ')
.replace(/\s*-\s*/g, '-')
.trim()
.replace(/-{2,}/g, '-')
.replace(/(^|\s)-+/gu, '$1')
.toLowerCase()
.replace(/\b\p{L}+\b/gu, (word) =>
connectors.has(word) ? word : word[0].toUpperCase() + word.slice(1),
)
.slice(0, 120)
const sanitize = (value: string) =>
format(value)
.trim()
.replace(/(^|\s)-+/gu, '$1')
.replace(/-(?!\p{L})/gu, '')
const connectors = new Set(['da', 'das', 'de', 'di', 'do', 'dos', 'du', 'e'])