Dropdown menu
Radix's menu is the hardest part to rebuild: roving focus, typeahead, submenus with a pointer-intent polygon, checkbox and radio items, and a trigger that opens on…
Radix's menu is the hardest part to rebuild: roving focus, typeahead, submenus with a pointer-intent polygon, checkbox
and radio items, and a trigger that opens on pointer down. DropdownMenu provides all of it, so the shadcn port is
again only class names.
src/ui/dropdown-menu.tsxtsx
import { DropdownMenu as MenuPrimitive, mergeElementProps } from "@shadow-js/primitives";
import type { JSX } from "@shadow-js/web";
import { CheckIcon } from "./icons";
export const DropdownMenu = MenuPrimitive;
export const DropdownMenuTrigger = MenuPrimitive.Trigger;
export const DropdownMenuGroup = MenuPrimitive.Group;
export const DropdownMenuRadioGroup = MenuPrimitive.RadioGroup;
export function DropdownMenuContent(props: JSX.IntrinsicElements["div"] & { sideOffset?: number }) {
return (
<MenuPrimitive.Portal>
<MenuPrimitive.Content
{...mergeElementProps(
{
sideOffset: 4,
class:
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out",
},
props,
)}
/>
</MenuPrimitive.Portal>
);
}
const ITEM =
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50";
export function DropdownMenuItem(
props: JSX.IntrinsicElements["div"] & { disabled?: boolean; onSelect?: (event: Event) => void },
) {
return <MenuPrimitive.Item {...mergeElementProps({ class: ITEM }, props)} />;
}
export function DropdownMenuCheckboxItem(
props: JSX.IntrinsicElements["div"] & {
checked?: boolean;
onCheckedChange?: (checked: boolean) => void;
disabled?: boolean;
},
) {
return (
<MenuPrimitive.CheckboxItem {...mergeElementProps({ class: `${ITEM} pl-8` }, props)}>
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<MenuPrimitive.ItemIndicator>
<CheckIcon class="h-4 w-4" />
</MenuPrimitive.ItemIndicator>
</span>
{props.children}
</MenuPrimitive.CheckboxItem>
);
}
export function DropdownMenuRadioItem(props: JSX.IntrinsicElements["div"] & { value: string }) {
return (
<MenuPrimitive.RadioItem {...mergeElementProps({ class: `${ITEM} pl-8` }, props)} value={props.value}>
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<MenuPrimitive.ItemIndicator>
<CheckIcon class="h-4 w-4" />
</MenuPrimitive.ItemIndicator>
</span>
{props.children}
</MenuPrimitive.RadioItem>
);
}
export function DropdownMenuLabel(props: JSX.IntrinsicElements["div"]) {
return <MenuPrimitive.Label {...mergeElementProps({ class: "px-2 py-1.5 text-sm font-semibold" }, props)} />;
}
export function DropdownMenuSeparator(props: JSX.IntrinsicElements["div"]) {
return <MenuPrimitive.Separator {...mergeElementProps({ class: "-mx-1 my-1 h-px bg-muted" }, props)} />;
}