Skip to content

Commit

Permalink
fix(input-validation): add validationState prop
Browse files Browse the repository at this point in the history
fix #373
  • Loading branch information
morewings committed Jul 25, 2024
1 parent ce36533 commit 147dde8
Show file tree
Hide file tree
Showing 6 changed files with 68 additions and 6 deletions.
5 changes: 5 additions & 0 deletions src/internal/inputs/ValidationProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,9 @@ export type ValidationProps = {
validityState: ValidityState,
formState: FormState
) => string | Promise<string>;
/**
* Set external validation state for input. NB! On change validation takes preference over this.
* @see ValidationState
*/
validationState?: keyof typeof ValidationState;
};
1 change: 1 addition & 0 deletions src/internal/inputs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ export {
} from './validatorMocks.ts';
export {useHandleFormReset} from './useHandleFormReset.ts';
export {useRevalidateOnFormChange} from './useRevalidateOnFormChange.ts';
export {useSyncValidation} from './useSyncValidation.ts';
28 changes: 28 additions & 0 deletions src/internal/inputs/useSyncValidation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type {Dispatch, SetStateAction, MutableRefObject} from 'react';
import {useEffect} from 'react';

import {ValidationState} from '@/internal/inputs';

export type Props = {
validationState?: keyof typeof ValidationState;
setValidity: Dispatch<SetStateAction<keyof typeof ValidationState | null>>;
inputRef: MutableRefObject<HTMLInputElement | HTMLTextAreaElement | null>;
};

/**
* React hook designed to contain effects which synchronize input validation
* with external validation state via prop or context (TODO).
* NB! On change validation takes preference.
* @see ValidationState
*/
export const useSyncValidation = ({validationState, inputRef, setValidity}: Props) => {
useEffect(() => {
if (validationState === ValidationState.error && inputRef.current) {
inputRef.current.setCustomValidity(ValidationState.error);
setValidity(ValidationState.error);
} else if (validationState && inputRef.current) {
inputRef.current.setCustomValidity('');
setValidity(validationState);
}
}, [inputRef, setValidity, validationState]);
};
7 changes: 6 additions & 1 deletion src/lib/InputCheckbox/InputCheckbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
defaultValidator,
useValidation,
useRevalidateOnFormChange,
useSyncValidation,
} from '@/internal/inputs';
import type {
NativePropsInteractive,
Expand All @@ -25,6 +26,7 @@ export type Props = DataAttributes &
NativePropsInteractive &
CallbackPropsInteractive &
ValidationProps & {
/** Set a text for checkbox label */
label?: string;
};

Expand All @@ -46,6 +48,7 @@ export const InputCheckbox = forwardRef<HTMLInputElement, Props>(
validatorFn = defaultValidator,
required,
revalidateOnFormChange,
validationState,
...nativeProps
},
ref
Expand All @@ -56,6 +59,8 @@ export const InputCheckbox = forwardRef<HTMLInputElement, Props>(
const inputRef = useInternalRef(ref);
useRevalidateOnFormChange(inputRef, validateInteractive, revalidateOnFormChange);

useSyncValidation({inputRef, setValidity, validationState});

const ValidationIcon = {
[ValidationState.error]: IconError,
[ValidationState.valid]: IconValid,
Expand All @@ -78,7 +83,7 @@ export const InputCheckbox = forwardRef<HTMLInputElement, Props>(
<input
{...nativeProps}
className={classes.input}
ref={ref}
ref={inputRef}
disabled={disabled}
type="checkbox"
id={id}
Expand Down
21 changes: 19 additions & 2 deletions src/lib/InputColor/InputColor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@ import {forwardRef, useCallback} from 'react';
import classNames from 'classnames';
import {useLocalTheme} from 'css-vars-hook';

import {IconPalette} from '@/internal/Icons';
import {IconError, IconLoader, IconPalette, IconValid} from '@/internal/Icons';
import type {DataAttributes, LibraryProps} from '@/internal/LibraryAPI';
import type {
CallbackPropsTextual,
ValidationProps,
NativePropsInteractive,
} from '@/internal/inputs';
import {ValidationState} from '@/internal/inputs';
import {useSyncValidation, useValidation, defaultValidator} from '@/internal/inputs';
import {useInternalId} from '@/internal/hooks/useInternalId.ts';
import {useInternalRef} from '@/internal/hooks/useInternalRef.ts';

import classes from './InputColor.module.css';
import {invertColor} from './invertColor.ts';
Expand Down Expand Up @@ -48,12 +51,25 @@ export const InputColor = forwardRef<HTMLInputElement, Props>(
defaultValue,
id: idProp,
predefinedColors = [],
validationState,
...nativeProps
},
ref
) => {
const {validity, setValidity} = useValidation({
validatorFn: defaultValidator,
});
const id = useInternalId(idProp);

const inputRef = useInternalRef(ref);
useSyncValidation({inputRef, setValidity, validationState});

const ValidationIcon = {
[ValidationState.error]: IconError,
[ValidationState.valid]: IconValid,
[ValidationState.inProgress]: IconLoader,
}[validity!];

const {LocalRoot, setTheme} = useLocalTheme();
const displayValue = (value ?? defaultValue) as string;
const theme = useMemo(
Expand Down Expand Up @@ -112,7 +128,7 @@ export const InputColor = forwardRef<HTMLInputElement, Props>(
<input
{...nativeProps}
id={id}
ref={ref}
ref={inputRef}
type="color"
disabled={disabled}
defaultValue={displayValue}
Expand All @@ -129,6 +145,7 @@ export const InputColor = forwardRef<HTMLInputElement, Props>(
<label htmlFor={id} className={classes.label} ref={labelRef}>
{displayValue.toLowerCase() || placeholder}
</label>
{validity && <ValidationIcon />}
{hasPredefinedColors && (
<datalist id={predefinedColorsListId}>
{predefinedColors.map(color => {
Expand Down
12 changes: 9 additions & 3 deletions src/lib/InputText/InputText.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type {ChangeEvent, FC, InputHTMLAttributes} from 'react';
import type {FC, InputHTMLAttributes} from 'react';
import type {ChangeEvent} from 'react';
import {forwardRef, useCallback} from 'react';
import classNames from 'classnames';

Expand All @@ -10,6 +11,7 @@ import {
defaultValidator,
useValidation,
useRevalidateOnFormChange,
useSyncValidation,
} from '@/internal/inputs';
import {useInternalId} from '@/internal/hooks/useInternalId.ts';
import {useInternalRef} from '@/internal/hooks/useInternalRef.ts';
Expand All @@ -32,11 +34,12 @@ export type Props = DataAttributes &
* Define the width of the input in characters
*/
size?: InputHTMLAttributes<HTMLInputElement>['size'];
/**
* Provide an icon to prepend to the input
*/
prefix?: FC;
};

const ChangeEvent = new Event('change', {bubbles: false});

export const InputText = forwardRef<HTMLInputElement, Props>(
(
{
Expand All @@ -58,6 +61,7 @@ export const InputText = forwardRef<HTMLInputElement, Props>(
id,
required,
revalidateOnFormChange,
validationState,
...nativeProps
},
ref
Expand All @@ -67,6 +71,8 @@ export const InputText = forwardRef<HTMLInputElement, Props>(
const inputRef = useInternalRef(ref);
useRevalidateOnFormChange(inputRef, validateTextual, revalidateOnFormChange);

useSyncValidation({inputRef, setValidity, validationState});

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

0 comments on commit 147dde8

Please sign in to comment.