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 6e5856afa6e3..a78d019f5bd0 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, OpenUI5PopupBasedControlClass, 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, Popover: OpenUI5PopupBasedControl, Patcher: OpenUI5Patcher) => { + window.sap.ui.require(deps, (Popup: OpenUI5PopupClass, Dialog: OpenUI5PopupBasedControlClass, Popover: OpenUI5PopupBasedControlClass, Patcher: OpenUI5Patcher) => { patchPatcher(Patcher); patchPopup(Popup, Dialog, Popover); resolve(); diff --git a/packages/base/src/features/patchPopup.ts b/packages/base/src/features/patchPopup.ts index db07986e49c0..de0d7e800beb 100644 --- a/packages/base/src/features/patchPopup.ts +++ b/packages/base/src/features/patchPopup.ts @@ -6,18 +6,21 @@ 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 +}; + +// 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 OpenUI5PopupBasedControl = { +type OpenUI5PopupBasedControlClass = { 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 @@ -37,6 +43,8 @@ const addOpenedPopup = (popupInfo: PopupInfo) => { }; const removeOpenedPopup = (popup: object) => { + arrangeNativePopovers(); + const index = AllOpenedPopupsRegistry.openedRegistry.findIndex(el => el.instance === popup); if (index > -1) { AllOpenedPopupsRegistry.openedRegistry.splice(index, 1); @@ -68,16 +76,56 @@ const hasWebComponentPopupAbove = (popup: object) => { return false; }; -const openNativePopover = (domRef: HTMLElement) => { +const openNativePopover = (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 arrangeNativePopovers = () => { + 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 closeNativePopover = (domRef: HTMLElement) => { if (domRef.hasAttribute("popover")) { domRef.hidePopover(); domRef.removeAttribute("popover"); } + + const lastPopup = AllOpenedPopupsRegistry.openedRegistry[AllOpenedPopupsRegistry.openedRegistry.length - 1]; + if (lastPopup.type === "OpenUI5") { + const openUI5BlockLayer = document.getElementById("sap-ui-blocklayer-popup"); + if (openUI5BlockLayer && openUI5BlockLayer.hasAttribute("popover")) { + openUI5BlockLayer.hidePopover(); + } + } }; const isNativePopoverOpen = (root: Document | ShadowRoot = document): boolean => { @@ -91,7 +139,7 @@ const isNativePopoverOpen = (root: Document | ShadowRoot = document): boolean => }); }; -const patchPopupBasedControl = (PopupBasedControl: OpenUI5PopupBasedControl) => { +const patchPopupBasedControl = (PopupBasedControl: OpenUI5PopupBasedControlClass) => { const origOnsapescape = PopupBasedControl.prototype.onsapescape; PopupBasedControl.prototype.onsapescape = function onsapescape(...args: any[]) { if (hasWebComponentPopupAbove(this.oPopup)) { @@ -102,18 +150,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); + openNativePopover(domRef, this); } } } @@ -125,7 +172,7 @@ 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(); @@ -139,7 +186,7 @@ const patchClosed = (Popup: OpenUI5Popup) => { }; }; -const patchFocusEvent = (Popup: OpenUI5Popup) => { +const patchFocusEvent = (Popup: OpenUI5PopupClass) => { const origFocusEvent = Popup.prototype.onFocusEvent; Popup.prototype.onFocusEvent = function onFocusEvent(...args: any[]) { if (!hasWebComponentPopupAbove(this)) { @@ -154,7 +201,7 @@ const createGlobalStyles = () => { document.adoptedStyleSheets = [...document.adoptedStyleSheets, stylesheet]; }; -const patchPopup = (Popup: OpenUI5Popup, Dialog: OpenUI5PopupBasedControl, Popover: OpenUI5PopupBasedControl) => { +const patchPopup = (Popup: OpenUI5PopupClass, Dialog: OpenUI5PopupBasedControlClass, Popover: OpenUI5PopupBasedControlClass) => { insertOpenUI5PopupStyles(); patchOpen(Popup); // Popup.prototype.open patchClosed(Popup); // Popup.prototype._closed @@ -171,4 +218,4 @@ export { getTopmostPopup, }; -export type { OpenUI5Popup, OpenUI5PopupBasedControl, PopupInfo }; +export type { OpenUI5PopupClass, OpenUI5PopupBasedControlClass, PopupInfo }; diff --git a/packages/main/cypress/specs/OpenUI5andWebCPopups.cy.tsx b/packages/main/cypress/specs/OpenUI5andWebCPopups.cy.tsx index f11c71d574f2..fb5355493235 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", () => { + + +
{ .should('be.focused'); } + function OpenWebCUI5DialogMixed() { + // Open UI5 Dialog + cy.get("#openUI5Button") + .should('be.visible') + .realClick(); + + cy.get("#openUI5Dialog1") + .should('be.visible'); + + // Open WebC Dialog from UI5 Dialog + cy.get("#openWebCDialog") + .should('be.visible') + .realClick(); + + cy.get("#webCDialog1") + .should('be.visible'); + + cy.get("#openUI5Dialog1") + .should('not.be.visible'); + + // Open UI5 Dialog from WebC Dialog + cy.get("#openUI5DialogFromWebC") + .should('be.visible') + .realClick(); + + cy.get("#webCDialog1") + .should('not.be.visible'); + + cy.get("#openUI5Dialog1") + .should('not.be.visible'); + + cy.get("#openUI5DialogWithButtons") + .should('be.visible'); + + // Open UI5 Dialog from UI5 Dialog + cy.get("#openUI5DialogFromUi5") + .should('be.visible') + .realClick(); + + cy.get("#openUI5DialogFinal") + .should('be.visible'); + + cy.get("#openUI5Dialog1") + .should('not.be.visible'); + + cy.get("#webCDialog1") + .should('not.be.visible'); + + cy.get("#openUI5DialogWithButtons") + .should('not.be.visible'); + + // Close all with Escape + cy.realPress("Escape"); + + cy.get("#openUI5DialogFinal") + .should('not.exist'); + + cy.get("#openUI5DialogWithButtons") + .should('be.visible'); + + cy.realPress("Escape"); + + cy.get("#openUI5DialogWithButtons") + .should('not.exist'); + + cy.get("#webCDialog1") + .should('be.visible'); + + cy.realPress("Escape"); + + cy.get("#openUI5Dialog1") + .should('be.visible'); + + cy.realPress("Escape"); + + cy.get("#openWebCDialog") + .should('not.exist'); + } + it("Keyboard", () => { OpenWebCDialog(); OpenWebCDialogOpenOpenUI5Dialog(); @@ -617,6 +734,7 @@ describe("ui5 and web components integration", () => { OpenUI5DialogWebCDialog(); OpenUI5DialogWebCPopoverNoFocus(); OpenUI5DialogWebCSelect(); + OpenWebCUI5DialogMixed(); // Merge it after OpenUI5 Popup shadow dom focus fix is released // OpenUI5DialogWebCComboBox(); }); diff --git a/packages/main/test/pages/DialogAndOpenUI5Dialog.html b/packages/main/test/pages/DialogAndOpenUI5Dialog.html index 8c118a4f8dd8..58fb5629124f 100644 --- a/packages/main/test/pages/DialogAndOpenUI5Dialog.html +++ b/packages/main/test/pages/DialogAndOpenUI5Dialog.html @@ -27,6 +27,7 @@ press: function () { new Dialog("openUI5Dialog", { title: "OpenUI5 Dialog", + draggable: true, content: [ new HTML({ content: @@ -62,6 +63,12 @@ press: function () { document.getElementById("respPopoverNoInitialFocus").open = true; } + }), + new Button("openUI5DialogOpener", { + text: "Open UI5 Dialog", + press: function () { + openUI5Dialog2(); + } }) ], afterClose: function () { @@ -112,7 +119,6 @@ } }).placeAt("dialog1content"); - ShortcutHintsMixin.addConfig(button, { event: "press", position: "0 0", @@ -128,20 +134,34 @@ document.getElementById("popoverButtonNoFocus").addEventListener("click", function (event) { openUI5Popover(event.target); }); + + document.getElementById("someButton2").addEventListener("click", function () { + openUI5Dialog2(); + }); + document.getElementById("someButton3").addEventListener("click", function () { + openUI5Dialog3(); + }); + document.getElementById("someButton4").addEventListener("click", function () { + document.getElementById("newDialog4").open = true; + }); + document.getElementById("openUi5PopoverBlockLayer").addEventListener("click", function () { + openUI5Popover(this); + }); } function openUI5Dialog() { sap.ui.require(["sap/m/Button", "sap/m/Dialog"], (Button, Dialog) => { new Dialog("openUI5DialogWithButtons", { - title: "OpenUI5 Dialog", + title: "UI5 Dialog 1", + draggable: true, content: [ new Button({ text: "Focus stop" }), new Button("openUI5DialogButton", { - text: "Open WebC Dialog", + text: "Open UI5 Dialog 2", press: function () { - document.getElementById("newDialog1").open = true; + openUI5Dialog2(); } }) ], @@ -152,20 +172,90 @@ }); } + function openUI5Dialog2() { + sap.ui.require(["sap/m/Button", "sap/m/Dialog"], (Button, Dialog) => { + new Dialog("openUI5DialogWithButtons2", { + title: "UI5 Dialog 2", + draggable: true, + content: [ + new Button({ + text: "Focus stop" + }), + new Button("openUI5DialogButton2", { + text: "Open WebC Dialog 2", + press: function () { + document.getElementById("newDialog2").open = true; + } + }) + ], + afterClose: function () { + this.destroy(); + } + }).open(); + }); + } + + function openUI5Dialog3() { + sap.ui.require(["sap/m/Button", "sap/m/Dialog"], (Button, Dialog) => { + new Dialog("openUI5DialogWithButtons3", { + title: "UI5 Dialog 3", + draggable: true, + content: [ + new Button({ + text: "Focus stop" + }), + new Button("openUI5DialogButton22", { + text: "Open WebC Dialog 3", + press: function () { + document.getElementById("newDialog3").open = true; + } + }) + ], + afterClose: function () { + this.destroy(); + } + }).open(); + }); + } + function openUI5Popover(opener) { sap.ui.require(["sap/m/Popover", "sap/m/Button"], (Popover, Button) => { new Popover("openUI5PopoverSecond", { - title: "OpenUI5 Popover", + title: "OpenUI5 Popover 1", content: [ - new Button("someButton", { + new Button({ + text: "Open ui5 Dialog", + press: function () { + openUI5Dialog3(); + } + }), + new Button({ + text: "Open WebC Dialog", + press: function () { + document.getElementById("newDialog4").open = true; + } + }), + new Button("popoverOpener", { text: "Open new OpenUI5 Popover", press: function (oEvent) { - new Popover({ + new Popover("openUI5PopoverThird", { title: "New OpenUI5 Popover", placement: "Bottom", content: [ new Button({ text: "Focus stop" + }), + new Button({ + text: "Open Ui5 Dialog", + press: function () { + openUI5Dialog3(); + } + }), + new Button({ + text: "Open WebC Dialog", + press: function () { + document.getElementById("newDialog4").open = true; + } }) ], initialFocus: "someButton", @@ -189,7 +279,7 @@
Open WebC Dialog
- +

Web Components: @@ -211,19 +301,30 @@ Open UI5 dialog Open UI5 Popover No Initial Focus
- - Some button + + Some button 1 + + + Open UI5 Dialog + Open UI5 Popover + + + Open WebC Dialog + + + + Some button 3
+ header-text="This is a Web Component Responsive Popover"> Some button + header-text="This is a Web Component RP with no initial focus"> Some button