# Dialog

> Modal dialogs via a service that wraps @angular/cdk/dialog. Open any component as a dialog and read its close result as a Promise.

Source: https://ngwr.dev/reference/components/dialog  
Kind: Service, Directives, CDK Dialog

## Installation

```angular-ts
// The component that OPENS a dialog injects the service.
import { WrDialog } from 'ngwr/dialog';

@Component({...})
export class MyComponent {
  private readonly dialog = inject(WrDialog);
}

// The component OPENED as a dialog imports the layout directives it uses.
// Selector -> class: [wrDialogTitle] -> WrDialogTitle, and so on.
import { WrDialogClose, WrDialogContent, WrDialogFooter, WrDialogTitle } from 'ngwr/dialog';

@Component({
  imports: [WrDialogTitle, WrDialogContent, WrDialogFooter, WrDialogClose],
  templateUrl: './confirm.html',
})
export class ConfirmComponent {}
```

## Open a dialog

```angular-ts
const ref = dialog.open(ConfirmComponent, {
  data: { title: 'Delete', message: 'Are you sure?' },
  width: '24rem',
});

const ok = await ref.awaitClose(); // result from <wr-btn wrDialogClose value>
```

```angular-html
// Inside the opened component. Every attribute below is a directive:
// imports: [WrDialogTitle, WrDialogContent, WrDialogFooter, WrDialogClose]
<h2 wrDialogTitle>Delete</h2>
<div wrDialogContent>Are you sure?</div>
<div wrDialogFooter>
  <wr-btn wrDialogClose>Cancel</wr-btn>
  <wr-btn color="danger" [wrDialogClose]="true">Delete</wr-btn>
</div>
```

```html
dialog.open(ConfirmComponent, { data, width: '24rem' })
```

## Closing from the content

`[wrDialogClose]` covers close-on-click. When the dialog has to dismiss itself — after a save resolves, or when a store signal flips — inject its `WrDialogRef` and call `close(result)`.

```angular-ts
// Inside the opened component — close without a click.
import { WR_DIALOG_DATA, WrDialogRef } from 'ngwr/dialog';

@Component({...})
export class EditUserComponent {
  private readonly ref = inject(WrDialogRef);
  protected readonly data = inject<EditUserData>(WR_DIALOG_DATA);

  save(): void {
    this.store.saveUser(this.form.value);
    this.ref.close(true);            // the caller's awaitClose() resolves
  }
}

// The class token already types the generics — no cast, no WR_DIALOG_REF needed:
private readonly ref = inject<WrDialogRef<EditUserComponent, boolean>>(WR_DIALOG_REF);
```

## Lifetime — the dialog outlives its opener

`WrDialog` is root-provided, so a dialog is not tied to the component that opened it: destroy that component and the panel, its backdrop and the `cdk-global-scrollblock` stay on the page. Navigation is the one case handled for you — `closeOnNavigation` is on by default. Everything else (an `@if` that removes the opener, a row dropped from a list) is the caller's job.

```angular-ts
// A dialog outlives the component that opened it. `WrDialog` is root-provided
// and the panel's injector hangs off the root environment injector, so an
// `@if` that removes the opener leaves the panel, its backdrop and the
// `cdk-global-scrollblock` on the page. (Navigation is the one exception:
// `closeOnNavigation` is on by default.) Close the ref when you go away.
import { DestroyRef, inject } from '@angular/core';

@Component({...})
export class MyComponent {
  private readonly dialog = inject(WrDialog);
  private readonly destroyRef = inject(DestroyRef);

  async confirmDelete(): Promise<void> {
    const ref = this.dialog.open<ConfirmComponent, boolean>(ConfirmComponent);
    const stop = this.destroyRef.onDestroy(() => ref.close());

    // awaitClose() is a Promise, so takeUntilDestroyed() does not apply to it —
    // that operator takes an Observable. Two ways to not act on a stale result:
    const ok = await ref.awaitClose();
    stop();                                  // nothing left to cancel
    if (ok) this.store.remove();
  }
}

// Or subscribe instead of awaiting, and takeUntilDestroyed() does apply —
// `ref.closed` is an Observable, and the ref is the same one either way.
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

ref.closed.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(ok => { … });
```

## The close button

Dialogs render a dismiss (×) in the top-right corner by default, on top of closing via backdrop click, Escape, and any element carrying `[wrDialogClose]`. Opt out per dialog with `closable: false`. `<wr-drawer>` and `WrDrawerManager` take the same `closable` / `closeLabel` pair.

```typescript
// The × comes for free — nothing to add. It sits in the panel's top-right
// corner, is labelled from the `dialog.close` i18n key, and the title row
// reserves the gutter so a long heading wraps instead of running under it.
dialog.open(EditUserComponent);

// Turn it off when the content already owns its dismiss, or when the dialog
// must be resolved through its own actions:
dialog.open(EditUserComponent, { closable: false });

// Override just the accessible name:
dialog.open(EditUserComponent, { closeLabel: 'Discard changes' });
```

## Where focus goes, and where it comes back

On open, focus moves into the panel: to whatever carries `cdkFocusInitial` if anything does, and otherwise to the first tabbable element in DOM order. The built-in ✕ is appended after your content, so it is the LAST tabbable element rather than the first — a dialog whose content starts with a text field opens with the caret in that field. A panel with nothing tabbable in it takes no focus at all; Escape still closes it, because the key is handled by the overlay rather than by whatever is focused.

```angular-html
<!-- Inside the opened component. Nothing to import: the focus trap looks
     for the attribute by name, so a bare `cdkFocusInitial` is enough. -->
<h2 wrDialogTitle>Rename project</h2>
<div wrDialogContent>
  <wr-form-field label="Name">
    <input wrInput cdkFocusInitial [(value)]="name" />
  </wr-form-field>
</div>
<div wrDialogFooter>
  <wr-btn wrDialogClose>Cancel</wr-btn>
  <wr-btn color="primary" [wrDialogClose]="name()">Save</wr-btn>
</div>

<!-- Without `cdkFocusInitial` the first tabbable element wins — here the same
     input, since the ✕ is appended AFTER your content and comes last. A panel
     of plain text focuses nothing; Escape still closes it. -->
```

On close, focus returns to the element that was active when `open()` was called — captured then, restored after the panel is disposed. All four dismissal paths go through the same `WrDialogRef.close()`, so Escape, the ✕, a `[wrDialogClose]` button and a backdrop click all restore it. The one case it cannot cover is a trigger that no longer exists: if the button that opened the dialog has been removed from the DOM meanwhile, focus lands on `<body>` and the next Tab starts from the top of the document. Move focus yourself after `awaitClose()` when the dialog's own result removes its trigger.

Focus is trapped inside the panel while it is open, and the panel announces itself with `role="dialog"` plus `aria-modal="true"`. The rest of the page is _not_ marked `inert` and carries no `aria-hidden` — the modality is the focus trap and the `aria-modal` claim, which is what screen readers act on, and the backdrop, which takes every pointer event aimed at the page. What is not covered is everything that ignores both — find-in-page, and a reading mode that walks the DOM rather than the accessibility tree. If your app needs the harder guarantee, set `inert` on your own root element for as long as the dialog is open; the library does not, because choosing which subtree to freeze is not something it can know.

## Overlays are in the top layer, so z-index cannot reach them

CDK 22 promotes every overlay by calling `showPopover()` on its host, which puts the panel and its backdrop in the browser's top layer. The top layer is ordered by the moment of promotion and sits above the whole page — no `z-index`, stacking context or DOM order in your application can put anything over it. A sticky header at `z-index: 100000` still renders under a dialog backdrop, and a click where it appears goes to the backdrop and closes the dialog.

```typescript
// The overlay container is isolated, and that is what `provideWrOverlay()`
// promises — not a place in your z-index scale. These do nothing:
//
//   .wr-overlay-container .cdk-overlay-pane { z-index: 1100; }
//   header { z-index: 100000; }
//
// To opt the whole application out of the top layer and back into ordinary
// stacking, configure the CDK itself at bootstrap:
import { OVERLAY_DEFAULT_CONFIG } from '@angular/cdk/overlay';

bootstrapApplication(AppComponent, {
  providers: [
    { provide: OVERLAY_DEFAULT_CONFIG, useValue: { usePopover: false } },
  ],
});

// Opting out is a real trade: overlays go back to being clipped by an
// ancestor's `overflow` and to competing on z-index with everything else.
```

Two consequences worth knowing before you spend an afternoon on CSS. Chrome that must stay above a dialog — a cookie bar, a support widget — has to be in the top layer itself (its own `popover` or native `<dialog>`), or you turn the mechanism off as shown above and go back to ordinary stacking, at the cost of every overlay becoming clippable by an ancestor again.

And the mirror image: an ngwr overlay opened from inside a native `<dialog open>` shown with `showModal()` is drawn correctly and is unreachable by the mouse. The native dialog makes the rest of the document inert, and hit-testing stops at it — the panel is visible above it, the keyboard still works, and every click passes through to the native dialog underneath. Nothing throws. Use `WrDialog` rather than nesting inside a native modal.

## A select or popconfirm opened inside a dialog

Panels anchored to a trigger — select, dropdown, popover, popconfirm, date-picker — carry no backdrop of their own; they close on the first click that lands outside them. Inside a dialog that click also lands on the dialog's backdrop, so one click away from an open select closes the select AND the dialog. Escape is different: it goes to the topmost overlay only, so it closes the panel and leaves the dialog up.

```typescript
// A dialog that owns a form: keep the backdrop, ignore its clicks.
const ref = this.dialog.open(EditUserComponent, {
  closeOnBackdropClick: false,   // a click beside an open select cannot lose the form
  // closeOnEscape stays true — Escape closes the select first, the dialog next.
});
```

That is why a dialog holding a form is usually opened with `closeOnBackdropClick: false` — losing unsaved input to a mis-click beside a dropdown is the same accident either way, and the ✕ and Escape both remain.

## Responsive (bottom-sheet)

With `responsive: true` — or app-wide via `provideWrResponsiveOverlays()` — the dialog slides up as a full-width bottom-sheet on small viewports and stays a centred modal on larger ones. Open this on a phone (or narrow the window) to see it dock to the bottom.

```angular-ts
// Per dialog — slides up as a bottom-sheet on small screens.
dialog.open(ConfirmComponent, { responsive: true });

// Or app-wide, for every overlay:
provideWrResponsiveOverlays();          // default breakpoint 640px
provideWrResponsiveOverlays({ breakpoint: 768 });
```

```html
dialog.open(ConfirmComponent, { responsive: true })
```

## Service API

| Name | Description | Type | Default |
| --- | --- | --- | --- |
| `open(component, options?)` | Opens a dialog. Returns a WrDialogRef. | `(component, WrDialogOptions) => WrDialogRef` | `—` |

## WrDialogRef

What `open()` hands back — and what the content gets by injecting `WrDialogRef`. One thing it cannot tell you: a dismissal and a `close(undefined)` are the same event. `closed` emits `undefined` for the ✕, Escape, the backdrop, a navigation and a bare `[wrDialogClose]` alike, so a dialog that has to distinguish “cancelled” from “saved nothing” must close with a value of its own.

| Name | Description | Type | Default |
| --- | --- | --- | --- |
| `WrDialogRef<C, R>` | Returned by `open()`, and provided inside the dialog’s own injector. `C` is the opened component, `R` the close result. | `class` | — |
| `close(result?)` | Dismiss the dialog, optionally with a result. Idempotent — a second call is a no-op, so a save handler racing the ✕ cannot emit twice. | `(result?: R) => void` | — |
| `awaitClose()` | Resolves with the close result once the dialog is dismissed. A Promise, so `takeUntilDestroyed()` does not apply to it — see “Lifetime” above. | `() => Promise<R \| undefined>` | — |
| `closed` | The same result as an Observable — emits once, then completes. A `ReplaySubject`, so subscribing after the dialog has already closed still gets the value rather than a bare completion. | `ReplaySubject<R \| undefined>` | — |
| `componentInstance` | The instantiated dialog component, for reading a signal on it or calling one of its methods. Throws while the dialog is still attaching — which is only reachable from the content’s own constructor. | `C` | — |
| `overlayRef` | The underlying CDK `OverlayRef` — an escape hatch for the cases the options do not cover. Do not dispose it directly: that bypasses `closed`, leaves `awaitClose()` pending and never destroys the focus trap. Call `close()`. | `OverlayRef` | — |

```typescript
const ref = this.dialog.open<ConfirmComponent, 'saved' | 'discarded'>(ConfirmComponent);

// `undefined` is every dismissal: ✕, Escape, backdrop, navigation, and a bare
// [wrDialogClose]. Give the outcomes you care about their own values.
const result = await ref.awaitClose();   // 'saved' | 'discarded' | undefined
if (result === undefined) return;        // dismissed — leave the page as it was

// Or subscribe, when the caller is not an async method:
ref.closed.subscribe(result => { … });   // emits once, then completes
```

## WrDialogOptions

| Name | Description | Type | Default |
| --- | --- | --- | --- |
| `WrDialogOptions` | Second argument of `open()`. Every field is optional. | `interface` | `—` |
| `data` | Payload exposed to the content via WR_DIALOG_DATA. | `D` | `—` |
| `width` | Panel width — any CSS length. | `string` | `—` |
| `maxWidth` | Panel maximum width. | `string` | `—` |
| `closeOnBackdropClick` | Close when the backdrop is clicked. | `boolean` | `true` |
| `closeOnEscape` | Close on Escape. | `boolean` | `true` |
| `closeOnNavigation` | Close as soon as the URL changes — Back and `router.navigate()` alike. Turn it off only for a dialog that owns the navigation. | `boolean` | `true` |
| `closable` | Show the built-in dismiss (×) in the top-right corner. | `boolean` | `true` |
| `closeLabel` | Accessible name for the dismiss button. Falls back to the dialog.close catalog key. | `string` | `—` |
| `responsive` | Present as a bottom-sheet on small viewports. Undefined follows provideWrResponsiveOverlays(). | `boolean` | `—` |
| `panelClass` | Extra class(es) on the panel. | `string \| readonly string[]` | `—` |

## Layout directives

| Name | Description | Type | Default |
| --- | --- | --- | --- |
| `[wrDialogTitle]` | Styles the title row, and supplies the panel’s `aria-labelledby`. Import `WrDialogTitle`. | `directive` | `—` |
| `[wrDialogContent]` | Styles the scrollable body. Import `WrDialogContent`. | `directive` | `—` |
| `[wrDialogFooter]` | Styles the footer. Import `WrDialogFooter`. | `directive` | `—` |
| `align` | Footer alignment. | `'start' \| 'center' \| 'end'` | `'end'` |
| `[wrDialogClose]` | Closes the dialog when clicked. Import `WrDialogClose`. | `directive` | `—` |
| `wrDialogClose` | Value passed to `close()`. Bare attribute closes with `undefined`. | `R \| undefined` | `undefined` |

## Available inside the dialog

| Name | Description | Type | Default |
| --- | --- | --- | --- |
| `WR_DIALOG_DATA` | The data payload passed to open(). undefined when you didn't pass any. | `InjectionToken<D>` | `—` |
| `WrDialogRef` | The open dialog’s own ref — call close(result) to dismiss it from the content. | `WrDialogRef<unknown, unknown>` | `—` |
| `WR_DIALOG_REF` | The same ref under a second key, used by `[wrDialogClose]`. Prefer `inject(WrDialogRef)` — it already supports `{ optional: true }` and typed generics. | `InjectionToken<WrDialogRef<C, R>>` | `—` |

## CSS variables

Custom properties `ngwr/dialog` 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-dialog-bg` | `var(--wr-color-surface)` | `.wr-dialog-panel` |
| `--wr-dialog-min-width` | `18rem` | `.wr-dialog-panel` |
| `--wr-dialog-padding-x` | `1.25rem` | `.wr-dialog-panel` |
| `--wr-dialog-padding-y` | `1rem` | `.wr-dialog-panel` |
| `--wr-dialog-radius` | `var(--wr-border-radius-lg)` | `.wr-dialog-panel` |
| `--wr-dialog-shadow` | `var(--wr-shadow-modal)` | `.wr-dialog-panel` |
