Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions packages/main/cypress/specs/Popover.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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(
<div id="scrollContainer" style={{ height: "200px", overflowY: "auto" }}>
<div style={{ height: "500px" }}>
<Button id="opener" style={{ marginTop: "100px" }}>Open</Button>
<Popover opener="opener" open={true}>
<div>Popover Content</div>
</Popover>
</div>
</div>
);

cy.get<Popover>("[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(
<Dialog open={true}>
<div slot="header" style={{ height: "200px" }}>LargeHeader</div>
<div id="dialogScrollContainer" style={{ height: "200px", overflowY: "auto" }}>
<div style={{ height: "1000px" }}>
<Button id="dialogOpener" style={{ marginTop: "100px" }}>Opener</Button>
<Popover opener="dialogOpener" open={true}>
<div>Popover in Dialog</div>
</Popover>
</div>
</div>
</Dialog>
);

cy.get<Popover>("[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(
<div style={{ height: "300px", overflowY: "auto" }}>
<div style={{ height: "500px" }}>
<div id="innerScroll" style={{ height: "150px", overflowY: "auto", marginTop: "50px" }}>
<div style={{ height: "800px" }}>
<Button id="nestedOpener" style={{ marginTop: "80px" }}>Nested Opener</Button>
<Popover opener="nestedOpener" open={true}>
<div>Nested Popover</div>
</Popover>
</div>
</div>
</div>
</div>
);

cy.get<Popover>("[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(
<div id="horizontalScroll" style={{ width: "200px", height: "200px", overflowX: "auto" }}>
<div style={{ width: "1000px", height: "100px" }}>
<Button id="hOpener" style={{ marginLeft: "100px" }}>Horizontal Opener</Button>
<Popover opener="hOpener" open={true}>
<div>Horizontal Popover</div>
</Popover>
</div>
</div>
);

cy.get<Popover>("[ui5-popover]").ui5PopoverOpened();

cy.get("#horizontalScroll").scrollTo(500, 0);

cy.get("[ui5-popover]").should("have.prop", "open", false);
});
});
51 changes: 51 additions & 0 deletions packages/main/src/Popover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ class Popover extends Popup {
_oldPlacement?: CalculatedPlacement;
_width?: string;
_height?: string;
_openerIntersectionObserver?: IntersectionObserver | null;

_popoverResize: PopoverResize;

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -363,6 +367,10 @@ class Popover extends Popup {

let rootNode = this.getRootNode();

if (!rootNode) {
return null;
}

if (rootNode === this) {
rootNode = document;
}
Expand Down Expand Up @@ -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.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* Callback invoked when the opener element's intersection changes.
* Callback invoked when the opener element's intersection status changes.

* Closes popover when opener is out of view.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* Closes popover when opener is out of view.
* Closes the popover when the opener is no longer visible.

? no longer visible in the viewport. ?

* @private
*/
_onOpenerIntersection(entries: Array<IntersectionObserverEntry>): 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.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* Stops observing and cleans up the IntersectionObserver.
* Stops observing the opener element and cleans up the IntersectionObserver instance.

* @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;
Expand Down
24 changes: 24 additions & 0 deletions packages/main/test/pages/Popover.html
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,21 @@
<ui5-button id="focusMe">Close</ui5-button>
</div>
</ui5-popover>

<br>
<br>
<ui5-button id="closePopoverAfterScroll">Open popover and scroll</ui5-button>
<ui5-dialog id="scrollableDialog">
<div slot="header" style="height: 200px">LargeHeader</div>
<div style="height: 200px; overflow: auto">
<div style="height: 1000px">
<ui5-input value-state="Negative">
<span slot="valueStateMessage">Message</span>
</ui5-input>
</div>
</div>
<ui5-button slot="footer" id="closeAfterScrollBtn" design="Emphasized">Close</ui5-button>
</ui5-dialog>

<br>
<br>
Expand Down Expand Up @@ -747,6 +762,15 @@ <h3>Popover in ShadowRoot, Opener set as ID in window.document</h3>
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;
Expand Down
Loading