'use client'; import * as React from 'react'; import { Pressable, type GestureResponderEvent } from 'react-native'; import { useMenuRootContext } from '../root/MenuRootContext'; import { useMenuSubmenuRootContext } from '../submenu-root/MenuSubmenuRootContext'; import { useRenderElement } from '../../use-render/useRenderElement'; import { useButton } from '../../internals/use-button/useButton'; import { useCompositeListItem } from '../../internals/composite/list/useCompositeListItem'; import { useControlled } from '../../hooks/useControlled'; import type { ZestUIComponentProps } from '../../types'; import type { ZestChangeEventDetails } from '../../utils/createChangeEventDetails'; import { createChangeEventDetails } from '../../utils/createChangeEventDetails'; import { REASONS } from '../../utils/reasons'; import { MenuCheckboxItemContext } from './MenuCheckboxItemContext'; /** * A menu item that toggles a setting on and off. * Renders a ``. * * Unlike `Menu.Item`, `closeOnPress` defaults to `false`: ticking a box is * usually not the end of the interaction. */ export function MenuCheckboxItem(componentProps: MenuCheckboxItem.Props) { const { checked: checkedProp, className, closeOnClick: closeOnClickProp, closeOnPress: closeOnPressProp, defaultChecked = false, disabled = false, onCheckedChange, onPress: onPressProp, render, style, ref, ...elementProps } = componentProps; // `closeOnPress` is the React Native name; `closeOnPress` is a deprecated // alias kept for Base UI parity. const closeOnPress = closeOnPressProp ?? closeOnClickProp ?? false; const store = useMenuRootContext(); const submenuRootContext = useMenuSubmenuRootContext(); const { index, onLayout } = useCompositeListItem(); const [checked, setCheckedState] = useControlled({ controlled: checkedProp, default: defaultChecked, name: 'MenuCheckboxItem', state: 'checked', }); const [pressed, setPressed] = React.useState(false); const { getButtonProps } = useButton({ disabled }); const state: MenuCheckboxItemState = React.useMemo( () => ({ checked, disabled, pressed, index }), [checked, disabled, pressed, index], ); const element = useRenderElement(Pressable, componentProps, { state, ref, props: [ { onLayout, accessibilityRole: 'menuitem' as const, accessibilityState: { checked, disabled: disabled || undefined }, 'aria-checked': checked, onPress(event: GestureResponderEvent) { if (disabled) { return; } const nextChecked = !checked; const eventDetails = createChangeEventDetails(REASONS.itemPress, event); onCheckedChange?.(nextChecked, eventDetails); if (eventDetails.isCanceled) { return; } setCheckedState(nextChecked); onPressProp?.(event); if (closeOnPress) { store.setOpen(false, createChangeEventDetails(REASONS.itemPress, event)); submenuRootContext?.closeAncestors(REASONS.itemPress, event); } }, onPressIn() { setPressed(true); }, onPressOut() { setPressed(false); }, }, elementProps, getButtonProps, ], }); return ( {element} ); } export interface MenuCheckboxItemState { /** * Whether the checkbox item is currently ticked. */ checked: boolean; /** * Whether the component should ignore user interaction. */ disabled: boolean; /** * Whether the item is currently pressed. */ pressed: boolean; /** * The item's index in the menu. */ index: number; } export interface MenuCheckboxItemProps extends ZestUIComponentProps { /** * Whether the checkbox item is currently ticked. * * To render an uncontrolled checkbox item, use the `defaultChecked` prop instead. */ checked?: boolean | undefined; /** * Whether the checkbox item is initially ticked. * * To render a controlled checkbox item, use the `checked` prop instead. * @default false */ defaultChecked?: boolean | undefined; /** * Event handler called when the checkbox item is ticked or unticked. */ onCheckedChange?: | ((checked: boolean, eventDetails: MenuCheckboxItem.ChangeEventDetails) => void) | undefined; /** * Whether the component should ignore user interaction. * @default false */ disabled?: boolean | undefined; /** * Whether to close the menu when the item is pressed. * @default false */ closeOnPress?: boolean | undefined; /** * @deprecated Use `closeOnPress`. Kept as an alias for Base UI parity. */ closeOnClick?: boolean | undefined; } export type MenuCheckboxItemChangeEventReason = typeof REASONS.itemPress | typeof REASONS.none; export type MenuCheckboxItemChangeEventDetails = ZestChangeEventDetails; export namespace MenuCheckboxItem { export type State = MenuCheckboxItemState; export type Props = MenuCheckboxItemProps; export type ChangeEventReason = MenuCheckboxItemChangeEventReason; export type ChangeEventDetails = MenuCheckboxItemChangeEventDetails; }