UNPKG

@loke/ui

Version:
259 lines (219 loc) 7.89 kB
--- name: dropdown-menu type: core domain: overlays requires: [loke-ui] description: > Trigger-based dropdown menus. DropdownMenu, DropdownMenuTrigger, DropdownMenuPortal, DropdownMenuContent, DropdownMenuItem, DropdownMenuCheckboxItem, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSub, DropdownMenuSubTrigger, DropdownMenuSubContent. modal=true default. onSelect fires and auto-closes; onClick does not auto-close. Typeahead requires textValue for icon items. CSS vars: --loke-dropdown-menu-content-available-height, -width, -transform-origin, --loke-dropdown-menu-trigger-height, -width. --- # Dropdown Menu ## Setup Basic menu with portal, items, separator, and group. ```tsx import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuSeparator, DropdownMenuTrigger, } from "@loke/ui/dropdown-menu"; function Example() { return ( <DropdownMenu> <DropdownMenuTrigger>Actions</DropdownMenuTrigger> <DropdownMenuPortal> <DropdownMenuContent> <DropdownMenuGroup> <DropdownMenuLabel>File</DropdownMenuLabel> <DropdownMenuItem onSelect={() => console.log("new")}> New </DropdownMenuItem> <DropdownMenuItem onSelect={() => console.log("open")}> Open </DropdownMenuItem> </DropdownMenuGroup> <DropdownMenuSeparator /> <DropdownMenuItem onSelect={() => console.log("delete")}> Delete </DropdownMenuItem> </DropdownMenuContent> </DropdownMenuPortal> </DropdownMenu> ); } ``` `DropdownMenu` defaults to `modal={true}`. `DropdownMenuPortal` renders into `document.body`. Arrow keys navigate items, typeahead jumps to matching items by first character. ## Core Patterns ### Checkbox items ```tsx import { useState } from "react"; import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuItemIndicator, DropdownMenuPortal, DropdownMenuTrigger, } from "@loke/ui/dropdown-menu"; function ViewOptions() { const [showGrid, setShowGrid] = useState(false); const [showRulers, setShowRulers] = useState(true); return ( <DropdownMenu> <DropdownMenuTrigger>View</DropdownMenuTrigger> <DropdownMenuPortal> <DropdownMenuContent> <DropdownMenuCheckboxItem checked={showGrid} onCheckedChange={setShowGrid} > <DropdownMenuItemIndicator></DropdownMenuItemIndicator> Show grid </DropdownMenuCheckboxItem> <DropdownMenuCheckboxItem checked={showRulers} onCheckedChange={setShowRulers} > <DropdownMenuItemIndicator></DropdownMenuItemIndicator> Show rulers </DropdownMenuCheckboxItem> </DropdownMenuContent> </DropdownMenuPortal> </DropdownMenu> ); } ``` `DropdownMenuItemIndicator` renders only when the item is checked. Style it with `data-state="checked"` / `data-state="unchecked"`. ### Radio items ```tsx import { useState } from "react"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItemIndicator, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuTrigger, } from "@loke/ui/dropdown-menu"; function SortMenu() { const [sort, setSort] = useState("name"); return ( <DropdownMenu> <DropdownMenuTrigger>Sort by</DropdownMenuTrigger> <DropdownMenuPortal> <DropdownMenuContent> <DropdownMenuRadioGroup value={sort} onValueChange={setSort}> <DropdownMenuRadioItem value="name"> <DropdownMenuItemIndicator></DropdownMenuItemIndicator> Name </DropdownMenuRadioItem> <DropdownMenuRadioItem value="date"> <DropdownMenuItemIndicator></DropdownMenuItemIndicator> Date modified </DropdownMenuRadioItem> <DropdownMenuRadioItem value="size"> <DropdownMenuItemIndicator></DropdownMenuItemIndicator> Size </DropdownMenuRadioItem> </DropdownMenuRadioGroup> </DropdownMenuContent> </DropdownMenuPortal> </DropdownMenu> ); } ``` ### Submenus ```tsx import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuPortal, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, } from "@loke/ui/dropdown-menu"; function MenuWithSubmenu() { return ( <DropdownMenu> <DropdownMenuTrigger>Options</DropdownMenuTrigger> <DropdownMenuPortal> <DropdownMenuContent> <DropdownMenuItem onSelect={() => {}}>Cut</DropdownMenuItem> <DropdownMenuItem onSelect={() => {}}>Copy</DropdownMenuItem> <DropdownMenuSub> <DropdownMenuSubTrigger>Share</DropdownMenuSubTrigger> <DropdownMenuPortal> <DropdownMenuSubContent> <DropdownMenuItem onSelect={() => {}}>Email</DropdownMenuItem> <DropdownMenuItem onSelect={() => {}}>Slack</DropdownMenuItem> </DropdownMenuSubContent> </DropdownMenuPortal> </DropdownMenuSub> </DropdownMenuContent> </DropdownMenuPortal> </DropdownMenu> ); } ``` `DropdownMenuSubContent` must also be wrapped in `DropdownMenuPortal` to avoid stacking and clipping issues. ## Common Mistakes ### Using onClick instead of onSelect `DropdownMenuItem` fires a `menu.itemSelect` custom event that calls `onSelect` and then auto-closes the menu. `onClick` does not trigger the close sequence. ```tsx // Wrong — menu stays open after click <DropdownMenuItem onClick={() => doAction()}> Action </DropdownMenuItem> // Correct — menu closes after selection <DropdownMenuItem onSelect={() => doAction()}> Action </DropdownMenuItem> ``` To prevent auto-close on selection (e.g., for a checkbox item you want to stay open), call `event.preventDefault()` inside `onSelect`. Source: `src/components/menu/menu.tsx` — `MenuItem` dispatches `menu.itemSelect` custom event. ### Forgetting DropdownMenuPortal around DropdownMenuContent Without a portal, the menu content renders in the DOM flow. It will be clipped by `overflow: hidden` ancestors and may appear behind other elements due to z-index stacking. ```tsx // Wrong — may be clipped or obscured <DropdownMenu> <DropdownMenuTrigger>Actions</DropdownMenuTrigger> <DropdownMenuContent>...</DropdownMenuContent> </DropdownMenu> // Correct <DropdownMenu> <DropdownMenuTrigger>Actions</DropdownMenuTrigger> <DropdownMenuPortal> <DropdownMenuContent>...</DropdownMenuContent> </DropdownMenuPortal> </DropdownMenu> ``` Source: `src/components/dropdown-menu/dropdown-menu.tsx`. ### Not providing textValue for items with non-text children Menu typeahead reads `textContent` from each item to match keystrokes. If an item contains icons, badges, or other non-text nodes, `textContent` includes the icon's text nodes and typeahead matches incorrectly or not at all. ```tsx // Wrong — typeahead reads "Delete" plus any icon text <DropdownMenuItem> <TrashIcon /> Delete </DropdownMenuItem> // Correct — typeahead uses explicit value <DropdownMenuItem textValue="Delete"> <TrashIcon /> Delete </DropdownMenuItem> ``` Source: `src/components/menu/menu.tsx` — typeahead uses `textContent` by default. ## Cross-references - See also: [references/menu-components.md](references/menu-components.md) — full sub-component prop reference - See also: [popover](../popover/SKILL.md) — for custom floating panels that aren't menus - See also: [choosing-the-right-component](../choosing-the-right-component/SKILL.md) — DropdownMenu vs Select decision