# Dropdown

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

Source: https://ngwr.dev/reference/components/dropdown  
Kind: Directive, Components, CDK Overlay

## Installation

```angular-ts
import { WrDropdown, WrDropdownMenu, WrDropdownItem } from 'ngwr/dropdown';

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

## Basic usage

```html
<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).

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

## Positions

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

```html
<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.

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

## Disabled item

```html
<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.

```angular-html
<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.

```angular-html
<!-- 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.

```angular-ts
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

| Name | Description | Type | Default |
| --- | --- | --- | --- |
| `wrDropdown`required | Menu to open. Pass a `<wr-dropdown-menu>` template reference. | `WrDropdownMenu` | — |
| `trigger` | How the menu opens. | `WrDropdownTrigger` | `'click'` |
| `position` | Where the menu anchors relative to the trigger. | `WrDropdownPosition` | `'bottom-start'` |
| `responsive` | Present 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 \| undefined` | `undefined` |
| `(opened)` | Fires after the menu opens. | `void` | — |
| `(closed)` | Fires after the menu closes. | `void` | — |

## Item API

| Name | Description | Type | Default |
| --- | --- | --- | --- |
| `icon` | Optional leading icon name. | `WrIconName \| null` | `null` |
| `disabled` | Disable interaction (suppresses pointer + keyboard). | `boolean` | `false` |

## 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.

| Variable | Default | Declared on |
| --- | --- | --- |
| `--wr-dropdown-bg` | `var(--wr-color-surface)` | `.wr-dropdown-menu` |
| `--wr-dropdown-border` | `var(--wr-color-outline)` | `.wr-dropdown-menu` |
| `--wr-dropdown-item-bg-hover` | `var(--wr-color-hover)` | `.wr-dropdown-item` |
| `--wr-dropdown-item-color` | `var(--wr-color-on-surface)` | `.wr-dropdown-item` |
| `--wr-dropdown-item-padding-x` | `0.625rem` | `.wr-dropdown-item` |
| `--wr-dropdown-item-padding-y` | `0.375rem` | `.wr-dropdown-item` |
| `--wr-dropdown-item-radius` | `var(--wr-border-radius-sm)` | `.wr-dropdown-item` |
| `--wr-dropdown-min-width` | `10rem` | `.wr-dropdown-menu` |
| `--wr-dropdown-padding` | `0.25rem` | `.wr-dropdown-menu` |
| `--wr-dropdown-radius` | `var(--wr-border-radius-base)` | `.wr-dropdown-menu` |
| `--wr-dropdown-shadow` | `var(--wr-shadow-overlay)` | `.wr-dropdown-menu` |
