diff --git a/packages/main/cypress/specs/Popover.cy.tsx b/packages/main/cypress/specs/Popover.cy.tsx index 9c197a00f026..59920f1f0f43 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]").ui5PopoverOpened(); + + 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]").ui5PopoverOpened(); + + 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]").ui5PopoverOpened(); + + 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]").ui5PopoverOpened(); + + 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..5bc66cc7c2fe 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; @@ -290,11 +291,14 @@ class Popover extends Popup { this._initialHeight = this.style.height; this._openerRect = opener.getBoundingClientRect(); + this._observeOpenerVisibility(); await super.openPopup(); } closePopup(escPressed = false, preventRegistryUpdate = false, preventFocusRestore = false) : void { + this._unobserveOpenerVisibility(); + Object.assign(this.style, { width: this._initialWidth, height: this._initialHeight, @@ -363,6 +367,10 @@ class Popover extends Popup { let rootNode = this.getRootNode(); + if (!rootNode) { + return null; + } + if (rootNode === this) { rootNode = document; } @@ -550,6 +558,49 @@ 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 (this.open && !entries[0]?.isIntersecting) { + this.closePopup(); + } + } + + /** + * Starts observing the opener element's visibility in the viewport. + * @private + */ + _observeOpenerVisibility(): void { + this._unobserveOpenerVisibility(); + + 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;