// Copyright (c) Meta Platforms, Inc. and affiliates. 'use client'; /** * @file DropdownMenuRadioItem.tsx * @input React, stylex, Item + Icon + DropdownMenu context + tokens from core * @output DropdownMenuRadioItem — a single option in a radio group. * @position Sub-component; must be used inside a DropdownMenuRadioGroup. * * A menu item representing one option in a single-select group * (role="menuitemradio"). The row owns the role and aria-checked; there is no * nested native (per the WAI-ARIA menuitemradio pattern). Selection * state + onChange come from DropdownMenuRadioGroupContext. Keyboard nav + * Enter/Space activation come from the parent DropdownMenu. * * The round radio visual is the shared radio indicator, decorative * (aria-hidden) — the row owns the checked state, and menu radios pick up the * same `radio` theming (and any theme replacement) as RadioList. This row keeps * the marker box: its size is derived from the menu's item size and it swaps to * the inline-end of the row on coarse-pointer (touch) devices via CSS `order`. */ 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"; import { useDropdownMenuRadioGroupContext } from "./DropdownMenuContext.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 RadioList uses, so a radio reads the same in a // menu row as it does in a radio group. export interface DropdownMenuRadioItemProps extends Omit { /** * The value this item represents within its group. The group's `value` * matches against this to determine the checked state. */ value: string; /** * Primary label text identifying the option. */ 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 this individual radio item is disabled. Disabled items stay * focusable (via `aria-disabled`) so they remain discoverable by keyboard * and assistive technology, but selection is blocked. * @default false */ isDisabled?: boolean; /** * Content to render after the label and description, such as a badge or * metadata. */ endContent?: ReactNode; } /** * A single option in a DropdownMenuRadioGroup (role="menuitemradio"). * * @example * ``` * * * * * ``` */ export function DropdownMenuRadioItem({ value, label, description, icon, isDisabled = false, endContent, xstyle, className, style, ...rest }: DropdownMenuRadioItemProps) { const menuCtx = useDropdownMenuContext(); const groupCtx = useDropdownMenuRadioGroupContext(); if (!groupCtx) { throw new Error('DropdownMenuRadioItem must be used within a DropdownMenuRadioGroup'); } const menuSize = menuCtx?.menuSize ?? 'md'; const controlSize = menuSize === 'sm' ? 'sm' : 'md'; const isChecked = groupCtx.value === value; const RadioControl = useIndicator('radio'); const handleClick = useCallback(() => { if (isDisabled) { return; } groupCtx.onChange(value); if (groupCtx.hasCloseOnSelect) { menuCtx?.closeMenu(); } }, [isDisabled, groupCtx, value, menuCtx]); 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 })} />; } DropdownMenuRadioItem.displayName = 'DropdownMenuRadioItem';