'use client' import { forwardRef, useCallback, useMemo, useState } from 'react' import * as React from 'react' import classNames from 'classnames' import useId from '~/src/hooks/useId' import { type FormFieldProps, type FormFieldSize, type SizeProps, } from '~/src/types/props' import { getFormFieldSizeClassName } from '~/src/types/props-helpers' import { ariaAttr } from '~/src/utils/aria' import { createContext } from '~/src/utils/react' import { isNil } from '~/src/utils/type' import { Stack } from '~/src/components/Stack' import { type ContainerProps, type ErrorMessagePropsGetter, type FieldPropsGetter, type FormControlContextValue, type FormControlProps, type GroupPropsGetter, type HelperTextPropsGetter, type LabelPropsGetter, } from './FormControl.types' import styles from './FormControl.module.scss' const [FormControlContextProvider, useFormControlContext] = createContext< FormControlContextValue | undefined >(undefined) export { useFormControlContext } const FORM_CONTROL_TEST_ID = 'bezier-form-control' const Container = forwardRef(function Container( { labelPosition, children, className, ...rest }, forwardedRef ) { switch (labelPosition) { case 'top': return ( {children} ) case 'left': default: return (
} className={classNames(styles.Grid, className)} {...rest} > {children}
) } }) export const FormControl = forwardRef( function FormControl( { children, id: idProp, labelPosition = 'top', size = 'm', hasError, required, readOnly, disabled, ...rest }, forwardedRef ) { const [groupNode, setGroupNode] = useState(null) const [helperTextNode, setHelperTextNode] = useState( null ) const [errorMessageNode, setErrorMessageNode] = useState(null) const id = useId(idProp, 'field') const groupId = `${id}-group` const labelId = `${id}-label` const helperTextId = `${id}-help-text` const errorMessageId = `${id}-error-message` const fieldId = groupNode ? undefined : id const describerId = useMemo(() => { if (errorMessageNode) { return errorMessageId } if (helperTextNode) { return helperTextId } return undefined }, [errorMessageNode, helperTextNode, errorMessageId, helperTextId]) const getGroupProps = useCallback( (ownProps) => ({ id: groupId, 'aria-labelledby': labelId, 'aria-describedby': describerId, ref: setGroupNode, ...ownProps, }), [groupId, labelId, describerId] ) const getLabelProps = useCallback( (ownProps) => ({ id: labelId, htmlFor: fieldId, className: classNames( styles.FormLabelWrapper, styles[`position-${labelPosition}`], getFormFieldSizeClassName(size) ), typo: labelPosition === 'top' ? '13' : '14', ...ownProps, }), [fieldId, labelId, labelPosition, size] ) const getFieldProps = useCallback( (ownProps) => ({ id: fieldId, size, 'aria-describedby': groupNode ? undefined : describerId, hasError, required, readOnly, disabled, ...ownProps, }), [ fieldId, describerId, size, hasError, required, readOnly, disabled, groupNode, ] ) const getHelperTextProps = useCallback( (ownProps) => ({ id: helperTextId, visible: isNil(hasError) || !hasError, ref: setHelperTextNode, className: classNames( styles.FormHelperTextWrapper, labelPosition === 'left' && styles['position-left'] ), ...ownProps, }), [helperTextId, labelPosition, hasError] ) const getErrorMessageProps = useCallback( (ownProps) => ({ id: errorMessageId, visible: isNil(hasError) || hasError, ref: setErrorMessageNode, className: classNames( styles.FormHelperTextWrapper, labelPosition === 'left' && styles['position-left'] ), ...ownProps, }), [errorMessageId, labelPosition, hasError] ) const contextValue = useMemo( () => ({ id, labelId, helperTextId, errorMessageId, getGroupProps, getLabelProps, getFieldProps, getHelperTextProps, getErrorMessageProps, hasError, required, readOnly, disabled, }), [ id, labelId, helperTextId, errorMessageId, getGroupProps, getLabelProps, getFieldProps, getHelperTextProps, getErrorMessageProps, hasError, required, readOnly, disabled, ] ) if (!children) { return null } return ( {children} ) } ) export function useFormFieldProps< Props extends FormFieldProps & SizeProps, >(props?: Props) { const contextValue = useFormControlContext() const formFieldProps = useMemo(() => { const mergedProps = contextValue?.getFieldProps(props) ?? { ...props } const { disabled = false, readOnly = false, required = false, hasError = false, size = undefined, ...rest } = mergedProps return { ...rest, 'aria-disabled': ariaAttr(disabled), 'aria-invalid': ariaAttr(hasError), 'aria-required': ariaAttr(required), 'aria-readonly': ariaAttr(readOnly), size, disabled, hasError, required, readOnly, } }, [props, contextValue]) return formFieldProps as typeof formFieldProps & Props }