// 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';