'use client'; import * as React from 'react'; import { Pressable, type GestureResponderEvent } from 'react-native'; import { useControlled } from '../../hooks/useControlled'; import { useRenderElement } from '../../use-render/useRenderElement'; import { useButton } from '../../internals/use-button/useButton'; import type { ZestUIComponentProps } from '../../types'; import { createChangeEventDetails, type ZestChangeEventDetails, } from '../../utils/createChangeEventDetails'; import { REASONS } from '../../utils/reasons'; import { useCheckboxGroupContext } from '../../checkbox-group/CheckboxGroupContext'; import { useFieldControlRegistration } from '../../internals/field/useFieldControlRegistration'; import { CheckboxRootContext } from './CheckboxRootContext'; /** * Represents the checkbox itself. * Renders a ``. * * Placed inside a `Field.Root`, it is labelled by `Field.Label`, described by * `Field.Description`/`Error`, inherits the field's `disabled`, and runs the * field's `validate` on change. */ export function CheckboxRoot(componentProps: CheckboxRoot.Props) { const { checked: checkedProp, className, defaultChecked = false, disabled: disabledProp = false, indeterminate = false, onCheckedChange, parent = false, readOnly = false, render, required = false, style, value, ref, ...elementProps } = componentProps; const groupContext = useCheckboxGroupContext(); const parentContext = groupContext?.parent; const isGroupedWithParent = parentContext != null && groupContext?.allValues != null; const { fieldDisabled, fieldProps, markChanged, markTouched } = useFieldControlRegistration({ initialValue: defaultChecked ?? checkedProp ?? false, ownsValue: true, }); const disabled = (groupContext?.disabled || disabledProp || fieldDisabled) ?? false; const { getButtonProps } = useButton({ disabled }); let groupProps: Partial> = {}; if (isGroupedWithParent) { if (parent) { groupProps = parentContext.getParentProps(); } else if (value) { groupProps = parentContext.getChildProps(value); } } const { checked: groupChecked = checkedProp, indeterminate: groupIndeterminate = indeterminate, onCheckedChange: groupOnChange, } = groupProps; const groupValue = groupContext?.value; const setGroupValue = groupContext?.setValue; const defaultGroupValue = groupContext?.defaultValue; const [checked, setCheckedState] = useControlled({ controlled: value && groupValue && !parent ? groupValue.includes(value) : groupChecked, default: value && defaultGroupValue && !parent ? defaultGroupValue.includes(value) : defaultChecked, name: 'Checkbox', state: 'checked', }); const computedChecked = isGroupedWithParent ? Boolean(groupChecked) : checked; const computedIndeterminate = isGroupedWithParent ? groupIndeterminate || indeterminate : indeterminate; React.useEffect(() => { if (!parentContext || !value) { return undefined; } const disabledStates = parentContext.disabledStatesRef.current; disabledStates.set(value, disabled); return () => { disabledStates.delete(value); }; }, [parentContext, disabled, value]); const state: CheckboxRootState = React.useMemo( () => ({ checked: computedChecked, disabled, readOnly, required, indeterminate: computedIndeterminate, }), [computedChecked, disabled, readOnly, required, computedIndeterminate], ); const element = useRenderElement(Pressable, componentProps, { state, ref, props: [ { onPress(event: GestureResponderEvent) { if (readOnly || disabled) { return; } const nextChecked = !computedChecked; const eventDetails = createChangeEventDetails(REASONS.none, event); onCheckedChange?.(nextChecked, eventDetails); if (eventDetails.isCanceled) { return; } groupOnChange?.(nextChecked, eventDetails); if (eventDetails.isCanceled) { return; } setCheckedState(nextChecked); // A press is both the change and the end of the interaction: there is // no blur on a checkbox to mark it touched later. markChanged(nextChecked); markTouched(nextChecked); if (value && groupValue && setGroupValue && !parent && !isGroupedWithParent) { const nextGroupValue = nextChecked ? [...groupValue, value] : groupValue.filter((item) => item !== value); setGroupValue(nextGroupValue, eventDetails); } }, accessibilityRole: 'checkbox' as const, accessibilityState: { checked: computedIndeterminate ? ('mixed' as const) : computedChecked, disabled: disabled || undefined, }, ...fieldProps, }, elementProps, getButtonProps, ], }); return ( {element} ); } export interface CheckboxRootState { /** * Whether the checkbox is currently ticked. */ checked: boolean; /** * Whether the component should ignore user interaction. */ disabled: boolean; /** * Whether the user should be unable to tick or untick the checkbox. */ readOnly: boolean; /** * Whether the user must tick the checkbox before submitting a form. */ required: boolean; /** * Whether the checkbox is in a mixed state: neither ticked, nor unticked. */ indeterminate: boolean; } export interface CheckboxRootProps extends Omit, 'onPress' | 'value'> { /** * Whether the checkbox is currently ticked. * * To render an uncontrolled checkbox, use the `defaultChecked` prop instead. */ checked?: boolean | undefined; /** * The value identifying the checkbox within a ``. */ value?: string | undefined; /** * Whether the checkbox controls a parent checkbox in a ``. * A parent checkbox ticks or unticks all of the group's checkboxes at once, * and reports a mixed state while only some of them are ticked. * @default false */ parent?: boolean | undefined; /** * Whether the checkbox is initially ticked. * * To render a controlled checkbox, use the `checked` prop instead. * @default false */ defaultChecked?: boolean | undefined; /** * Whether the component should ignore user interaction. * @default false */ disabled?: boolean | undefined; /** * Whether the checkbox is in a mixed state: neither ticked, nor unticked. * @default false */ indeterminate?: boolean | undefined; /** * Event handler called when the checkbox is ticked or unticked. */ onCheckedChange?: | ((checked: boolean, eventDetails: CheckboxRoot.ChangeEventDetails) => void) | undefined; /** * Whether the user should be unable to tick or untick the checkbox. * @default false */ readOnly?: boolean | undefined; /** * Whether the user must tick the checkbox before submitting a form. * @default false */ required?: boolean | undefined; } export type CheckboxRootChangeEventReason = typeof REASONS.none; export namespace CheckboxRoot { export type State = CheckboxRootState; export type Props = CheckboxRootProps; export type ChangeEventReason = CheckboxRootChangeEventReason; export type ChangeEventDetails = ZestChangeEventDetails; }