@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.
137 lines (136 loc) • 4.77 kB
JavaScript
'use client';
import * as React from 'react';
import { useControlled } from '../../utils/useControlled.js';
import { visuallyHidden } from '../../utils/visuallyHidden.js';
import { useForkRef } from '../../utils/useForkRef.js';
import { mergeReactProps } from '../../utils/mergeReactProps.js';
import { useBaseUiId } from '../../utils/useBaseUiId.js';
import { useEventCallback } from '../../utils/useEventCallback.js';
import { useEnhancedEffect } from '../../utils/useEnhancedEffect.js';
import { useFieldRootContext } from '../../field/root/FieldRootContext.js';
import { useFieldControlValidation } from '../../field/control/useFieldControlValidation.js';
import { useField } from '../../field/useField.js';
import { useCheckboxGroupContext } from '../../checkbox-group/CheckboxGroupContext.js';
export function useCheckboxRoot(params) {
const {
id: idProp,
checked: externalChecked,
inputRef: externalInputRef,
onCheckedChange: onCheckedChangeProp = () => {},
name,
value,
defaultChecked = false,
readOnly = false,
required = false,
autoFocus = false,
indeterminate = false,
disabled = false
} = params;
const groupContext = useCheckboxGroupContext();
const groupValue = groupContext?.value;
const setGroupValue = groupContext?.setValue;
const defaultGroupValue = groupContext?.defaultValue;
const [checked, setCheckedState] = useControlled({
controlled: name && groupValue ? groupValue.includes(name) : externalChecked,
default: name && defaultGroupValue ? defaultGroupValue.includes(name) : defaultChecked,
name: 'Checkbox',
state: 'checked'
});
const {
labelId,
setControlId,
setTouched,
setDirty,
validityData
} = useFieldRootContext();
const buttonRef = React.useRef(null);
const {
getValidationProps,
getInputValidationProps,
inputRef: inputValidationRef,
commitValidation
} = useFieldControlValidation();
const onCheckedChange = useEventCallback(onCheckedChangeProp);
const id = useBaseUiId(idProp);
useEnhancedEffect(() => {
setControlId(id);
return () => {
setControlId(undefined);
};
}, [id, setControlId]);
useField({
id,
commitValidation,
value: checked,
controlRef: buttonRef
});
const inputRef = React.useRef(null);
const mergedInputRef = useForkRef(externalInputRef, inputRef, inputValidationRef);
React.useEffect(() => {
if (inputRef.current) {
inputRef.current.indeterminate = indeterminate;
}
}, [indeterminate]);
const getButtonProps = React.useCallback((externalProps = {}) => mergeReactProps(getValidationProps(externalProps), {
id,
ref: buttonRef,
type: 'button',
role: 'checkbox',
disabled,
'aria-checked': indeterminate ? 'mixed' : checked,
'aria-readonly': readOnly || undefined,
'aria-labelledby': labelId,
onBlur() {
const element = inputRef.current;
if (!element) {
return;
}
setTouched(true);
commitValidation(element.checked);
},
onClick(event) {
if (event.defaultPrevented || readOnly) {
return;
}
event.preventDefault();
inputRef.current?.click();
}
}), [id, getValidationProps, indeterminate, checked, disabled, readOnly, labelId, setTouched, commitValidation]);
const getInputProps = React.useCallback((externalProps = {}) => mergeReactProps(getInputValidationProps(externalProps), {
checked,
disabled,
name,
// React <19 sets an empty value if `undefined` is passed explicitly
// To avoid this, we only set the value if it's defined
...(value !== undefined ? {
value
} : {}),
required,
autoFocus,
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;
setDirty(nextChecked !== validityData.initialValue);
setCheckedState(nextChecked);
onCheckedChange?.(nextChecked, event.nativeEvent);
if (name && groupValue && setGroupValue) {
const nextGroupValue = nextChecked ? [...groupValue, name] : groupValue.filter(item => item !== name);
setGroupValue(nextGroupValue, event.nativeEvent);
}
}
}), [getInputValidationProps, checked, disabled, name, value, required, autoFocus, mergedInputRef, setDirty, validityData.initialValue, setCheckedState, onCheckedChange, groupValue, setGroupValue]);
return React.useMemo(() => ({
checked,
getButtonProps,
getInputProps
}), [checked, getButtonProps, getInputProps]);
}
export let UseCheckboxRoot;