import { Fragment, type ReactNode } from 'react';
import {
DropdownMenuCheckboxItem,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
} from '../dropdown-menu';
import { MenuIndicatorRow, MenuRow } from './menu-row';
import type { MenuItem } from './types';
interface RenderOptions {
showDescriptions: boolean;
}
/** Drop `hidden` rows before rendering. */
function isVisible(item: MenuItem): boolean {
return !('hidden' in item && item.hidden);
}
/**
* Recursive renderer — powers both the root content and every
* `SubContent`. Pure: it only maps data to Radix elements.
*/
export function renderItems(
items: MenuItem[],
opts: RenderOptions,
): ReactNode {
return items.filter(isVisible).map((item, index) => {
switch (item.kind) {
case 'separator':
return ;
case 'label':
return (
{item.label}
);
case 'custom':
return {item.render()};
case 'item':
return (
{
// closeOnSelect=false → keep the menu open.
if (item.closeOnSelect === false) event.preventDefault();
item.onSelect?.(event);
}}
>
);
case 'submenu':
return (
{renderItems(item.items, opts)}
);
case 'checkbox':
return (
);
case 'radio-group':
return (
{item.label ? (
{item.label}
) : null}
{item.options.filter((o) => !o.hidden).map((option) => (
))}
);
case 'section': {
// A separator follows the section unless explicitly disabled or
// it is the last visible item.
const isLast =
index === items.filter(isVisible).length - 1;
const withSeparator = item.separator !== false && !isLast;
return (
{item.label ? (
{item.label}
) : null}
{renderItems(item.items, opts)}
{withSeparator ? : null}
);
}
default: {
// Exhaustiveness guard — a new `kind` without a branch fails here.
const _never: never = item;
return _never;
}
}
});
}