DirectiveComponentsCDK Overlay

Dropdown

Attach a floating menu to any trigger via the wrDropdown directive. Built on CDK Overlay — handles outside clicks, Escape, and edge collisions.

Installation

import { WrDropdown, WrDropdownMenu, WrDropdownItem } from 'ngwr/dropdown';

@Component({ imports: [WrDropdown, WrDropdownMenu, WrDropdownItem] })
export class MyComponent {}

Basic usage

<button wr-btn [wrDropdown]="menu">Actions</button>

<wr-dropdown-menu #menu>
  <wr-dropdown-item icon="copy-outline">Copy</wr-dropdown-item>
  <wr-dropdown-item icon="download">Download</wr-dropdown-item>
  <wr-dropdown-item icon="trash">Delete</wr-dropdown-item>
</wr-dropdown-menu>

Responsive (bottom-sheet)

With responsive — or app-wide via provideWrResponsiveOverlays() — the menu detaches from the trigger and slides up as a full-width bottom-sheet with a backdrop on small viewports. Open this on a phone (or narrow the window below 640px).

<button wr-btn [wrDropdown]="menu" responsive>Actions</button>

Positions

Eight anchor sides; CDK auto-flips when there's not enough room.

<button wr-btn [wrDropdown]="menu" position="top-start">Top start</button>

Hover trigger

Opens on mouseenter; closes when the pointer leaves both trigger and menu.

<button wr-btn [wrDropdown]="menu" trigger="hover">Hover me</button>

Disabled item

<wr-dropdown-item icon="cog" disabled>Disabled item</wr-dropdown-item>

Template reference

The trigger exports itself as wrDropdown (the panel keeps wrDropdownMenu), so a template can open, close or read the menu from elsewhere.

<wr-btn [wrDropdown]="menu" #actions="wrDropdown">Actions</wr-btn>
<wr-dropdown-menu #menu>
  <wr-dropdown-item>Copy</wr-dropdown-item>
</wr-dropdown-menu>

<!-- Anywhere else in the same template -->
<wr-btn (click)="actions.toggle()">Toggle from here</wr-btn>
@if (actions.isOpen()) {
  <span>The menu is up.</span>
}

The trigger keeps its own id

The menu names itself after its trigger (aria-labelledby), so the trigger needs an id. Yours wins — static, bound or interpolated alike — and <label for>, document.getElementById, an aria-labelledby aimed at it from elsewhere and any test id keep working. Only an element with no id of its own gets the generated wr-dropdown-trigger-N fallback.

<!-- Your id stays on the element; the menu points at it. -->
<button wr-btn id="row-actions" [wrDropdown]="menu">Actions</button>
<!-- => <button id="row-actions" aria-haspopup="menu" …>
     <div role="menu" aria-labelledby="row-actions"> -->

<!-- Bound and interpolated forms are honoured too. -->
<button wr-btn [id]="'actions-' + row.id" [wrDropdown]="menu">Actions</button>

<!-- No id of your own: the fallback is generated, and is not a stable
     locator — it counts dropdown instances, so it differs between the
     prerendered page and the hydrated one.
     => <button id="wr-dropdown-trigger-3" …> -->
<button wr-btn [wrDropdown]="menu">Actions</button>

Do not write a test locator against the generated id. It is a per-application counter, so it depends on how many dropdowns were constructed before this one — which differs between a prerendered page and the same page after hydration, and between one route and another. Give the trigger an id when you need to address it.

Scrolling containers

The menu is positioned against the trigger and repositions on scroll — but only for scroll containers the CDK knows about, plus the window. A panel anchored inside your own overflow: auto box stays where it was opened and drifts away from its trigger, because nothing told the CDK that box scrolls. The fix is one directive on the container, and it is the same rule for every anchored ngwr overlay: dropdown, popover, popconfirm, select, cascader, tree, the date pickers, and mention, tour, the colour-picker trigger and a context-menu submenu.

import { CdkScrollable } from '@angular/cdk/scrolling';

@Component({
  imports: [CdkScrollable, WrDropdown, WrDropdownMenu, WrDropdownItem],
  template: `
    <!-- Without `cdkScrollable` the menu is placed once and stays there while
         this box scrolls — it ends up floating over, or away from, its own
         trigger. The window itself is always tracked; a nested scroller is not
         until it registers. -->
    <div class="side-panel" cdkScrollable>
      <button wr-btn [wrDropdown]="menu">Actions</button>
    </div>
  `,
})
export class PanelComponent {}

Directive API

NameDescriptionTypeDefault
wrDropdownrequiredMenu to open. Pass a <wr-dropdown-menu> template reference.WrDropdownMenu
triggerHow the menu opens.WrDropdownTrigger'click'
positionWhere the menu anchors relative to the trigger.WrDropdownPosition'bottom-start'
responsivePresent the menu as a full-width bottom-sheet on small viewports instead of an anchored panel. undefined follows the app-wide provideWrResponsiveOverlays() setting; true/false overrides it.boolean | undefinedundefined
(opened)Fires after the menu opens.void
(closed)Fires after the menu closes.void

Item API

NameDescriptionTypeDefault
iconOptional leading icon name.WrIconName | nullnull
disabledDisable interaction (suppresses pointer + keyboard).booleanfalse

CSS variables

Custom properties ngwr/dropdown publishes. Each default below is declared on the component's own selector, so a :root override is shadowed by it — set them on that selector, on a wrapper you scope yourself, or inline on the element. Unlike the BEM class names, these are the supported way to restyle the component.

VariableDefaultDeclared on
--wr-dropdown-bgvar(--wr-color-surface).wr-dropdown-menu
--wr-dropdown-bordervar(--wr-color-outline).wr-dropdown-menu
--wr-dropdown-item-bg-hovervar(--wr-color-hover).wr-dropdown-item
--wr-dropdown-item-colorvar(--wr-color-on-surface).wr-dropdown-item
--wr-dropdown-item-padding-x0.625rem.wr-dropdown-item
--wr-dropdown-item-padding-y0.375rem.wr-dropdown-item
--wr-dropdown-item-radiusvar(--wr-border-radius-sm).wr-dropdown-item
--wr-dropdown-min-width10rem.wr-dropdown-menu
--wr-dropdown-padding0.25rem.wr-dropdown-menu
--wr-dropdown-radiusvar(--wr-border-radius-base).wr-dropdown-menu
--wr-dropdown-shadowvar(--wr-shadow-overlay).wr-dropdown-menu