// Copyright (c) Meta Platforms, Inc. and affiliates. 'use client'; /** * @file DropdownMenuCheckboxItem.tsx * @input React, stylex, Item + CheckboxInput + DropdownMenu context + tokens from core * @output DropdownMenuCheckboxItem — a standalone checkable menu item. * @position Sub-component; used inside DropdownMenu. * * A menu item that toggles an independent boolean (role="menuitemcheckbox"). * Unlike CheckboxInput, there is no nested native that participates in * accessibility: the row itself owns the role and aria-checked, per the * WAI-ARIA menuitemcheckbox pattern. Keyboard navigation (arrows/typeahead) and * Enter/Space activation come from the parent DropdownMenu's useListFocus + * activation path, which matches menuitemcheckbox alongside plain menuitem rows. * * The checkbox visual is the shared checkbox indicator, decorative * (aria-hidden) — the row owns the role, checked state, and accessible name, so * there is no nested native to shim out of the accessibility tree. It * picks up the same `checkbox` theming (and any theme replacement) as * CheckboxInput. The control size is derived from the menu's item size (a `sm` * menu gets the compact control; `md`/`lg` get the standard one) and the marker * box swaps to the inline-end of the row on coarse-pointer (touch) devices via * CSS `order`, so it lands where selection toggles are conventionally placed on * mobile. */ import { useCallback, type PointerEvent, type ReactNode } from 'react'; import * as stylex from "../style-runtime.js"; import { renderIconSlot, type IconType } from "../Icon/index.js"; import { useIndicator } from "../Indicator/index.js"; import { Item } from "../Item/index.js"; import { useDropdownMenuContext } from "./DropdownMenuContext.js"; import { focusMenuItemOnHover } from "./menuItemHover.js"; import "../theme/tokens.stylex.js"; import { colorVars, spacingVars } from "../theme/tokens.stylex.js"; import { mergeProps, themeProps } from "../utils/index.js"; import type { BaseProps } from "../BaseProps.js"; const styles = { root: { kzqmXN: "xh8yej3", kaIpWk: "x1ws5lxm", kMwMTN: "x1tgivj0", kWkggS: "xjbqb8w x1c52tdz", kkrTdU: "x1ypdohk x16khyan", kI3sdo: "x1a2a7pz", $$css: true as const }, disabled: { kSiTet: "xbyyjgo", kkrTdU: "xt0e3qv", $$css: true as const }, marker: { kfzvcC: "x47corl", kayTVb: "x1g77sc7 xozvky", keTefX: "x1lziwak x1jymrmb", $$css: true as const } }; // Matches the control sizes CheckboxInput uses, so a checkbox reads the same in // a menu row as it does in a form. export interface DropdownMenuCheckboxItemProps extends Omit { /** * Primary label text identifying the item. */ label: ReactNode; /** * Secondary description text displayed below the label. */ description?: ReactNode; /** * Icon to display before the label. Accepts a semantic icon name (see * `npx astryx docs icons`) or a rendered node. */ icon?: ReactNode | IconType; /** * Whether the item is checked. Controlled — pair with `onChange`. */ value: boolean; /** * Callback fired with the next checked state when the item is toggled. */ onChange?: (checked: boolean) => void; /** * Whether the item is disabled. Disabled items stay focusable (via * `aria-disabled`) so they remain discoverable by keyboard and assistive * technology, but activation is blocked. * @default false */ isDisabled?: boolean; /** * Whether toggling the item closes the menu. Checkbox items default to * staying open so several can be toggled in a single session, unlike radio * items which default to closing on selection. * @default false */ hasCloseOnSelect?: boolean; /** * Content to render after the label and description, such as a keyboard * shortcut hint or badge. */ endContent?: ReactNode; } /** * A checkable dropdown menu item (role="menuitemcheckbox"). * * Must be used inside a DropdownMenu. Toggles an independent boolean; for a * one-of-N choice use DropdownMenuRadioGroup + DropdownMenuRadioItem instead. * * @example * ``` * import {DropdownMenuCheckboxItem} from '@astryxdesign/core/DropdownMenu'; * * * * ``` */ export function DropdownMenuCheckboxItem({ label, description, icon, value, onChange, isDisabled = false, hasCloseOnSelect = false, endContent, xstyle, className, style, ...rest }: DropdownMenuCheckboxItemProps) { const ctx = useDropdownMenuContext(); const menuSize = ctx?.menuSize ?? 'md'; const controlSize = menuSize === 'sm' ? 'sm' : 'md'; const CheckboxControl = useIndicator('checkbox'); const handleClick = useCallback(() => { if (isDisabled) { return; } onChange?.(!value); if (hasCloseOnSelect) { ctx?.closeMenu(); } }, [isDisabled, onChange, value, hasCloseOnSelect, ctx]); const handlePointerMove = useCallback((e: PointerEvent) => focusMenuItemOnHover(e, isDisabled), [isDisabled]); return } startContent={icon ? renderIconSlot(icon, { size: 'sm', color: 'secondary' }) : undefined} label={label} description={description} endContent={endContent} onClick={handleClick} isDisabled={isDisabled} xstyle={[styles.root, isDisabled && styles.disabled, xstyle]} {...mergeProps(themeProps('dropdown-menu-item', { size: menuSize }), { className, style })} />; } DropdownMenuCheckboxItem.displayName = 'DropdownMenuCheckboxItem';