Skip to content
Open
Show file tree
Hide file tree
Changes from all 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%;
}
4 changes: 2 additions & 2 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 @@ -110,7 +110,7 @@ class OpenUI5Support {
"sap/ui/core/date/CalendarUtils",
];
}
window.sap.ui.require(deps, (Popup: OpenUI5Popup, Dialog: OpenUI5PopupBasedControl, Patcher: OpenUI5Patcher) => {
window.sap.ui.require(deps, (Popup: OpenUI5PopupClass, Dialog: OpenUI5DialogClass, Patcher: OpenUI5Patcher) => {
patchPatcher(Patcher);
patchPopup(Popup, Dialog);
resolve();
Expand Down
137 changes: 100 additions & 37 deletions packages/base/src/features/patchPopup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,31 @@ type Control = {

// 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 = {
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) {
fixTopmostOpenUI5Popup();
}

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

const openNativePopover = (domRef: HTMLElement) => {
const getPopupContentElement = (popup: OpenUI5Popup): HTMLElement | null => {
const content = popup.getContent()!;
return content instanceof HTMLElement ? content : content?.getDomRef() || null;
};

const openNativePopoverForOpenUI5 = (popup: OpenUI5Popup) => {
const openingInitiated = ["OPENING", "OPEN"].includes(popup.getOpenState());
if (!openingInitiated || !isNativePopoverOpen()) {
return;
}

const domRef = getPopupContentElement(popup);

if (!domRef) {
return;
}

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 closeNativePopoverForOpenUI5 = (popup: OpenUI5Popup) => {
const domRef = getPopupContentElement(popup);

if (!domRef) {
return;
}

if (domRef.hasAttribute("popover")) {
domRef.hidePopover();
domRef.removeAttribute("popover");
}

if (getTopmostPopup() !== popup) {
return;
}

// The OpenUI5 block layer is only one for all modal OpenUI5 popups,
// and it is displayed above all opened pupups - OpenUI5 and Web Components,
// as a result, we need to hide this block layer.
// If the underlying popup is a Web Component - it is displayed like a native popover, and we don't need to do anything
// If the underlying popup is an OpenUI5 popup, it will be fixed in fixTopmostOpenUI5Popup method.
if (popup.getModal()) {
const openUI5BlockLayer = document.getElementById("sap-ui-blocklayer-popup");
if (openUI5BlockLayer && openUI5BlockLayer.hasAttribute("popover")) {
openUI5BlockLayer.hidePopover();
}
}
};

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

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

const content = getPopupContentElement(prevPopup.instance);
const openUI5BlockLayer = document.getElementById("sap-ui-blocklayer-popup");

content?.hidePopover();
openUI5BlockLayer?.showPopover();

content?.showPopover();
};

const isNativePopoverOpen = (root: Document | ShadowRoot = document): boolean => {
Expand All @@ -91,9 +169,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,21 +180,11 @@ 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) {
const element = this.getContent();
if (element) {
const domRef = element instanceof HTMLElement ? element : element?.getDomRef();
if (domRef) {
openNativePopover(domRef);
}
}
}
openNativePopoverForOpenUI5(this);

addOpenedPopup({
type: "OpenUI5",
Expand All @@ -125,21 +193,16 @@ const patchOpen = (Popup: OpenUI5Popup) => {
};
};

const patchClosed = (Popup: OpenUI5Popup) => {
const patchClosed = (Popup: OpenUI5PopupClass) => {
const _origClosed = Popup.prototype._closed;
Popup.prototype._closed = function _closed(...args: any[]) {
const element = this.getContent();
const domRef = element instanceof HTMLElement ? element : element?.getDomRef();
closeNativePopoverForOpenUI5(this);
_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
}

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,13 +217,13 @@ const createGlobalStyles = () => {
document.adoptedStyleSheets = [...document.adoptedStyleSheets, stylesheet];
};

const patchPopup = (Popup: OpenUI5Popup, Dialog: 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
patchDialog(Dialog); // Dialog.prototype.onsapescape
};

export {
Expand All @@ -170,4 +233,4 @@ export {
getTopmostPopup,
};

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