Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 135 additions & 8 deletions docs/components/buttons-menus/dropdowns.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,23 +42,150 @@ Interaction states are identical to the [normal buttons](buttons.md).
If the overlay should show a contextual menu, please use the [menu component](menu.md).
It already implements specific keyboard interactions and aria roles that are needed for a _dropdown menu_.

This chapter describes how to create an interactive contextual overlay using the [CDK Overlay module](https://material.angular.io/cdk/overlay/overview) and the [CDK Focus trap](https://material.angular.io/cdk/a11y/overview#focustrap)
without special keyboard handling and aria-roles.
Use a [button](buttons.md) with the `dropdown-toggle` class as the trigger and place the content in a
`dropdown-menu`. Add a `dropdown-caret` icon to indicate that the button opens an overlay.

The example uses the markup of [Bootstrap dropdown](https://getbootstrap.com/docs/5.1/components/dropdowns/).
It is also possible to use the CDK directives without using markup related to the `dropdown-menu`.
The theme provides the dropdown styles. Use the [CDK Overlay module](https://material.angular.io/cdk/overlay/overview)
to create, position, and dismiss the overlay. The CDK directives can also be used with content that does not use
the `dropdown-menu` class.

<si-docs-component example="dropdown/dropdown-with-overlay" height="200"></si-docs-component>

The overlay is toggled by the `open` property which needs to be updated properly.
We added a transparent backdrop to the overlay so that we can listen for `(backdropClick)` which should close the overlay.
The listener for `(detach)` is necessary for updating the `open` property after escape was pressed.
In a real world scenario, one has to be careful here, because `(detach)` is always called when the overlay is closed.
### Basic markup

Use a button for actions and an anchor for navigation. Set `aria-expanded` to the current open state and add
`show` to the trigger while the overlay is open so that the caret rotates.

```html
<button
#trigger="cdkOverlayOrigin"
type="button"
class="btn btn-primary dropdown-toggle"
cdkOverlayOrigin
[class.show]="open()"
[attr.aria-expanded]="open()"
(click)="open.set(true)"
>
Actions
<si-icon class="dropdown-caret icon" icon="element-down-2" />
</button>

<ng-template
cdkConnectedOverlay
cdkConnectedOverlayBackdropClass="cdk-overlay-transparent-backdrop"
[cdkConnectedOverlayOpen]="open()"
[cdkConnectedOverlayOrigin]="trigger"
[cdkConnectedOverlayHasBackdrop]="true"
(backdropClick)="open.set(false)"
(detach)="open.set(false)"
>
<div class="dropdown-menu position-static d-block">
<button type="button" class="dropdown-item">Edit</button>
<button type="button" class="dropdown-item">Duplicate</button>
<div class="dropdown-divider"></div>
<button type="button" class="dropdown-item">Delete</button>
</div>
</ng-template>
```

The `open` state controls the overlay. Use a transparent backdrop to close it when the user clicks outside.
Also update the state on `(detach)`, which is emitted when the overlay closes, including after pressing Escape.
The `position-static` and `d-block` utility classes allow the CDK to position the rendered menu.

For having a proper keyboard interaction, we are using the [CDK Focus trap](https://material.angular.io/cdk/a11y/overview#focustrap).
It ensures that the focus will remain within the overlay while it is open.
Enabling `[cdkTrapFocusAutoCapture]="true"` ensures, that the focus will be moved into the overlay on creation and returned to the trigger when the overlay is closed.

### Directions

Wrap the trigger and menu in one of the direction classes to place a statically positioned menu relative to its
trigger. The caret direction changes automatically.

| Class | Menu placement |
| ----------- | ------------------ |
| `dropdown` | Below the trigger |
| `dropup` | Above the trigger |
| `dropstart` | Before the trigger |
| `dropend` | After the trigger |

```html
<div class="dropend">
<button type="button" class="btn btn-secondary dropdown-toggle">
More actions
<si-icon class="dropdown-caret icon" icon="element-down-2" />
</button>
<div class="dropdown-menu show">
<button type="button" class="dropdown-item">Action</button>
</div>
</div>
```

When using the CDK Overlay, configure the connected overlay positions instead. The direction classes are useful
for menus that remain in the document flow or when the position is managed by the application.

### Alignment

Use an alignment class on the `dropdown-menu` to align it with its trigger. `align-start` is the default placement.

| Class | Alignment |
| -------------- | ------------------------------------------ |
| `align-start` | Align the menu with the trigger start edge |
| `align-center` | Center the menu on the trigger start edge |
| `align-end` | Align the menu with the trigger end edge |

```html
<div class="dropdown">
<button type="button" class="btn btn-secondary dropdown-toggle">Actions</button>
<div class="dropdown-menu show align-end">
<button type="button" class="dropdown-item">Action</button>
</div>
</div>
```

### Menu content

Use `dropdown-item` for interactive actions. It supports the `active` and `disabled` states. Use
`dropdown-item-text` for non-interactive content and `dropdown-header` to label a group of items.
Use `dropdown-divider` to separate related groups.

```html
<div class="dropdown-menu show">
<div class="dropdown-header">Export</div>
<button type="button" class="dropdown-item active">PDF</button>
<button type="button" class="dropdown-item">Excel</button>
<button type="button" class="dropdown-item" disabled>Word</button>
<div class="dropdown-divider"></div>
<div class="dropdown-item-text">Exports include the current filters.</div>
</div>
```

For items with icons, place the icon before an `item-title`. A trailing `menu-end-icon` can communicate an
additional state or a follow-up action.

```html
<button type="button" class="dropdown-item">
<si-icon class="icon" icon="element-download" />
<span class="item-title">Download</span>
<si-icon class="menu-end-icon icon" icon="element-right-2" />
</button>
```

### Long menus

`dropdown-menu` scrolls when its height exceeds the viewport. Add `dropdown-menu-scroller` to apply the
recommended bounded height for menus with many items.

```html
<div class="dropdown-menu dropdown-menu-scroller show">
<!-- Dropdown items -->
</div>
```

### Related components

Use [button groups](button-group.md) to create split-button patterns. Use the [menu component](menu.md) when the
content, see [content actions](content-actions.md).

<si-docs-api directive="CdkConnectedOverlay"></si-docs-api>

<si-docs-api directive="CdkOverlayOrigin"></si-docs-api>
Expand Down
32 changes: 16 additions & 16 deletions src/app/examples/dropdown/dropdown-with-overlay.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,36 @@
type="button"
class="btn btn-primary dropdown-toggle"
cdkOverlayOrigin
[class.show]="open"
[attr.aria-expanded]="open"
(click)="open = true"
[class.show]="open()"
[attr.aria-expanded]="open()"
(click)="open.set(true)"
>
Overlay trigger / origin
<i aria-hidden="true" class="dropdown-caret icon element-down-2"></i>
<si-icon class="dropdown-caret icon" [icon]="icons.elementDown2" />
</button>

<ng-template
cdkConnectedOverlay
cdkConnectedOverlayBackdropClass="cdk-overlay-transparent-backdrop"
[cdkConnectedOverlayOpen]="open"
[cdkConnectedOverlayOpen]="open()"
[cdkConnectedOverlayOrigin]="trigger"
[cdkConnectedOverlayHasBackdrop]="true"
(backdropClick)="open = false"
(detach)="open = false"
(backdropClick)="open.set(false)"
(detach)="open.set(false)"
>
<div
class="dropdown-menu position-static d-block"
[cdkTrapFocus]="true"
[cdkTrapFocusAutoCapture]="true"
>
<button class="dropdown-item focus-inside" type="button" (click)="logEvent('Action')"
>Action</button
>
<button class="dropdown-item focus-inside" type="button" (click)="logEvent('Action 2')"
>Action 2</button
>
<button class="dropdown-item focus-inside" type="button" (click)="logEvent('Action 3')"
>Action 3</button
>
<button class="dropdown-item focus-inside" type="button" (click)="logEvent('Action')">
Action
</button>
<button class="dropdown-item focus-inside" type="button" (click)="logEvent('Action 2')">
Action 2
</button>
<button class="dropdown-item focus-inside" type="button" (click)="logEvent('Action 3')">
Action 3
</button>
</div>
</ng-template>
12 changes: 7 additions & 5 deletions src/app/examples/dropdown/dropdown-with-overlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,19 @@
*/
import { A11yModule } from '@angular/cdk/a11y';
import { CdkConnectedOverlay, CdkOverlayOrigin } from '@angular/cdk/overlay';
import { Component, inject } from '@angular/core';
import { Component, inject, signal } from '@angular/core';
import { elementDown2 } from '@siemens/element-icons';
import { addIcons, SiIconComponent } from '@siemens/element-ng/icon';
import { LOG_EVENT } from '@siemens/live-preview';

@Component({
selector: 'app-sample',
imports: [CdkOverlayOrigin, CdkConnectedOverlay, A11yModule],
imports: [A11yModule, CdkOverlayOrigin, CdkConnectedOverlay, SiIconComponent],
templateUrl: './dropdown-with-overlay.html',
host: { class: 'p-5' }
})
export class SampleComponent {
logEvent = inject(LOG_EVENT);

open = false;
protected readonly logEvent = inject(LOG_EVENT);
protected readonly icons = addIcons({ elementDown2 });
protected readonly open = signal(false);
}
Loading