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.

269 lines (266 loc) 9.43 kB
'use client'; import * as React from 'react'; import { EMPTY_OBJECT } from '@base-ui-components/utils/empty'; import { useControlled } from '@base-ui-components/utils/useControlled'; import { useStableCallback } from '@base-ui-components/utils/useStableCallback'; import { useIsoLayoutEffect } from '@base-ui-components/utils/useIsoLayoutEffect'; import { useMergedRefs } from '@base-ui-components/utils/useMergedRefs'; import { visuallyHidden } from '@base-ui-components/utils/visuallyHidden'; import { NOOP } from "../../utils/noop.js"; import { useStateAttributesMapping } from "../utils/useStateAttributesMapping.js"; import { useRenderElement } from "../../utils/useRenderElement.js"; import { useBaseUiId } from "../../utils/useBaseUiId.js"; import { mergeProps } from "../../merge-props/index.js"; import { useButton } from "../../use-button/useButton.js"; import { useFieldRootContext } from "../../field/root/FieldRootContext.js"; import { useFieldItemContext } from "../../field/item/FieldItemContext.js"; import { useField } from "../../field/useField.js"; import { useFormContext } from "../../form/FormContext.js"; import { useLabelableContext } from "../../labelable-provider/LabelableContext.js"; import { useCheckboxGroupContext } from "../../checkbox-group/CheckboxGroupContext.js"; import { CheckboxRootContext } from "./CheckboxRootContext.js"; import { createChangeEventDetails } from "../../utils/createBaseUIEventDetails.js"; import { REASONS } from "../../utils/reasons.js"; import { useValueChanged } from "../../utils/useValueChanged.js"; import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; export const PARENT_CHECKBOX = 'data-parent'; /** * Represents the checkbox itself. * Renders a `<span>` element and a hidden `<input>` beside. * * Documentation: [Base UI Checkbox](https://base-ui.com/react/components/checkbox) */ export const CheckboxRoot = /*#__PURE__*/React.forwardRef(function CheckboxRoot(componentProps, forwardedRef) { const { checked: checkedProp, className, defaultChecked = false, disabled: disabledProp = false, id: idProp, indeterminate = false, inputRef: inputRefProp, name: nameProp, onCheckedChange: onCheckedChangeProp, parent = false, readOnly = false, render, required = false, value: valueProp, nativeButton = false, ...elementProps } = componentProps; const { clearErrors } = useFormContext(); const { disabled: rootDisabled, name: fieldName, setDirty, setFilled, setFocused, setTouched, state: fieldState, validationMode, validityData, shouldValidateOnChange, validation: localValidation } = useFieldRootContext(); const fieldItemContext = useFieldItemContext(); const { labelId, controlId, setControlId, getDescriptionProps } = useLabelableContext(); const groupContext = useCheckboxGroupContext(); const parentContext = groupContext?.parent; const isGroupedWithParent = parentContext && groupContext.allValues; const disabled = rootDisabled || fieldItemContext.disabled || groupContext?.disabled || disabledProp; const name = fieldName ?? nameProp; const value = valueProp ?? name; const id = useBaseUiId(); const parentId = useBaseUiId(); let inputId = controlId; if (isGroupedWithParent) { inputId = parent ? parentId : `${parentContext.id}-${value}`; } else if (idProp) { inputId = idProp; } let groupProps = {}; if (isGroupedWithParent) { if (parent) { groupProps = groupContext.parent.getParentProps(); } else if (value) { groupProps = groupContext.parent.getChildProps(value); } } const onCheckedChange = useStableCallback(onCheckedChangeProp); const { checked: groupChecked = checkedProp, indeterminate: groupIndeterminate = indeterminate, onCheckedChange: groupOnChange, ...otherGroupProps } = groupProps; const groupValue = groupContext?.value; const setGroupValue = groupContext?.setValue; const defaultGroupValue = groupContext?.defaultValue; const controlRef = React.useRef(null); const { getButtonProps, buttonRef } = useButton({ disabled, native: nativeButton }); const validation = groupContext?.validation ?? localValidation; const [checked, setCheckedState] = useControlled({ controlled: value && groupValue && !parent ? groupValue.includes(value) : groupChecked, default: value && defaultGroupValue && !parent ? defaultGroupValue.includes(value) : defaultChecked, name: 'Checkbox', state: 'checked' }); // can't use useLabelableId because of optional groupContext and/or parent useIsoLayoutEffect(() => { if (setControlId === NOOP) { return undefined; } setControlId(inputId); return () => { setControlId(undefined); }; }, [inputId, groupContext, setControlId, parent]); useField({ enabled: !groupContext, id, commit: validation.commit, value: checked, controlRef, name, getValue: () => checked }); const inputRef = React.useRef(null); const mergedInputRef = useMergedRefs(inputRefProp, inputRef, validation.inputRef); useIsoLayoutEffect(() => { if (inputRef.current) { inputRef.current.indeterminate = groupIndeterminate; if (checked) { setFilled(true); } } }, [checked, groupIndeterminate, setFilled]); useValueChanged(checked, () => { if (groupContext && !parent) { return; } clearErrors(name); setFilled(checked); setDirty(checked !== validityData.initialValue); if (shouldValidateOnChange()) { validation.commit(checked); } else { validation.commit(checked, true); } }); const inputProps = mergeProps({ checked, disabled, // parent checkboxes unset `name` to be excluded from form submission name: parent ? undefined : name, // Set `id` to stop Chrome warning about an unassociated input id: inputId ?? undefined, required, ref: mergedInputRef, style: visuallyHidden, tabIndex: -1, type: 'checkbox', 'aria-hidden': true, onChange(event) { // Workaround for https://github.com/facebook/react/issues/9023 if (event.nativeEvent.defaultPrevented) { return; } const nextChecked = event.target.checked; const details = createChangeEventDetails(REASONS.none, event.nativeEvent); groupOnChange?.(nextChecked, details); onCheckedChange(nextChecked, details); if (details.isCanceled) { return; } setCheckedState(nextChecked); if (value && groupValue && setGroupValue && !parent) { const nextGroupValue = nextChecked ? [...groupValue, value] : groupValue.filter(item => item !== value); setGroupValue(nextGroupValue, details); } }, onFocus() { controlRef.current?.focus(); } }, // React <19 sets an empty value if `undefined` is passed explicitly // To avoid this, we only set the value if it's defined valueProp !== undefined ? { value: (groupContext ? checked && valueProp : valueProp) || '' } : EMPTY_OBJECT, getDescriptionProps, groupContext ? validation.getValidationProps : validation.getInputValidationProps); const computedChecked = isGroupedWithParent ? Boolean(groupChecked) : checked; const computedIndeterminate = isGroupedWithParent ? groupIndeterminate || indeterminate : indeterminate; React.useEffect(() => { if (parentContext && value) { parentContext.disabledStatesRef.current.set(value, disabled); } }, [parentContext, disabled, value]); const state = React.useMemo(() => ({ ...fieldState, checked: computedChecked, disabled, readOnly, required, indeterminate: computedIndeterminate }), [fieldState, computedChecked, disabled, readOnly, required, computedIndeterminate]); const stateAttributesMapping = useStateAttributesMapping(state); const element = useRenderElement('span', componentProps, { state, ref: [buttonRef, controlRef, forwardedRef, groupContext?.registerControlRef], props: [{ id, role: 'checkbox', 'aria-checked': groupIndeterminate ? 'mixed' : checked, 'aria-readonly': readOnly || undefined, 'aria-required': required || undefined, 'aria-labelledby': labelId, [PARENT_CHECKBOX]: parent ? '' : undefined, onFocus() { setFocused(true); }, onBlur() { const inputEl = inputRef.current; if (!inputEl) { return; } setTouched(true); setFocused(false); if (validationMode === 'onBlur') { validation.commit(groupContext ? groupValue : inputEl.checked); } }, onClick(event) { if (readOnly || disabled) { return; } event.preventDefault(); inputRef.current?.click(); } }, getDescriptionProps, validation.getValidationProps, elementProps, otherGroupProps, getButtonProps], stateAttributesMapping }); return /*#__PURE__*/_jsxs(CheckboxRootContext.Provider, { value: state, children: [element, !checked && !groupContext && name && !parent && /*#__PURE__*/_jsx("input", { type: "hidden", name: name, value: "off" }), /*#__PURE__*/_jsx("input", { ...inputProps })] }); }); if (process.env.NODE_ENV !== "production") CheckboxRoot.displayName = "CheckboxRoot";