angular / angular/components

bug(overlay): backdrop click and `open` input stop closing cdkConnectedOverlay once the host view is detached (only Escape still works)

オープン
#33,665 コメント 1 件 リアクション 0 件 担当者 0 名 GitHub で見る
area: cdk/overlay gemini-triaged
主要言語
TypeScript
スター
25k
フォーク
6.8k
平均マージ
1日 8時間
マージ済み PR(30日)
91

説明

### Is this a regression?

- [ ] Yes, this behavior used to work in the previous version

### The previous version in which this bug was not present was

_No response_

### Description

When a component that hosts a `cdkConnectedOverlay` is **detached** (not destroyed) from the view tree while its overlay is open — for example via `ViewContainerRef.detach()`, which is exactly what `RouterOutlet.detach()` does when a `RouteReuseStrategy.shouldDetach()` returns `true` — the overlay becomes permanently stuck open and unresponsive to two of the three interactions users expect to close it with:

- Clicking the backdrop does **not** close the overlay.
- Programmatically setting the bound `[cdkConnectedOverlayOpen]` input back to `false` (through any means — a signal, an `async` pipe, an explicit `ChangeDetectorRef.detectChanges()` call) does **not** close the overlay.
- **Only pressing Escape closes the overlay.**

Reading `@angular/cdk` source (`_overlay-module-chunk.mjs`), `CdkConnectedOverlay` only reacts to its `[cdkConnectedOverlayOpen]` input through the `ngOnChanges` lifecycle hook:

```js
// CdkConnectedOverlay
ngOnChanges(changes) {
...
if (changes['open']) {
this.open ? this.attachOverlay() : this.detachOverlay();
}
}
```

`ngOnChanges` only runs when Angular's change detector actually visits the component's view. If that view has been detached from its view container (`ViewContainerRef.detach()`), Angular's CD walk no longer reaches it — so `ngOnChanges` never fires again, no matter how the `open` input is set afterward, including explicit, direct `ChangeDetectorRef.detectChanges()` calls on that specific component instance.

Meanwhile, the overlay's DOM (backdrop + panel) is portaled into a global overlay container appended to ``, entirely outside the detached view's own DOM subtree. So the rendered overlay persists on screen even though the logical component that owns it is no longer part of the CD tree.

By contrast, the Escape key path bypasses the `open` input entirely:

```js
// CdkConnectedOverlay._createOverlay()
overlayRef.keydownEvents().subscribe(event => {
this.overlayKeydown.next(event);
if (event.keyCode === ESCAPE && !this.disableClose && !hasModifierKey(event)) {
event.preventDefault();
this.detachOverlay(); // called directly on the OverlayRef/directive, not through the `open` input
}
});
```

This is a plain RxJS subscription held by the `OverlayRef`/`CdkConnectedOverlay` instance, so it keeps firing and calling `detachOverlay()` imperatively regardless of whether the host view is currently attached to the CD tree. This is why Escape is the only interaction that still works — it is the only one that doesn't route through `ngOnChanges`.

The backdrop-click path (`OverlayOutsideClickDispatcher`) is architecturally similar to Escape — it's a global `document`-level listener, independent of CD — but it only *emits* an `outsidePointerEvents`/`backdropClick` event. It is the consuming component's responsibility to react to that event by flipping its own `open` state, and that round-trip through the `open` input is exactly the mechanism that silently no-ops once the host view is detached. So backdrop click *is* detected, but has no observable effect.

`RouteReuseStrategy` and `ViewContainerRef.detach()` are public, documented Angular Router APIs — not an edge case or misuse. Any component built the idiomatic Angular way — driving overlay visibility through a bound `@Input`/signal rather than imperative `OverlayRef` calls — is exposed to this the moment it's used on a route that gets detached rather than destroyed. This affects `CdkConnectedOverlay` directly, and by extension anything built on top of it that follows the same input-driven-open pattern (custom menus, custom autocompletes, custom popovers, etc.).

The fact that Escape already has a CD-independent close path, while backdrop click and the `open` input do not, suggests this asymmetry is an oversight rather than an intentional design choice.

**Suggested directions (not prescriptive):**

1. Give `CdkConnectedOverlay`/`OverlayRef` a way to close in response to the host view being detached from change detection, mirroring the existing `ngOnDestroy` handling — e.g. reacting to `ViewRef` detachment, not only `ngOnChanges`.
2. At minimum, make backdrop click behave like Escape: call `detachOverlay()` directly on the `OverlayRef` instance when a backdrop click is detected, rather than only emitting an event and relying on the host's `open` input to round-trip back through `ngOnChanges`.
3. Document explicitly that `CdkConnectedOverlay`-based components must not be detached (only destroyed) while their overlay is open, if a code-level fix is not pursued.

### Reproduction

StackBlitz link: [CDK overlay](https://stackblitz.com/edit/stackblitz-starters-75hnhsnq?file=src%2Fapp%2Fapp.component.ts)
Steps to reproduce:

1. Open the StackBlitz above. It's a routed app with two routes, `/a` and `/b`, using a `RouteReuseStrategy` whose `shouldDetach()` always returns `true` (i.e. `ViewContainerRef.detach()` is called on navigation instead of destroying the component — a supported, documented use of `RouteReuseStrategy`, commonly used to preserve scroll position, form state, or tab state across navigations).
2. On Page A, click **"Go to Page B (in 5s)"**. This intentionally delays the actual navigation by 5 seconds (`setTimeout`) — a test aid, not part of the bug — so there's a window to still interact with Page A before it's detached.
3. During that 5-second window, while still on Page A, click **"Open overlay"**. A panel with a backdrop appears.
4. Wait for the 5 seconds to elapse. Navigation to Page B happens and Page A's view is detached (kept alive, not destroyed) with the overlay still open.
5. Try clicking the backdrop, or the **"Close"** button inside the overlay panel.
6. Observe: nothing happens — the backdrop and panel are still visible on top of Page B and still block interaction with it.
7. Press Escape: the overlay closes immediately.

Steps 5–6 are the bug: backdrop click and the in-app "Close" button (both driven by the same `open` input) silently do nothing once Page A's view has been detached, while Escape (step 7) still works.

### Expected Behavior

The overlay should close consistently regardless of which interaction is used, and regardless of whether the host component's view happens to currently be receiving change detection.

### Actual Behavior

Backdrop click and the `open` input binding silently do nothing once the host view is detached from the CD tree. Escape is the only interaction that still closes the overlay.

### Environment

- Angular: 22.0.8
- CDK/Material: 22.0.2
- Browser(s): Chrome (latest)
- Operating System: macOS

コントリビューションガイド

コントリビューションガイドを開く

調査の方向性

リンクされている StackBlitz の再現から始め、RouteReuseStrategy と ViewContainerRef.detach() のフローに焦点を当てます。次に @angular/cdk の CdkConnectedOverlay を調査し、報告で説明されている _overlay-module-chunk.mjs の動作も確認します。ホストビューがデタッチされた後、backdrop のクリックと open input によって overlay が閉じることを確認できれば完了です。Escape ではすでにそうなっています。

索引モデルが issue の本文から書いたものです。

評価

技術スタック
angular, typescript
領域
frontend
issue の種類
バグ
難易度
4/5
見積もり時間
3〜5日
活発さ
静か
明瞭さ
おおむね明確
初心者へのやさしさ
48/100

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。