UNPKG

@base-ui-components/react

Version:

Base UI is a library of headless ('unstyled') React components and low-level hooks. You gain complete control over your app's CSS and accessibility features.

127 lines (126 loc) 4.22 kB
'use client'; import * as React from 'react'; import { useEventCallback } from '../../utils/useEventCallback.js'; import { useFieldRootContext } from '../root/FieldRootContext.js'; import { mergeReactProps } from '../../utils/mergeReactProps.js'; import { DEFAULT_VALIDITY_STATE } from '../utils/constants.js'; import { useFormContext } from '../../form/FormContext.js'; import { getCombinedFieldValidityData } from '../utils/getCombinedFieldValidityData.js'; const validityKeys = Object.keys(DEFAULT_VALIDITY_STATE); export function useFieldControlValidation() { const { setValidityData, validate, messageIds, validityData, validationMode, validationDebounceTime, invalid, markedDirtyRef, controlId, state } = useFieldRootContext(); const { formRef } = useFormContext(); const timeoutRef = React.useRef(-1); const inputRef = React.useRef(null); React.useEffect(() => { return () => { window.clearTimeout(timeoutRef.current); }; }, []); const commitValidation = useEventCallback(async value => { const element = inputRef.current; if (!element) { return; } function getState(el) { return validityKeys.reduce((acc, key) => { acc[key] = el.validity[key]; if (!el.validity.customError && !markedDirtyRef.current) { acc[key] = key === 'valid'; } return acc; }, {}); } window.clearTimeout(timeoutRef.current); const resultOrPromise = validate(value); let result = null; if (typeof resultOrPromise === 'object' && resultOrPromise !== null && 'then' in resultOrPromise) { result = await resultOrPromise; } else { result = resultOrPromise; } let errorMessage = ''; if (result !== null) { errorMessage = Array.isArray(result) ? result.join('\n') : result; } element.setCustomValidity(errorMessage); const nextState = getState(element); let validationErrors = []; if (Array.isArray(result)) { validationErrors = result; } else if (result) { validationErrors = [result]; } else if (element.validationMessage) { validationErrors = [element.validationMessage]; } const nextValidityData = { value, state: nextState, error: Array.isArray(result) ? result[0] : result ?? element.validationMessage, errors: validationErrors, initialValue: validityData.initialValue }; if (controlId) { const currentFieldData = formRef.current.fields.get(controlId); if (currentFieldData) { formRef.current.fields.set(controlId, { ...currentFieldData, ...getCombinedFieldValidityData(nextValidityData, invalid) }); } } setValidityData(nextValidityData); }); const getValidationProps = React.useCallback((externalProps = {}) => mergeReactProps(externalProps, { ...(messageIds.length && { 'aria-describedby': messageIds.join(' ') }), ...(state.valid === false && { 'aria-invalid': true }) }), [messageIds, state.valid]); const getInputValidationProps = React.useCallback((externalProps = {}) => mergeReactProps(getValidationProps(externalProps), { onChange(event) { // Workaround for https://github.com/facebook/react/issues/9023 if (event.nativeEvent.defaultPrevented) { return; } if (invalid || validationMode !== 'onChange') { return; } const element = event.currentTarget; if (element.value === '') { // Ignore the debounce time for empty values. commitValidation(element.value); return; } window.clearTimeout(timeoutRef.current); if (validationDebounceTime) { timeoutRef.current = window.setTimeout(() => { commitValidation(element.value); }, validationDebounceTime); } else { commitValidation(element.value); } } }), [getValidationProps, invalid, validationMode, validationDebounceTime, commitValidation]); return React.useMemo(() => ({ getValidationProps, getInputValidationProps, inputRef, commitValidation }), [getValidationProps, getInputValidationProps, commitValidation]); }