Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
e76d000
fix(ui5-popover): render block layers in the correct order
kskondov Nov 11, 2025
9f880ec
fix(ui5-popover): render block layers in the correct order
kskondov Nov 11, 2025
2f34f5c
Merge remote-tracking branch 'origin/main' into ui5-popover-block-layers
TeodorTaushanov Nov 11, 2025
598adbb
chore: handle opening and closing
TeodorTaushanov Nov 11, 2025
f7fd078
fix(ui5-popover): render block layers in the correct order
kskondov Nov 19, 2025
9b7f595
Merge remote-tracking branch 'origin/main' into ui5-popover-block-layers
TeodorTaushanov Nov 21, 2025
63fe5d3
chore: improve TypeScript structure
TeodorTaushanov Nov 21, 2025
b214070
chore: improve TypeScript structure, fix some issues
TeodorTaushanov Nov 24, 2025
da23cdf
Merge remote-tracking branch 'origin/main' into ui5-popover-block-layers
TeodorTaushanov Nov 24, 2025
4a25afd
Merge remote-tracking branch 'origin/main' into ui5-popover-block-layers
TeodorTaushanov Nov 27, 2025
b4ea772
Merge remote-tracking branch 'origin/main' into ui5-popover-block-layers
TeodorTaushanov Nov 28, 2025
d2d83e6
chore: improve code
TeodorTaushanov Nov 28, 2025
2a94f75
Merge remote-tracking branch 'origin/main' into ui5-popover-block-layers
TeodorTaushanov Nov 28, 2025
06049d7
Merge remote-tracking branch 'origin/main' into ui5-popover-block-layers
TeodorTaushanov Dec 1, 2025
dc01f29
chore: address code comments
TeodorTaushanov Dec 1, 2025
c0a5936
Merge remote-tracking branch 'origin/main' into ui5-popover-block-layers
TeodorTaushanov Dec 1, 2025
75cc691
chore: address code comments
TeodorTaushanov Dec 1, 2025
b6ae187
Merge remote-tracking branch 'origin/main' into ui5-popover-block-layers
TeodorTaushanov Dec 1, 2025
1204bbe
chore: fix tests
TeodorTaushanov Dec 1, 2025
ff2da1c
Merge remote-tracking branch 'origin/main' into ui5-popover-block-layers
TeodorTaushanov Dec 1, 2025
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
5 changes: 5 additions & 0 deletions packages/base/src/css/OpenUI5PopupStyles.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,9 @@
border: none;
overflow: visible;
margin: 0;
}

.sapUiBLy[popover] {
width: 100%;
height: 100%;
}
8 changes: 4 additions & 4 deletions packages/base/src/features/OpenUI5Support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
removeOpenedPopup,
getTopmostPopup,
} from "./patchPopup.js";
import type { OpenUI5Popup, OpenUI5PopupBasedControl, PopupInfo } from "./patchPopup.js";
import type { OpenUI5PopupClass, OpenUI5DialogClass, PopupInfo } from "./patchPopup.js";
import { registerFeature } from "../FeaturesRegistry.js";
import { setTheme } from "../config/Theme.js";
import type { CLDRData } from "../asset-registries/LocaleData.js";
Expand Down Expand Up @@ -99,7 +99,7 @@ class OpenUI5Support {
OpenUI5Support.initPromise = new Promise<void>(resolve => {
window.sap.ui.require(["sap/ui/core/Core"], async (Core: OpenUI5Core) => {
const callback = () => {
let deps: Array<string> = ["sap/ui/core/Popup", "sap/m/Dialog", "sap/m/Popover", "sap/ui/core/Patcher", "sap/ui/core/LocaleData"];
let deps: Array<string> = ["sap/ui/core/Popup", "sap/m/Dialog", "sap/ui/core/Patcher", "sap/ui/core/LocaleData"];
if (OpenUI5Support.isAtLeastVersion116()) { // for versions since 1.116.0 and onward, use the modular core
deps = [
...deps,
Expand All @@ -110,9 +110,9 @@ class OpenUI5Support {
"sap/ui/core/date/CalendarUtils",
];
}
window.sap.ui.require(deps, (Popup: OpenUI5Popup, Dialog: OpenUI5PopupBasedControl, Popover: OpenUI5PopupBasedControl, Patcher: OpenUI5Patcher) => {
window.sap.ui.require(deps, (Popup: OpenUI5PopupClass, Dialog: OpenUI5DialogClass, Patcher: OpenUI5Patcher) => {
patchPatcher(Patcher);
patchPopup(Popup, Dialog, Popover);
patchPopup(Popup, Dialog);
resolve();
});
};
Expand Down
101 changes: 75 additions & 26 deletions packages/base/src/features/patchPopup.ts
Copy link
Member

Choose a reason for hiding this comment

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

in getTopmostPopup - AllOpenedPopupsRegistry.openedRegistry potentially could be empty and then the code will fail with error. Add a check and return null if empty

Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,33 @@ type Control = {
getDomRef: () => HTMLElement | null,
}

// The lifecycle of Popup.js is open -> _opened -> close -> _closed, we're interested in the first (open) and last (_closed)
type OpenUI5Popup = {
prototype: {
open: (...args: any[]) => void,
_closed: (...args: any[]) => void,
getOpenState: () => "CLOSED" | "CLOSING" | "OPEN" | "OPENING",
getContent: () => Control | HTMLElement | null, // this is the OpenUI5 Element/Control instance that opens the Popup (usually sap.m.Popover/sap.m.Dialog)
onFocusEvent: (...args: any[]) => void,
}
open: (...args: any[]) => void,
_closed: (...args: any[]) => void,
getOpenState: () => "CLOSED" | "CLOSING" | "OPEN" | "OPENING",
getContent: () => Control | HTMLElement | null, // this is the OpenUI5 Element/Control instance that opens the Popup (usually sap.m.Popover/sap.m.Dialog)
onFocusEvent: (...args: any[]) => void,
getModal: () => boolean
};

type OpenUI5PopupBasedControl = {
// The lifecycle of Popup.js is open -> _opened -> close -> _closed, we're interested in the first (open) and last (_closed)
type OpenUI5PopupClass = {
prototype: OpenUI5Popup
};

type OpenUI5DialogClass = {
prototype: {
onsapescape: (...args: any[]) => void,
oPopup: OpenUI5Popup,
}
};

type PopupInfo = {
type: "OpenUI5" | "WebComponent";
type: "WebComponent";
instance: object;
} | {
type: "OpenUI5";
instance: OpenUI5Popup;
};

// contains all OpenUI5 and Web Component popups that are currently opened
Expand All @@ -38,6 +44,11 @@ const addOpenedPopup = (popupInfo: PopupInfo) => {

const removeOpenedPopup = (popup: object) => {
const index = AllOpenedPopupsRegistry.openedRegistry.findIndex(el => el.instance === popup);

if (index === AllOpenedPopupsRegistry.openedRegistry.length - 1) {
fixOpenUI5PopupBelow();
}

if (index > -1) {
AllOpenedPopupsRegistry.openedRegistry.splice(index, 1);
}
Expand Down Expand Up @@ -68,16 +79,56 @@ const hasWebComponentPopupAbove = (popup: object) => {
return false;
};

const openNativePopover = (domRef: HTMLElement) => {
const enableNativePopoverForOpenUI5 = (domRef: HTMLElement, popup: OpenUI5Popup) => {
const openUI5BlockLayer = document.getElementById("sap-ui-blocklayer-popup");

if (popup.getModal() && openUI5BlockLayer) {
openUI5BlockLayer.setAttribute("popover", "manual");
openUI5BlockLayer.hidePopover();
openUI5BlockLayer.showPopover();
}

domRef.setAttribute("popover", "manual");
domRef.showPopover();
};

const closeNativePopover = (domRef: HTMLElement) => {
const disableNativePopoverForOpenUI5 = (domRef: HTMLElement) => {
if (domRef.hasAttribute("popover")) {
domRef.hidePopover();
domRef.removeAttribute("popover");
}

const lastPopup = AllOpenedPopupsRegistry.openedRegistry[AllOpenedPopupsRegistry.openedRegistry.length - 1];
if (lastPopup.type === "OpenUI5" && lastPopup.instance.getModal()) {
const openUI5BlockLayer = document.getElementById("sap-ui-blocklayer-popup");
if (openUI5BlockLayer && openUI5BlockLayer.hasAttribute("popover")) {
openUI5BlockLayer.hidePopover();
}
}
};

const fixOpenUI5PopupBelow = () => {
if (!isNativePopoverOpen()) {
return;
}

const prevPopup = AllOpenedPopupsRegistry.openedRegistry[AllOpenedPopupsRegistry.openedRegistry.length - 2];
if (!prevPopup || prevPopup.type !== "OpenUI5" || !prevPopup.instance.getModal()) {
return;
}

const prevPopupContent = prevPopup.instance.getContent()!;
const content = prevPopupContent instanceof HTMLElement ? prevPopupContent : prevPopupContent?.getDomRef();

const openUI5BlockLayer = document.getElementById("sap-ui-blocklayer-popup");

content?.hidePopover();

if (prevPopup.instance.getModal()) {
openUI5BlockLayer?.showPopover();
}

content?.showPopover();
};

const isNativePopoverOpen = (root: Document | ShadowRoot = document): boolean => {
Expand All @@ -91,9 +142,9 @@ const isNativePopoverOpen = (root: Document | ShadowRoot = document): boolean =>
});
};

const patchPopupBasedControl = (PopupBasedControl: OpenUI5PopupBasedControl) => {
const origOnsapescape = PopupBasedControl.prototype.onsapescape;
PopupBasedControl.prototype.onsapescape = function onsapescape(...args: any[]) {
const patchDialog = (Dialog: OpenUI5DialogClass) => {
const origOnsapescape = Dialog.prototype.onsapescape;
Dialog.prototype.onsapescape = function onsapescape(...args: any[]) {
if (hasWebComponentPopupAbove(this.oPopup)) {
return;
}
Expand All @@ -102,18 +153,17 @@ const patchPopupBasedControl = (PopupBasedControl: OpenUI5PopupBasedControl) =>
};
};

const patchOpen = (Popup: OpenUI5Popup) => {
const patchOpen = (Popup: OpenUI5PopupClass) => {
const origOpen = Popup.prototype.open;
Popup.prototype.open = function open(...args: any[]) {
origOpen.apply(this, args); // call open first to initiate opening
const topLayerAlreadyInUse = isNativePopoverOpen();
const openingInitiated = ["OPENING", "OPEN"].includes(this.getOpenState());
if (openingInitiated && topLayerAlreadyInUse) {
if (openingInitiated && isNativePopoverOpen()) {
const element = this.getContent();
if (element) {
const domRef = element instanceof HTMLElement ? element : element?.getDomRef();
if (domRef) {
openNativePopover(domRef);
enableNativePopoverForOpenUI5(domRef, this);
}
}
}
Expand All @@ -125,21 +175,21 @@ const patchOpen = (Popup: OpenUI5Popup) => {
};
};

const patchClosed = (Popup: OpenUI5Popup) => {
const patchClosed = (Popup: OpenUI5PopupClass) => {
Copy link
Member

Choose a reason for hiding this comment

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

if dialogs are destroyed instead of closed, the block layer remains with "popover=manual"
can be fixed in separate change/backlog

const _origClosed = Popup.prototype._closed;
Popup.prototype._closed = function _closed(...args: any[]) {
const element = this.getContent();
const domRef = element instanceof HTMLElement ? element : element?.getDomRef();
_origClosed.apply(this, args); // only then call _close
if (domRef) {
closeNativePopover(domRef); // unset the popover attribute and close the native popover, but only if still in DOM
disableNativePopoverForOpenUI5(domRef); // unset the popover attribute and close the native popover, but only if still in DOM
}

removeOpenedPopup(this);
};
};

const patchFocusEvent = (Popup: OpenUI5Popup) => {
const patchFocusEvent = (Popup: OpenUI5PopupClass) => {
const origFocusEvent = Popup.prototype.onFocusEvent;
Popup.prototype.onFocusEvent = function onFocusEvent(...args: any[]) {
if (!hasWebComponentPopupAbove(this)) {
Expand All @@ -154,14 +204,13 @@ const createGlobalStyles = () => {
document.adoptedStyleSheets = [...document.adoptedStyleSheets, stylesheet];
};

const patchPopup = (Popup: OpenUI5Popup, Dialog: OpenUI5PopupBasedControl, Popover: OpenUI5PopupBasedControl) => {
const patchPopup = (Popup: OpenUI5PopupClass, Dialog: OpenUI5DialogClass) => {
insertOpenUI5PopupStyles();
patchOpen(Popup); // Popup.prototype.open
patchClosed(Popup); // Popup.prototype._closed
createGlobalStyles(); // Ensures correct popover positioning by OpenUI5 (otherwise 0,0 is the center of the screen)
patchFocusEvent(Popup);// Popup.prototype.onFocusEvent
patchPopupBasedControl(Dialog); // Dialog.prototype.onsapescape
patchPopupBasedControl(Popover); // Popover.prototype.onsapescape
patchDialog(Dialog); // Dialog.prototype.onsapescape
};

export {
Expand All @@ -171,4 +220,4 @@ export {
getTopmostPopup,
};

export type { OpenUI5Popup, OpenUI5PopupBasedControl, PopupInfo };
export type { OpenUI5PopupClass, OpenUI5DialogClass, PopupInfo };
Loading
Loading