bug(overlay): backdrop click and `open` input stop closing cdkConnectedOverlay once the host view is detached (only Escape still works)
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 4/5
- Thời gian dự kiến
- 3-5 ngày
- Mức phù hợp với người mới
- 48/100
- Loại issue
- Lỗi
- Độ rõ ràng
- Khá rõ ràng
- Mức độ hoạt động
- Ít trao đổi
- Công nghệ
- angular, typescript
- Lĩnh vực
- frontend
Hướng nghiên cứu
Bắt đầu với bản tái hiện StackBlitz được liên kết, tập trung vào luồng RouteReuseStrategy và ViewContainerRef.detach(). Sau đó kiểm tra CdkConnectedOverlay trong @angular/cdk, bao gồm hành vi của _overlay-module-chunk.mjs được mô tả trong báo cáo; hoàn thành khi các lần nhấp vào backdrop và input open đóng overlay sau khi host view được detach, như Escape đã làm.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
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 tofalse(through any means — a signal, anasyncpipe, an explicitChangeDetectorRef.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:
// 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 <body>, 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:
// 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):
- Give
CdkConnectedOverlay/OverlayRefa way to close in response to the host view being detached from change detection, mirroring the existingngOnDestroyhandling — e.g. reacting toViewRefdetachment, not onlyngOnChanges. - At minimum, make backdrop click behave like Escape: call
detachOverlay()directly on theOverlayRefinstance when a backdrop click is detected, rather than only emitting an event and relying on the host'sopeninput to round-trip back throughngOnChanges. - 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
Steps to reproduce:
- Open the StackBlitz above. It's a routed app with two routes,
/aand/b, using aRouteReuseStrategywhoseshouldDetach()always returnstrue(i.e.ViewContainerRef.detach()is called on navigation instead of destroying the component — a supported, documented use ofRouteReuseStrategy, commonly used to preserve scroll position, form state, or tab state across navigations). - 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. - During that 5-second window, while still on Page A, click "Open overlay". A panel with a backdrop appears.
- 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.
- Try clicking the backdrop, or the "Close" button inside the overlay panel.
- Observe: nothing happens — the backdrop and panel are still visible on top of Page B and still block interaction with it.
- 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
- Ngôn ngữ chính
- TypeScript
- Star
- 25k
- Fork
- 6.8k
- Merge trung bình
- 1 ngày 1 giờ
- Pull request đã merge (30 ngày)
- 84
Hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của angular/components
-
area: material/tree docs gemini-triaged needs triage
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 68/100
angular/components#33832 ·
-
area: material/datepicker gemini-triaged P4
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
angular/components#33763 · 2 bình luận · 3 reaction ·
-
area: material/table gemini-triaged P4
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 88/100
angular/components#33709 · 1 bình luận ·
-
area: material/table docs gemini-triaged P4
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
angular/components#33455 ·
-
area: material/core gemini-triaged P3
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 78/100
angular/components#33059 · 2 bình luận ·
Tất cả issue của angular/components
Issue tương tự
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 70/100
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 75/100
mksglu/context-mode#1200 ·
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 75/100
jaegertracing/jaeger-ui#4506 ·
-
area:desktop area:ui bug platform:macos
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 75/100
anthropics/claude-code#96687 ·
-
good first issue
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 95/100
AOSSIE-Org/DebateAI#582 · 2 bình luận ·