[]previous and next page

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…

For agentsUpdated

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)} />;
}