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.

134 lines (133 loc) 4.33 kB
'use client'; import * as React from 'react'; import { useStableCallback } from '@base-ui-components/utils/useStableCallback'; import { useControlled } from '@base-ui-components/utils/useControlled'; import { useIsoLayoutEffect } from '@base-ui-components/utils/useIsoLayoutEffect'; import { useFieldRootContext } from "../root/FieldRootContext.js"; import { useLabelableContext } from "../../labelable-provider/LabelableContext.js"; import { useLabelableId } from "../../labelable-provider/useLabelableId.js"; import { fieldValidityMapping } from "../utils/constants.js"; import { useRenderElement } from "../../utils/useRenderElement.js"; import { useField } from "../useField.js"; import { createChangeEventDetails } from "../../utils/createBaseUIEventDetails.js"; import { REASONS } from "../../utils/reasons.js"; /** * The form control to label and validate. * Renders an `<input>` element. * * You can omit this part and use any Base UI input component instead. For example, * [Input](https://base-ui.com/react/components/input), [Checkbox](https://base-ui.com/react/components/checkbox), * or [Select](https://base-ui.com/react/components/select), among others, will work with Field out of the box. * * Documentation: [Base UI Field](https://base-ui.com/react/components/field) */ export const FieldControl = /*#__PURE__*/React.forwardRef(function FieldControl(componentProps, forwardedRef) { const { render, className, id: idProp, name: nameProp, value: valueProp, disabled: disabledProp = false, onValueChange, defaultValue, ...elementProps } = componentProps; const { state: fieldState, name: fieldName, disabled: fieldDisabled } = useFieldRootContext(); const disabled = fieldDisabled || disabledProp; const name = fieldName ?? nameProp; const state = React.useMemo(() => ({ ...fieldState, disabled }), [fieldState, disabled]); const { setTouched, setDirty, validityData, setFocused, setFilled, validationMode, validation } = useFieldRootContext(); const { labelId } = useLabelableContext(); const id = useLabelableId({ id: idProp }); useIsoLayoutEffect(() => { const hasExternalValue = valueProp != null; if (validation.inputRef.current?.value || hasExternalValue && valueProp !== '') { setFilled(true); } else if (hasExternalValue && valueProp === '') { setFilled(false); } }, [validation.inputRef, setFilled, valueProp]); const [value, setValueUnwrapped] = useControlled({ controlled: valueProp, default: defaultValue, name: 'FieldControl', state: 'value' }); const isControlled = valueProp !== undefined; const setValue = useStableCallback((nextValue, eventDetails) => { onValueChange?.(nextValue, eventDetails); if (eventDetails.isCanceled) { return; } setValueUnwrapped(nextValue); }); useField({ id, name, commit: validation.commit, value, getValue: () => validation.inputRef.current?.value, controlRef: validation.inputRef }); const element = useRenderElement('input', componentProps, { ref: forwardedRef, state, props: [{ id, disabled, name, ref: validation.inputRef, 'aria-labelledby': labelId, ...(isControlled ? { value } : { defaultValue }), onChange(event) { const inputValue = event.currentTarget.value; setValue(inputValue, createChangeEventDetails(REASONS.none, event.nativeEvent)); setDirty(inputValue !== validityData.initialValue); setFilled(inputValue !== ''); }, onFocus() { setFocused(true); }, onBlur(event) { setTouched(true); setFocused(false); if (validationMode === 'onBlur') { validation.commit(event.currentTarget.value); } }, onKeyDown(event) { if (event.currentTarget.tagName === 'INPUT' && event.key === 'Enter') { setTouched(true); validation.commit(event.currentTarget.value); } } }, validation.getInputValidationProps(), elementProps], stateAttributesMapping: fieldValidityMapping }); return element; }); if (process.env.NODE_ENV !== "production") FieldControl.displayName = "FieldControl";