diff --git a/src/components/FormInput.tsx b/src/components/FormInput.tsx index 395c5721..9fddef33 100644 --- a/src/components/FormInput.tsx +++ b/src/components/FormInput.tsx @@ -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) => { - const { onChange: onValueChange, ...registerResult } = useFormContext().register( + const { onChange, onBlur, ...registerResult } = useFormContext().register( name, sanitize ? { setValueAs: sanitize } : undefined, ) - const handleChange: ChangeEventHandler = (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 + const handleInput: InputEventHandler = (e) => { + if (e.nativeEvent.isComposing) { + return + } + + if (format) { + handleUpdate(e.currentTarget, format(e.currentTarget.value)) + } + + onChange(e) + } + + const handleCompositionEnd: CompositionEventHandler | undefined = format + ? (e) => handleUpdate(e.currentTarget, format(e.currentTarget.value)) + : undefined + + const handleBlur: FocusEventHandler = (e) => { + handleUpdate( + e.currentTarget, + format?.(sanitize?.(e.currentTarget.value) ?? e.currentTarget.value) ?? e.currentTarget.value, + ) + onBlur(e) + } + + return ( + + ) } diff --git a/src/components/NameInput.tsx b/src/components/NameInput.tsx index 197ad2f6..13a5dfab 100644 --- a/src/components/NameInput.tsx +++ b/src/components/NameInput.tsx @@ -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'])