diff --git a/packages/base/src/css/OpenUI5PopupStyles.css b/packages/base/src/css/OpenUI5PopupStyles.css index 49dad81fd254..d523159cf4fd 100644 --- a/packages/base/src/css/OpenUI5PopupStyles.css +++ b/packages/base/src/css/OpenUI5PopupStyles.css @@ -2,4 +2,9 @@ border: none; overflow: visible; margin: 0; +} + +.sapUiBLy[popover] { + width: 100%; + height: 100%; } \ No newline at end of file diff --git a/packages/base/src/features/OpenUI5Support.ts b/packages/base/src/features/OpenUI5Support.ts index 6ad44830cdcb..8bf7827e5082 100644 --- a/packages/base/src/features/OpenUI5Support.ts +++ b/packages/base/src/features/OpenUI5Support.ts @@ -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"; @@ -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(); diff --git a/packages/base/src/features/patchPopup.ts b/packages/base/src/features/patchPopup.ts index a5e6fafadf68..cac58ca5dff1 100644 --- a/packages/base/src/features/patchPopup.ts +++ b/packages/base/src/features/patchPopup.ts @@ -8,16 +8,19 @@ 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, @@ -25,8 +28,11 @@ type OpenUI5PopupBasedControl = { }; type PopupInfo = { - type: "OpenUI5" | "WebComponent"; + type: "WebComponent"; instance: object; +} | { + type: "OpenUI5"; + instance: OpenUI5Popup; }; // contains all OpenUI5 and Web Component popups that are currently opened @@ -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); } @@ -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 => { @@ -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; } @@ -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", @@ -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)) { @@ -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 { @@ -170,4 +233,4 @@ export { getTopmostPopup, }; -export type { OpenUI5Popup, OpenUI5PopupBasedControl, PopupInfo }; +export type { OpenUI5PopupClass, OpenUI5DialogClass, PopupInfo }; diff --git a/packages/main/cypress/specs/OpenUI5andWebCPopups.cy.tsx b/packages/main/cypress/specs/OpenUI5andWebCPopups.cy.tsx index f11c71d574f2..b5e62f6e546c 100644 --- a/packages/main/cypress/specs/OpenUI5andWebCPopups.cy.tsx +++ b/packages/main/cypress/specs/OpenUI5andWebCPopups.cy.tsx @@ -17,6 +17,7 @@ function onOpenUI5InitMethod(win) { press: function () { new Dialog("openUI5Dialog1", { title: "OpenUI5 Dialog", + draggable: true, content: [ new HTML({ content: @@ -49,6 +50,12 @@ function onOpenUI5InitMethod(win) { press: function () { (document.getElementById("respPopoverNoInitialFocus") as any).open = true; } + }), + new Button("openWebCDialog", { + text: "Open WebC Dialog", + press: function () { + (document.getElementById("webCDialog1") as any).open = true; + } }) ], afterClose: function () { @@ -112,6 +119,10 @@ function onOpenUI5InitMethod(win) { openUI5Dialog(win); }); + document.getElementById("openUI5DialogFromWebC").addEventListener("click", function () { + openUI5Dialog(win); + }); + document.getElementById("popoverButtonNoFocus").addEventListener("click", function (event) { openUI5Popover(win, event.target); }); @@ -121,6 +132,7 @@ function openUI5Dialog(win) { (win as any).sap.ui.require(["sap/m/Button", "sap/m/Dialog"], (Button, Dialog) => { new Dialog("openUI5DialogWithButtons", { title: "OpenUI5 Dialog", + draggable: true, content: [ new Button({ text: "Focus stop" @@ -130,6 +142,29 @@ function openUI5Dialog(win) { press: function () { (document.getElementById("newDialog1") as any).open = true; } + }), + new Button("openUI5DialogFromUi5", { + text: "Open UI5 Dialog", + press: function () { + openUI5DialogFromUi5(win) + } + }) + ], + afterClose: function () { + this.destroy(); + } + }).open(); + }); +} + +function openUI5DialogFromUi5(win) { + (win as any).sap.ui.require(["sap/m/Button", "sap/m/Dialog"], (Button, Dialog) => { + new Dialog("openUI5DialogFinal", { + title: "OpenUI5 Dialog", + draggable: true, + content: [ + new Button({ + text: "Focus stop" }) ], afterClose: function () { @@ -204,6 +239,9 @@ describe("ui5 and web components integration", () => { +