Skip to content

Commit

Permalink
fix(input-validation): add external validation interface to inputs
Browse files Browse the repository at this point in the history
fix #373
  • Loading branch information
morewings committed Jul 26, 2024
1 parent fd04cba commit a897591
Show file tree
Hide file tree
Showing 14 changed files with 120 additions and 33 deletions.
6 changes: 6 additions & 0 deletions src/internal/inputs/ValidationProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
2 changes: 1 addition & 1 deletion src/internal/inputs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import {ValidationState} from '@/internal/inputs';
export type Props = {
validationState?: keyof typeof ValidationState;
setValidity: Dispatch<SetStateAction<keyof typeof ValidationState | null>>;
inputRef: MutableRefObject<HTMLInputElement | HTMLTextAreaElement | null>;
inputRef: MutableRefObject<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | null>;
errorMessage?: string;
};

/**
Expand All @@ -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]);
};
5 changes: 3 additions & 2 deletions src/lib/InputCheckbox/InputCheckbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
defaultValidator,
useValidation,
useRevalidateOnFormChange,
useSyncValidation,
useExternalValidation,
} from '@/internal/inputs';
import type {
NativePropsInteractive,
Expand Down Expand Up @@ -49,6 +49,7 @@ export const InputCheckbox = forwardRef<HTMLInputElement, Props>(
required,
revalidateOnFormChange,
validationState,
errorMessage,
...nativeProps
},
ref
Expand All @@ -59,7 +60,7 @@ export const InputCheckbox = forwardRef<HTMLInputElement, Props>(
const inputRef = useInternalRef(ref);
useRevalidateOnFormChange(inputRef, validateInteractive, revalidateOnFormChange);

useSyncValidation({inputRef, setValidity, validationState});
useExternalValidation({inputRef, setValidity, validationState, errorMessage});

const ValidationIcon = {
[ValidationState.error]: IconError,
Expand Down
22 changes: 16 additions & 6 deletions src/lib/InputColor/InputColor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -23,7 +28,7 @@ export type Props = DataAttributes &
LibraryProps &
NativePropsInteractive &
CallbackPropsTextual &
Omit<ValidationProps, 'validatorFn'> & {
ValidationProps & {
/**
* Set text for placeholder.
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#placeholder
Expand Down Expand Up @@ -52,17 +57,22 @@ export const InputColor = forwardRef<HTMLInputElement, Props>(
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,
Expand Down
23 changes: 16 additions & 7 deletions src/lib/InputDate/InputDate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -36,6 +40,8 @@ export const InputDate = forwardRef<HTMLInputElement, Props>(
defaultValue,
validatorFn = defaultValidator,
revalidateOnFormChange,
validationState,
errorMessage,
...nativeProps
},
ref
Expand All @@ -44,15 +50,18 @@ export const InputDate = forwardRef<HTMLInputElement, Props>(
const labelRef = useRef<HTMLLabelElement>(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<HTMLInputElement>) => {
Expand Down Expand Up @@ -95,7 +104,7 @@ export const InputDate = forwardRef<HTMLInputElement, Props>(
<input
{...nativeProps}
id={id}
ref={ref}
ref={inputRef}
className={classes.input}
type="date"
disabled={disabled}
Expand Down
24 changes: 20 additions & 4 deletions src/lib/InputFile/InputFile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,23 @@ import {useLocalTheme} from 'css-vars-hook';

import {IconError, IconValid, IconLoader, IconFile} from '@/internal/Icons';
import type {DataAttributes, LibraryProps} from '@/internal/LibraryAPI';
import type {NativePropsTextual, CallbackPropsTextual} from '@/internal/inputs';
import {ValidationState, defaultValidator, useValidation} from '@/internal/inputs';
import type {NativePropsTextual, CallbackPropsTextual, ValidationProps} 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';

import classes from './InputFile.module.css';

export type Props = DataAttributes &
LibraryProps &
Omit<NativePropsTextual, 'inputMode' | 'maxLength' | 'minLength' | 'pattern' | 'readOnly'> &
ValidationProps &
CallbackPropsTextual & {
accept?: InputHTMLAttributes<HTMLInputElement>['accept'];
multiple?: InputHTMLAttributes<HTMLInputElement>['multiple'];
Expand All @@ -36,6 +44,10 @@ export const InputFile = forwardRef<HTMLInputElement, Props>(
onKeyUp = () => {},
defaultValue,
size = 16,
validationState,
errorMessage,
revalidateOnFormChange,
validatorFn = defaultValidator,
...nativeProps
},
ref
Expand All @@ -51,8 +63,12 @@ export const InputFile = forwardRef<HTMLInputElement, Props>(
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,
Expand All @@ -79,7 +95,7 @@ export const InputFile = forwardRef<HTMLInputElement, Props>(
type="file"
placeholder={placeholder}
className={classes.input}
ref={ref}
ref={inputRef}
disabled={disabled}
value={value}
defaultValue={defaultValue}
Expand Down
12 changes: 10 additions & 2 deletions src/lib/InputNumber/InputNumber.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -43,6 +48,8 @@ export const InputNumber = forwardRef<HTMLInputElement, Props>(
size = 10,
step = 1,
revalidateOnFormChange,
validationState,
errorMessage,
...nativeProps
},
ref
Expand All @@ -51,6 +58,7 @@ export const InputNumber = forwardRef<HTMLInputElement, Props>(

const inputRef = useInternalRef(ref);
useRevalidateOnFormChange(inputRef, validateTextual, revalidateOnFormChange);
useExternalValidation({inputRef, setValidity, validationState, errorMessage});

const ValidationIcon = {
[ValidationState.error]: IconError,
Expand Down
14 changes: 11 additions & 3 deletions src/lib/InputPassword/InputPassword.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -43,6 +48,8 @@ export const InputPassword = forwardRef<HTMLInputElement, Props>(
readOnly,
size = 16,
revalidateOnFormChange,
validationState,
errorMessage,
...nativeProps
},
ref
Expand All @@ -51,6 +58,7 @@ export const InputPassword = forwardRef<HTMLInputElement, Props>(

const inputRef = useInternalRef(ref);
useRevalidateOnFormChange(inputRef, validateTextual, revalidateOnFormChange);
useExternalValidation({inputRef, setValidity, validationState, errorMessage});

const ValidationIcon = {
[ValidationState.error]: IconError,
Expand Down Expand Up @@ -109,7 +117,7 @@ export const InputPassword = forwardRef<HTMLInputElement, Props>(
readOnly={readOnly}
placeholder={placeholder}
className={classes.input}
ref={ref}
ref={inputRef}
disabled={disabled}
type={type}
value={value}
Expand Down
6 changes: 5 additions & 1 deletion src/lib/InputRange/InputRange.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -71,6 +72,8 @@ export const InputRange = forwardRef<HTMLInputElement, Props>(
bars = 5,
scaleUnit = '',
revalidateOnFormChange,
validationState,
errorMessage,
...nativeProps
},
ref
Expand All @@ -79,6 +82,7 @@ export const InputRange = forwardRef<HTMLInputElement, Props>(

const inputRef = useInternalRef(ref);
useRevalidateOnFormChange(inputRef, validateTextual, revalidateOnFormChange);
useExternalValidation({inputRef, setValidity, validationState, errorMessage});

const ValidationIcon = {
[ValidationState.error]: IconError,
Expand Down Expand Up @@ -128,7 +132,7 @@ export const InputRange = forwardRef<HTMLInputElement, Props>(
id={inputId}
type="range"
className={classes.input}
ref={ref}
ref={inputRef}
disabled={disabled}
value={value}
defaultValue={defaultValue}
Expand Down
5 changes: 3 additions & 2 deletions src/lib/InputText/InputText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -62,6 +62,7 @@ export const InputText = forwardRef<HTMLInputElement, Props>(
required,
revalidateOnFormChange,
validationState,
errorMessage,
...nativeProps
},
ref
Expand All @@ -71,7 +72,7 @@ export const InputText = forwardRef<HTMLInputElement, Props>(
const inputRef = useInternalRef(ref);
useRevalidateOnFormChange(inputRef, validateTextual, revalidateOnFormChange);

useSyncValidation({inputRef, setValidity, validationState});
useExternalValidation({inputRef, setValidity, validationState, errorMessage});

const ValidationIcon = {
[ValidationState.error]: IconError,
Expand Down
Loading

0 comments on commit a897591

Please sign in to comment.