diff --git a/src/internal/inputs/ValidationProps.ts b/src/internal/inputs/ValidationProps.ts index 704944c6..b59c68d4 100644 --- a/src/internal/inputs/ValidationProps.ts +++ b/src/internal/inputs/ValidationProps.ts @@ -23,4 +23,10 @@ export type ValidationProps = { * @see ValidationState */ validationState?: keyof typeof ValidationState; + /** + * Provide custom message for external validation errors. Applies only to errors reported by + * validationState prop. + * @see validationState + */ + errorMessage?: string; }; diff --git a/src/internal/inputs/index.ts b/src/internal/inputs/index.ts index 8cde4143..d47a789c 100644 --- a/src/internal/inputs/index.ts +++ b/src/internal/inputs/index.ts @@ -17,4 +17,4 @@ export { } from './validatorMocks.ts'; export {useHandleFormReset} from './useHandleFormReset.ts'; export {useRevalidateOnFormChange} from './useRevalidateOnFormChange.ts'; -export {useSyncValidation} from './useSyncValidation.ts'; +export {useExternalValidation} from './useExternalValidation.ts'; diff --git a/src/internal/inputs/useSyncValidation.ts b/src/internal/inputs/useExternalValidation.ts similarity index 58% rename from src/internal/inputs/useSyncValidation.ts rename to src/internal/inputs/useExternalValidation.ts index 6602a79a..1d849d58 100644 --- a/src/internal/inputs/useSyncValidation.ts +++ b/src/internal/inputs/useExternalValidation.ts @@ -6,7 +6,8 @@ import {ValidationState} from '@/internal/inputs'; export type Props = { validationState?: keyof typeof ValidationState; setValidity: Dispatch>; - inputRef: MutableRefObject; + inputRef: MutableRefObject; + errorMessage?: string; }; /** @@ -15,14 +16,22 @@ export type Props = { * NB! On change validation takes preference. * @see ValidationState */ -export const useSyncValidation = ({validationState, inputRef, setValidity}: Props) => { +export const useExternalValidation = ({ + validationState, + inputRef, + setValidity, + errorMessage = ValidationState.error, +}: Props) => { useEffect(() => { + // Empty string is considered to be positive validation result for HTMLInputElement.setCustomValidity + const normalizedErrorMessage = errorMessage ? errorMessage : ValidationState.error; if (validationState === ValidationState.error && inputRef.current) { - inputRef.current.setCustomValidity(ValidationState.error); + console.log('ValidationState.error', validationState, inputRef.current); + inputRef.current.setCustomValidity(normalizedErrorMessage); setValidity(ValidationState.error); } else if (validationState && inputRef.current) { inputRef.current.setCustomValidity(''); setValidity(validationState); } - }, [inputRef, setValidity, validationState]); + }, [errorMessage, inputRef, setValidity, validationState]); }; diff --git a/src/lib/InputCheckbox/InputCheckbox.tsx b/src/lib/InputCheckbox/InputCheckbox.tsx index fb8710b4..48a0b2b0 100644 --- a/src/lib/InputCheckbox/InputCheckbox.tsx +++ b/src/lib/InputCheckbox/InputCheckbox.tsx @@ -8,7 +8,7 @@ import { defaultValidator, useValidation, useRevalidateOnFormChange, - useSyncValidation, + useExternalValidation, } from '@/internal/inputs'; import type { NativePropsInteractive, @@ -49,6 +49,7 @@ export const InputCheckbox = forwardRef( required, revalidateOnFormChange, validationState, + errorMessage, ...nativeProps }, ref @@ -59,7 +60,7 @@ export const InputCheckbox = forwardRef( const inputRef = useInternalRef(ref); useRevalidateOnFormChange(inputRef, validateInteractive, revalidateOnFormChange); - useSyncValidation({inputRef, setValidity, validationState}); + useExternalValidation({inputRef, setValidity, validationState, errorMessage}); const ValidationIcon = { [ValidationState.error]: IconError, diff --git a/src/lib/InputColor/InputColor.tsx b/src/lib/InputColor/InputColor.tsx index ae4bd6b6..7a82f619 100644 --- a/src/lib/InputColor/InputColor.tsx +++ b/src/lib/InputColor/InputColor.tsx @@ -11,8 +11,13 @@ import type { ValidationProps, NativePropsInteractive, } from '@/internal/inputs'; -import {ValidationState} from '@/internal/inputs'; -import {useSyncValidation, useValidation, defaultValidator} from '@/internal/inputs'; +import {useRevalidateOnFormChange} from '@/internal/inputs'; +import { + ValidationState, + useExternalValidation, + useValidation, + defaultValidator, +} from '@/internal/inputs'; import {useInternalId} from '@/internal/hooks/useInternalId.ts'; import {useInternalRef} from '@/internal/hooks/useInternalRef.ts'; @@ -23,7 +28,7 @@ export type Props = DataAttributes & LibraryProps & NativePropsInteractive & CallbackPropsTextual & - Omit & { + ValidationProps & { /** * Set text for placeholder. * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#placeholder @@ -52,17 +57,22 @@ export const InputColor = forwardRef( id: idProp, predefinedColors = [], validationState, + errorMessage, + revalidateOnFormChange, + validatorFn = defaultValidator, ...nativeProps }, ref ) => { - const {validity, setValidity} = useValidation({ - validatorFn: defaultValidator, + const {validity, setValidity, validateTextual} = useValidation({ + validatorFn, }); const id = useInternalId(idProp); const inputRef = useInternalRef(ref); - useSyncValidation({inputRef, setValidity, validationState}); + useExternalValidation({inputRef, setValidity, validationState, errorMessage}); + + useRevalidateOnFormChange(inputRef, validateTextual, revalidateOnFormChange); const ValidationIcon = { [ValidationState.error]: IconError, diff --git a/src/lib/InputDate/InputDate.tsx b/src/lib/InputDate/InputDate.tsx index eb114fd7..903d6f4e 100644 --- a/src/lib/InputDate/InputDate.tsx +++ b/src/lib/InputDate/InputDate.tsx @@ -6,9 +6,13 @@ import classNames from 'classnames'; import {IconError, IconValid, IconLoader, IconCalendar} from '@/internal/Icons'; import type {DataAttributes, LibraryProps} from '@/internal/LibraryAPI'; import type {NativePropsTextual, CallbackPropsTextual, ValidationProps} from '@/internal/inputs'; -import {useRevalidateOnFormChange} from '@/internal/inputs'; -import {defaultValidator} from '@/internal/inputs'; -import {ValidationState, useValidation} from '@/internal/inputs'; +import { + useRevalidateOnFormChange, + useExternalValidation, + defaultValidator, + ValidationState, + useValidation, +} from '@/internal/inputs'; import {useInternalId} from '@/internal/hooks/useInternalId.ts'; import {useInternalRef} from '@/internal/hooks/useInternalRef.ts'; @@ -36,6 +40,8 @@ export const InputDate = forwardRef( defaultValue, validatorFn = defaultValidator, revalidateOnFormChange, + validationState, + errorMessage, ...nativeProps }, ref @@ -44,15 +50,18 @@ export const InputDate = forwardRef( const labelRef = useRef(null); const {validateTextual, validity, setValidity} = useValidation({validatorFn}); + + const inputRef = useInternalRef(ref); + + useExternalValidation({inputRef, setValidity, validationState, errorMessage}); + useRevalidateOnFormChange(inputRef, validateTextual, revalidateOnFormChange); + const ValidationIcon = { [ValidationState.error]: IconError, [ValidationState.valid]: IconValid, [ValidationState.inProgress]: IconLoader, }[validity!]; - const inputRef = useInternalRef(ref); - useRevalidateOnFormChange(inputRef, validateTextual, revalidateOnFormChange); - const displayValue = (value ?? defaultValue) as string; const handleChange = useCallback( (event: ChangeEvent) => { @@ -95,7 +104,7 @@ export const InputDate = forwardRef( & + ValidationProps & CallbackPropsTextual & { accept?: InputHTMLAttributes['accept']; multiple?: InputHTMLAttributes['multiple']; @@ -36,6 +44,10 @@ export const InputFile = forwardRef( onKeyUp = () => {}, defaultValue, size = 16, + validationState, + errorMessage, + revalidateOnFormChange, + validatorFn = defaultValidator, ...nativeProps }, ref @@ -51,8 +63,12 @@ export const InputFile = forwardRef( const id = useInternalId(idProp); const [filename, setFileName] = useState(''); const {validateTextual, validity, setValidity} = useValidation({ - validatorFn: defaultValidator, + validatorFn, }); + const inputRef = useInternalRef(ref); + useExternalValidation({inputRef, setValidity, validationState, errorMessage}); + useRevalidateOnFormChange(inputRef, validateTextual, revalidateOnFormChange); + const ValidationIcon = { [ValidationState.error]: IconError, [ValidationState.valid]: IconValid, @@ -79,7 +95,7 @@ export const InputFile = forwardRef( type="file" placeholder={placeholder} className={classes.input} - ref={ref} + ref={inputRef} disabled={disabled} value={value} defaultValue={defaultValue} diff --git a/src/lib/InputNumber/InputNumber.tsx b/src/lib/InputNumber/InputNumber.tsx index 07ef0fa1..8a657db7 100644 --- a/src/lib/InputNumber/InputNumber.tsx +++ b/src/lib/InputNumber/InputNumber.tsx @@ -7,8 +7,13 @@ import {useLocalTheme} from 'css-vars-hook'; import {IconError, IconValid, IconLoader, IconArrowUp, IconArrowDown} from '@/internal/Icons'; import type {DataAttributes, LibraryProps} from '@/internal/LibraryAPI'; import type {NativePropsNumeric, CallbackPropsTextual, ValidationProps} from '@/internal/inputs'; -import {useRevalidateOnFormChange} from '@/internal/inputs'; -import {ValidationState, defaultValidator, useValidation} from '@/internal/inputs'; +import { + ValidationState, + defaultValidator, + useValidation, + useExternalValidation, + useRevalidateOnFormChange, +} from '@/internal/inputs'; import {useInternalRef} from '@/internal/hooks/useInternalRef.ts'; import classes from './InputNumber.module.css'; @@ -43,6 +48,8 @@ export const InputNumber = forwardRef( size = 10, step = 1, revalidateOnFormChange, + validationState, + errorMessage, ...nativeProps }, ref @@ -51,6 +58,7 @@ export const InputNumber = forwardRef( const inputRef = useInternalRef(ref); useRevalidateOnFormChange(inputRef, validateTextual, revalidateOnFormChange); + useExternalValidation({inputRef, setValidity, validationState, errorMessage}); const ValidationIcon = { [ValidationState.error]: IconError, diff --git a/src/lib/InputPassword/InputPassword.tsx b/src/lib/InputPassword/InputPassword.tsx index 714ed3fe..cc0de10c 100644 --- a/src/lib/InputPassword/InputPassword.tsx +++ b/src/lib/InputPassword/InputPassword.tsx @@ -7,8 +7,13 @@ import classNames from 'classnames'; import {IconError, IconValid, IconLoader, IconLock, IconLockOpen} from '@/internal/Icons'; import type {DataAttributes, LibraryProps} from '@/internal/LibraryAPI'; import type {NativePropsTextual, CallbackPropsTextual, ValidationProps} from '@/internal/inputs'; -import {useRevalidateOnFormChange} from '@/internal/inputs'; -import {ValidationState, defaultValidator, useValidation} from '@/internal/inputs'; +import { + ValidationState, + defaultValidator, + useValidation, + useRevalidateOnFormChange, + useExternalValidation, +} from '@/internal/inputs'; import {useInternalId} from '@/internal/hooks/useInternalId.ts'; import {useInternalRef} from '@/internal/hooks/useInternalRef.ts'; @@ -43,6 +48,8 @@ export const InputPassword = forwardRef( readOnly, size = 16, revalidateOnFormChange, + validationState, + errorMessage, ...nativeProps }, ref @@ -51,6 +58,7 @@ export const InputPassword = forwardRef( const inputRef = useInternalRef(ref); useRevalidateOnFormChange(inputRef, validateTextual, revalidateOnFormChange); + useExternalValidation({inputRef, setValidity, validationState, errorMessage}); const ValidationIcon = { [ValidationState.error]: IconError, @@ -109,7 +117,7 @@ export const InputPassword = forwardRef( readOnly={readOnly} placeholder={placeholder} className={classes.input} - ref={ref} + ref={inputRef} disabled={disabled} type={type} value={value} diff --git a/src/lib/InputRange/InputRange.tsx b/src/lib/InputRange/InputRange.tsx index 226ab40c..88697462 100644 --- a/src/lib/InputRange/InputRange.tsx +++ b/src/lib/InputRange/InputRange.tsx @@ -7,6 +7,7 @@ import {useLocalTheme} from 'css-vars-hook'; import {IconError, IconValid, IconLoader} from '@/internal/Icons'; import type {DataAttributes, LibraryProps} from '@/internal/LibraryAPI'; import type {NativePropsNumeric, CallbackPropsTextual, ValidationProps} from '@/internal/inputs'; +import {useExternalValidation} from '@/internal/inputs'; import {useRevalidateOnFormChange} from '@/internal/inputs'; import {ValidationState, defaultValidator, useValidation} from '@/internal/inputs'; import {useControllableState} from '@/internal/hooks/useControllableState.ts'; @@ -71,6 +72,8 @@ export const InputRange = forwardRef( bars = 5, scaleUnit = '', revalidateOnFormChange, + validationState, + errorMessage, ...nativeProps }, ref @@ -79,6 +82,7 @@ export const InputRange = forwardRef( const inputRef = useInternalRef(ref); useRevalidateOnFormChange(inputRef, validateTextual, revalidateOnFormChange); + useExternalValidation({inputRef, setValidity, validationState, errorMessage}); const ValidationIcon = { [ValidationState.error]: IconError, @@ -128,7 +132,7 @@ export const InputRange = forwardRef( id={inputId} type="range" className={classes.input} - ref={ref} + ref={inputRef} disabled={disabled} value={value} defaultValue={defaultValue} diff --git a/src/lib/InputText/InputText.tsx b/src/lib/InputText/InputText.tsx index 89c4889c..80e66eaa 100644 --- a/src/lib/InputText/InputText.tsx +++ b/src/lib/InputText/InputText.tsx @@ -11,7 +11,7 @@ import { defaultValidator, useValidation, useRevalidateOnFormChange, - useSyncValidation, + useExternalValidation, } from '@/internal/inputs'; import {useInternalId} from '@/internal/hooks/useInternalId.ts'; import {useInternalRef} from '@/internal/hooks/useInternalRef.ts'; @@ -62,6 +62,7 @@ export const InputText = forwardRef( required, revalidateOnFormChange, validationState, + errorMessage, ...nativeProps }, ref @@ -71,7 +72,7 @@ export const InputText = forwardRef( const inputRef = useInternalRef(ref); useRevalidateOnFormChange(inputRef, validateTextual, revalidateOnFormChange); - useSyncValidation({inputRef, setValidity, validationState}); + useExternalValidation({inputRef, setValidity, validationState, errorMessage}); const ValidationIcon = { [ValidationState.error]: IconError, diff --git a/src/lib/InputTime/InputTime.tsx b/src/lib/InputTime/InputTime.tsx index 42fb42fd..97bed25c 100644 --- a/src/lib/InputTime/InputTime.tsx +++ b/src/lib/InputTime/InputTime.tsx @@ -5,6 +5,7 @@ import classNames from 'classnames'; import {IconError, IconValid, IconLoader, IconClock} from '@/internal/Icons'; import type {DataAttributes, LibraryProps} from '@/internal/LibraryAPI'; import type {NativePropsTextual, CallbackPropsTextual, ValidationProps} from '@/internal/inputs'; +import {useExternalValidation} from '@/internal/inputs'; import {useRevalidateOnFormChange} from '@/internal/inputs'; import {ValidationState, defaultValidator, useValidation} from '@/internal/inputs'; import {useInternalRef} from '@/internal/hooks/useInternalRef.ts'; @@ -36,6 +37,8 @@ export const InputTime = forwardRef( defaultValue, validatorFn = defaultValidator, revalidateOnFormChange, + validationState, + errorMessage, ...nativeProps }, ref @@ -44,6 +47,7 @@ export const InputTime = forwardRef( const {validity, setValidity, validateTextual} = useValidation({validatorFn}); useRevalidateOnFormChange(inputRef, validateTextual, revalidateOnFormChange); + useExternalValidation({inputRef, setValidity, validationState, errorMessage}); const ValidationIcon = { [ValidationState.error]: IconError, diff --git a/src/lib/Select/Select.tsx b/src/lib/Select/Select.tsx index ce4c59d9..001cb338 100644 --- a/src/lib/Select/Select.tsx +++ b/src/lib/Select/Select.tsx @@ -10,6 +10,7 @@ import type { CallbackPropsTextual, ValidationProps, } from '@/internal/inputs'; +import {useExternalValidation} from '@/internal/inputs'; import {useRevalidateOnFormChange} from '@/internal/inputs'; import {ValidationState, defaultValidator, useValidation} from '@/internal/inputs'; import {useInternalId} from '@/internal/hooks/useInternalId.ts'; @@ -54,6 +55,8 @@ export const Select = forwardRef( children, size = 16, revalidateOnFormChange, + validationState, + errorMessage, ...nativeProps }, ref @@ -70,6 +73,7 @@ export const Select = forwardRef( const inputRef = useInternalRef(ref); useRevalidateOnFormChange(inputRef, validateTextual, revalidateOnFormChange); + useExternalValidation({inputRef, setValidity, validationState, errorMessage}); const ValidationIcon = { [ValidationState.error]: IconError, diff --git a/src/lib/Textarea/Textarea.tsx b/src/lib/Textarea/Textarea.tsx index bf87021f..948c061a 100644 --- a/src/lib/Textarea/Textarea.tsx +++ b/src/lib/Textarea/Textarea.tsx @@ -6,6 +6,7 @@ import {useLocalTheme} from 'css-vars-hook'; import {IconError, IconValid, IconLoader} from '@/internal/Icons'; import type {DataAttributes, LibraryProps} from '@/internal/LibraryAPI'; import type {NativePropsTextual, CallbackPropsTextual, ValidationProps} from '@/internal/inputs'; +import {useExternalValidation} from '@/internal/inputs'; import {useRevalidateOnFormChange} from '@/internal/inputs'; import {ValidationState, defaultValidator, useValidation} from '@/internal/inputs'; import {useInternalId} from '@/internal/hooks/useInternalId.ts'; @@ -67,6 +68,8 @@ export const Textarea = forwardRef( rows = 3, resize = 'both', revalidateOnFormChange, + validationState, + errorMessage, ...nativeProps }, ref @@ -75,6 +78,10 @@ export const Textarea = forwardRef( const inputRef = useInternalRef(ref); useRevalidateOnFormChange(inputRef, validateTextual, revalidateOnFormChange); + useExternalValidation({inputRef, setValidity, validationState, errorMessage}); + + console.log('validity', validity); + console.log('validationState', validationState); const ValidationIcon = { [ValidationState.error]: IconError, @@ -132,7 +139,7 @@ export const Textarea = forwardRef( id={textareaId} placeholder={placeholder} className={classes.textarea} - ref={ref} + ref={inputRef} disabled={disabled} value={value} defaultValue={defaultValue}