From 1a3c772c9ff86d8f4bc97f3dcd621253dfc528f0 Mon Sep 17 00:00:00 2001 From: Duygu Ramadan Date: Thu, 5 Mar 2026 16:35:19 +0200 Subject: [PATCH 1/4] fix(ui5-popover): close the popover once it is out of the viewport fixes: #13158 --- packages/main/cypress/specs/Popover.cy.tsx | 86 ++++++++++++++++++++++ packages/main/src/Popover.ts | 45 +++++++++++ packages/main/test/pages/Popover.html | 24 ++++++ 3 files changed, 155 insertions(+) diff --git a/packages/main/cypress/specs/Popover.cy.tsx b/packages/main/cypress/specs/Popover.cy.tsx index 9c197a00f026..e3e4349004e5 100644 --- a/packages/main/cypress/specs/Popover.cy.tsx +++ b/packages/main/cypress/specs/Popover.cy.tsx @@ -7,6 +7,7 @@ import Label from "../../src/Label.js"; import List from "../../src/List.js"; import ListItem from "../../src/ListItemStandard.js"; import Input from "../../src/Input.js"; +import Dialog from "../../src/Dialog.js"; describe("Rendering", () => { it("tests arrow positioning", () => { @@ -1810,3 +1811,88 @@ describe("Responsive paddings", () => { cy.get("[ui5-popover]").should("have.attr", "media-range", "M"); }); }); + +describe("Opener visibility in scrollable containers", () => { + it("should close popover when opener scrolls out of view in scrollable container", () => { + cy.mount( +
+
+ + +
Popover Content
+
+
+
+ ); + + cy.get("[ui5-popover]").should("have.prop", "open", true); + + cy.get("#scrollContainer").scrollTo(0, 200); + + cy.get("[ui5-popover]").should("have.prop", "open", false); + }); + + it("should close popover when opener scrolls out in Dialog scenario", () => { + cy.mount( + +
LargeHeader
+
+
+ + +
Popover in Dialog
+
+
+
+
+ ); + + cy.get("[ui5-popover]").should("have.prop", "open", true); + + cy.get("#dialogScrollContainer").scrollTo(0, 500); + + cy.get("[ui5-popover]").should("have.prop", "open", false); + }); + + it("should work with nested scrollable containers", () => { + cy.mount( +
+
+
+
+ + +
Nested Popover
+
+
+
+
+
+ ); + + cy.get("[ui5-popover]").should("have.prop", "open", true); + + cy.get("#innerScroll").scrollTo(0, 300); + + cy.get("[ui5-popover]").should("have.prop", "open", false); + }); + + it("should handle horizontal scrolling", () => { + cy.mount( +
+
+ + +
Horizontal Popover
+
+
+
+ ); + + cy.get("[ui5-popover]").should("have.prop", "open", true); + + cy.get("#horizontalScroll").scrollTo(500, 0); + + cy.get("[ui5-popover]").should("have.prop", "open", false); + }); +}); diff --git a/packages/main/src/Popover.ts b/packages/main/src/Popover.ts index 37d3379583d4..e9ee6749d77e 100644 --- a/packages/main/src/Popover.ts +++ b/packages/main/src/Popover.ts @@ -227,6 +227,7 @@ class Popover extends Popup { _oldPlacement?: CalculatedPlacement; _width?: string; _height?: string; + _openerIntersectionObserver?: IntersectionObserver | null; _popoverResize: PopoverResize; @@ -292,9 +293,12 @@ class Popover extends Popup { this._openerRect = opener.getBoundingClientRect(); await super.openPopup(); + this._observeOpenerVisibility(); } closePopup(escPressed = false, preventRegistryUpdate = false, preventFocusRestore = false) : void { + this._unobserveOpenerVisibility(); + Object.assign(this.style, { width: this._initialWidth, height: this._initialHeight, @@ -550,6 +554,47 @@ class Popover extends Popup { return top + (Number.parseInt(this.style.top || "0") - actualTop); } + /** + * Callback invoked when the opener element's intersection changes. + * Closes popover when opener is out of view. + * @private + */ + _onOpenerIntersection(entries: Array): void { + if (!entries[0]?.isIntersecting) { + this.closePopup(); + } + } + + /** + * Starts observing the opener element's visibility in the viewport. + * @private + */ + _observeOpenerVisibility(): void { + const opener = this.getOpenerHTMLElement(this.opener); + + if (!opener) { + return; + } + + this._openerIntersectionObserver = new IntersectionObserver( + this._onOpenerIntersection.bind(this), + { threshold: 0 }, + ); + + this._openerIntersectionObserver.observe(opener); + } + + /** + * Stops observing and cleans up the IntersectionObserver. + * @private + */ + _unobserveOpenerVisibility(): void { + if (this._openerIntersectionObserver) { + this._openerIntersectionObserver.disconnect(); + this._openerIntersectionObserver = null; + } + } + getPopoverSize(calcScrollHeight: boolean = false): PopoverSize { const rect = this.getBoundingClientRect(); const width = rect.width; diff --git a/packages/main/test/pages/Popover.html b/packages/main/test/pages/Popover.html index dbf1b8fec06c..1719e3d661a0 100644 --- a/packages/main/test/pages/Popover.html +++ b/packages/main/test/pages/Popover.html @@ -228,6 +228,21 @@ Close + +
+
+ Open popover and scroll + +
LargeHeader
+
+
+ + Message + +
+
+ Close +


@@ -747,6 +762,15 @@

Popover in ShadowRoot, Opener set as ID in window.document

document.getElementById("big-popover").open = true; }); + document.getElementById("closePopoverAfterScroll").addEventListener("click", function (event) { + scrollableDialog.opener = event.target; + scrollableDialog.open = true; + }); + + document.getElementById("closeAfterScrollBtn").addEventListener("click", function (event) { + scrollableDialog.open = false; + }); + document.getElementById("acc-role-popover-button").addEventListener("click", function (event) { document.getElementById("acc-role-popover").opener = event.target; document.getElementById("acc-role-popover").open = true; From 9fa6bb97ba28323e308fcc90b10445074d79fcab Mon Sep 17 00:00:00 2001 From: Duygu Ramadan Date: Fri, 6 Mar 2026 14:56:28 +0200 Subject: [PATCH 2/4] fix(ui5-popover): add additional delay --- packages/main/src/Popover.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/main/src/Popover.ts b/packages/main/src/Popover.ts index e9ee6749d77e..d96b4bfbfb41 100644 --- a/packages/main/src/Popover.ts +++ b/packages/main/src/Popover.ts @@ -559,8 +559,10 @@ class Popover extends Popup { * Closes popover when opener is out of view. * @private */ - _onOpenerIntersection(entries: Array): void { + async _onOpenerIntersection(entries: Array): Promise { if (!entries[0]?.isIntersecting) { + await this._waitForDomRef(); + this.closePopup(); } } From 6395efc0d2791eb8ab0bce466a1a0004b1bdd6fd Mon Sep 17 00:00:00 2001 From: Duygu Ramadan Date: Fri, 6 Mar 2026 17:02:26 +0200 Subject: [PATCH 3/4] fix(ui5-dialog): edit tests --- packages/main/cypress/specs/Popover.cy.tsx | 8 ++++---- packages/main/src/Popover.ts | 6 ++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/main/cypress/specs/Popover.cy.tsx b/packages/main/cypress/specs/Popover.cy.tsx index e3e4349004e5..59920f1f0f43 100644 --- a/packages/main/cypress/specs/Popover.cy.tsx +++ b/packages/main/cypress/specs/Popover.cy.tsx @@ -1825,7 +1825,7 @@ describe("Opener visibility in scrollable containers", () => { ); - cy.get("[ui5-popover]").should("have.prop", "open", true); + cy.get("[ui5-popover]").ui5PopoverOpened(); cy.get("#scrollContainer").scrollTo(0, 200); @@ -1847,7 +1847,7 @@ describe("Opener visibility in scrollable containers", () => { ); - cy.get("[ui5-popover]").should("have.prop", "open", true); + cy.get("[ui5-popover]").ui5PopoverOpened(); cy.get("#dialogScrollContainer").scrollTo(0, 500); @@ -1870,7 +1870,7 @@ describe("Opener visibility in scrollable containers", () => { ); - cy.get("[ui5-popover]").should("have.prop", "open", true); + cy.get("[ui5-popover]").ui5PopoverOpened(); cy.get("#innerScroll").scrollTo(0, 300); @@ -1889,7 +1889,7 @@ describe("Opener visibility in scrollable containers", () => { ); - cy.get("[ui5-popover]").should("have.prop", "open", true); + cy.get("[ui5-popover]").ui5PopoverOpened(); cy.get("#horizontalScroll").scrollTo(500, 0); diff --git a/packages/main/src/Popover.ts b/packages/main/src/Popover.ts index d96b4bfbfb41..8acd0eb81f31 100644 --- a/packages/main/src/Popover.ts +++ b/packages/main/src/Popover.ts @@ -559,10 +559,8 @@ class Popover extends Popup { * Closes popover when opener is out of view. * @private */ - async _onOpenerIntersection(entries: Array): Promise { - if (!entries[0]?.isIntersecting) { - await this._waitForDomRef(); - + _onOpenerIntersection(entries: Array): void { + if (this.open && !entries[0]?.isIntersecting) { this.closePopup(); } } From d7bec63b6f4df502795d24b78d4cb7b8645f2f53 Mon Sep 17 00:00:00 2001 From: Teodor Taushanov Date: Wed, 11 Mar 2026 16:12:48 +0200 Subject: [PATCH 4/4] chore: fix tests --- packages/main/src/Popover.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/main/src/Popover.ts b/packages/main/src/Popover.ts index 8acd0eb81f31..5bc66cc7c2fe 100644 --- a/packages/main/src/Popover.ts +++ b/packages/main/src/Popover.ts @@ -291,9 +291,9 @@ class Popover extends Popup { this._initialHeight = this.style.height; this._openerRect = opener.getBoundingClientRect(); + this._observeOpenerVisibility(); await super.openPopup(); - this._observeOpenerVisibility(); } closePopup(escPressed = false, preventRegistryUpdate = false, preventFocusRestore = false) : void { @@ -367,6 +367,10 @@ class Popover extends Popup { let rootNode = this.getRootNode(); + if (!rootNode) { + return null; + } + if (rootNode === this) { rootNode = document; } @@ -570,6 +574,8 @@ class Popover extends Popup { * @private */ _observeOpenerVisibility(): void { + this._unobserveOpenerVisibility(); + const opener = this.getOpenerHTMLElement(this.opener); if (!opener) {