## dropdown_menu (`poetry_dropdown_menu`)

A menu of actions or options triggered by a button.

Class: Poetry::Ui::DropdownMenu::Component - BEM block `poetry-ui-dropdown_menu`.
Slot REQUIRED: with_trigger (the menu button) - a call without it raises.
Slot REQUIRED: with_item (at least one item) - a call without it raises.
- `align:` (symbol) - one of start|center|end, default "center" - The menu's alignment against the trigger's edge.
- `align_offset:` (integer) - default 0 - Pixel shift along the alignment edge.
- `avoid_collisions:` (boolean) - default true - Flips/shifts placement to keep the menu inside the viewport.
- `dir:` (symbol) - one of ltr|rtl - Reading direction; :rtl flips submenu sides and indicators.
- `disabled:` (boolean) - default false - Disables the menu trigger button.
- `loop:` (boolean) - default false - Arrow-key navigation wraps from the last item back to the first.
- `modal:` (boolean) - default true - While open, pointer interaction outside the menu is blocked; false keeps the rest of the page interactive.
- `open:` (boolean) - default false - Renders the menu already open on page load.
- `side:` (symbol) - one of top|right|bottom|left, default "bottom" - Which side of the trigger the menu opens on (flips on collision).
- `side_offset:` (integer) - default 4 - Gap in pixels between the trigger and the menu.
Slots: items (The menu composition API: one ordered items collection accepting seven kinds, interleaved in call order - with_item an action row (href: renders it as a real link; submit: as a real submit button) with_checkbox_item a toggleable checked/unchecked row with_radio_group a single-select scope; add rows inside it via with_radio_item(value:) with_label a non-interactive heading for a run of items with_separator a horizontal rule between runs with_group semantic grouping around the same union, one level down with_sub a nested submenu: its own with_trigger plus the same union, recursively; many; types item|checkbox_item|radio_group|label|separator|group|sub - one with_<type> setter each, options as keywords; with_item/with_checkbox_item/with_label yield NOTHING to the block - no |param|, write content directly; each with_radio_group REQUIRES with_radio_item inside its block (at least one radio item); each with_group REQUIRES with_item inside its block (at least one item); each with_sub REQUIRES with_trigger inside its block (the sub-menu item); each with_sub REQUIRES with_item inside its block (at least one item)), trigger (The menu button - a poetry Button (options forward to it, e.g. variant: :outline). The slot owns the aria-haspopup/expanded/ controls wiring regardless of the composed content, so composition cannot drop the aria.; takes poetry_button props, not a block; with_trigger yields NOTHING to the block - no |param|, write content directly).
- PART `dropdown-menu` - Root wrapper hosting the menu + popper controllers around the trigger and content
- PART `dropdown-menu-content` - The role=menu popup panel - positioning, animation, and the open state ride here | states: data-open (menu is open (presence flips the pair at runtime)); data-closed (menu is closed or animating out (the server-rendered state)); data-side=top|right|bottom|left (the placement side (popper re-writes it after collision flips)); data-align=start|center|end (the alignment against the trigger (popper re-resolves it)) | vars: --transform-origin (popper's anchor-facing animation origin); --available-width (popper: viewport space left for the panel (post-flip)); --available-height (popper: viewport space left for the panel (post-flip)); --anchor-width (popper: the trigger's measured width); --anchor-height (popper: the trigger's measured height)
- PART `dropdown-menu-group` - role=group semantic grouping between separators
- PART `dropdown-menu-label` - Non-interactive heading for a run of items | states: data-inset (indented to align with checkbox/radio item text (inset: true))
- PART `dropdown-menu-item` - One role=menuitem action row | states: data-variant (default or destructive (the danger treatment)); data-inset (indented to align with checkbox/radio item text (inset: true)); data-disabled (item is disabled (always written together with aria-disabled))
- PART `dropdown-menu-checkbox-item` - A role=menuitemcheckbox toggle row | states: data-checked (checked (the controller re-writes the pair with aria-checked on activation)); data-unchecked (unchecked); data-disabled (item is disabled (always written together with aria-disabled)); data-close-on-select (per-item override of the menu's close-on-select default ("false" keeps the menu open))
- PART `dropdown-menu-radio-group` - role=group scoping one single-select value | states: data-value (the selected radio value (the controller re-writes it on change))
- PART `dropdown-menu-radio-item` - A role=menuitemradio row inside a radio group | states: data-checked (the selected radio (the controller re-writes the pair with aria-checked)); data-unchecked (not selected); data-value (the radio's value); data-disabled (item is disabled (always written together with aria-disabled))
- PART `dropdown-menu-checkbox-item-indicator` - The check glyph inside checkbox items (aria-hidden; the item carries the checked state)
- PART `dropdown-menu-radio-item-indicator` - The circle glyph inside radio items (aria-hidden; the item carries the checked state)
- PART `dropdown-menu-separator` - role=separator rule between groups
- PART `dropdown-menu-shortcut` - The trailing keybinding HINT - aria-hidden, never binds the key
- PART `dropdown-menu-sub` - A submenu scope - hosts its own popper around the sub trigger/content pair
- PART `dropdown-menu-sub-trigger` - The role=menuitem row opening its submenu | states: data-popup-open (its submenu is open (written with aria-expanded; absence is the closed state)); data-inset (indented to align with checkbox/radio item text (inset: true))
- PART `dropdown-menu-sub-content` - The nested role=menu panel - its own popper content on the same presence machinery | states: data-open (submenu is open (presence flips the pair at runtime)); data-closed (submenu is closed (the server-rendered state)); data-side=top|right|bottom|left (the placement side (right/left by direction; popper resolves it at runtime)); data-align=start|center|end (the alignment against the sub-trigger (popper resolves it at runtime)) | vars: --transform-origin (popper's anchor-facing animation origin); --available-width (popper: viewport space left for the panel (post-flip)); --available-height (popper: viewport space left for the panel (post-flip)); --anchor-width (popper: the sub-trigger's measured width); --anchor-height (popper: the sub-trigger's measured height)
- WIRING root: `poetry--core--menu` registers; values open, modal, loop | `poetry--core--popper` registers; values side, align, side_offset, align_offset, avoid_collisions
- WIRING trigger: `poetry--core--menu` actions toggle on click, triggerKeydown on keydown | `poetry--core--popper` targets anchor
- WIRING content: `poetry--core--popper` targets content
- WIRING item: `poetry--core--menu` actions activate on click
- WIRING sub_trigger: `poetry--core--menu` actions subEnter on pointerenter, subLeave on pointerleave, openSub on click | `poetry--core--popper` targets anchor
- RULE: with_trigger(compose: true) { |wiring| ... } composes YOUR control as the trigger: the block is yielded the trigger wiring (the Stimulus behavior the overlay needs; poppers add id/aria and their trigger slot, modals hand only the open action) - splat it onto a wiring-free control (poetry_sidebar_menu_button, a plain tag); without compose: the classic composed Button renders.
- RULE: Use poetry_dropdown_menu - never hand-roll role=menu popups with Tailwind.
- RULE: Items are ACTIONS. Choosing a form VALUE is a Select/Combobox - do not fake it with radio items.
- RULE: Navigation items pass with_item(href:) (external: for a new tab); a form action (sign-out, a DELETE) passes with_item(submit:, method:). The item renders AS the anchor / submit button (role=menuitem on the <a> or <button>) - one interactive element - so NEVER nest a link_to or button_to inside an item.
- RULE: Icon-only triggers MUST have an accessible name (the composed Button's label: rule).
- RULE: Never write the state attributes (data-popup-open / data-checked / data-unchecked) without their aria twin (aria-expanded / aria-checked) - the controller writes both; agents patching DOM must too.
- RULE: Destructive items use variant: :destructive AND still confirm irreversible actions via a dialog.
- RULE: shortcut: is a visual hint only - it does NOT bind the key; wire a real hotkey separately or omit it.
- RULE: Do not nest interactive elements inside items (a menuitem IS the interactive unit).
- RULE: Keep submenus <= 2 levels; prefer grouping + separators over deep nesting.
- RULE: Critical actions must exist somewhere reachable without JS (menus are JS-required interaction).
