From 0c0afd47e866c53ca090841fe4451c8f92849519 Mon Sep 17 00:00:00 2001 From: Lukas Harbarth Date: Wed, 28 Jan 2026 11:07:31 +0100 Subject: [PATCH 1/7] feat(Modals): support auto-closing popovers sharing the same opener --- packages/main/src/components/Modals/index.tsx | 35 ++++++++++++++++++- packages/main/src/internal/ModalStore.ts | 4 +++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/packages/main/src/components/Modals/index.tsx b/packages/main/src/components/Modals/index.tsx index 7e1b931f64d..83f41f0dd80 100644 --- a/packages/main/src/components/Modals/index.tsx +++ b/packages/main/src/components/Modals/index.tsx @@ -21,6 +21,24 @@ import { Toast } from '../../webComponents/Toast/index.js'; import type { MessageBoxPropTypes } from '../MessageBox/index.js'; import { MessageBox } from '../MessageBox/index.js'; +interface ModalConfig { + /** + * Optional container where the component should be mounted. Defaults to `document.body`. + */ + container?: Element | DocumentFragment; +} + +interface ModalConfigPopover extends ModalConfig { + /** + * If set to `true`, opening a new Popover will automatically close all currently opened Popovers that share the __same opener__. + * + * __Note:__ + * - This only affects `Popover`, `Menu`, and `ResponsivePopover`. + * - Only closes open Popovers with the __same opener__. + */ + autoClosePopovers?: boolean; +} + type ModalReturnType = { ref: MutableRefObject; }; @@ -62,12 +80,27 @@ function showDialogFn( }; } +function showPopoverFn(props: PopoverPropTypes, config?: ModalConfigPopover): ClosableModalReturnType; function showPopoverFn( props: PopoverPropTypes, container?: Element | DocumentFragment, +): ClosableModalReturnType; +function showPopoverFn( + props: PopoverPropTypes, + containerOrConfig?: Element | DocumentFragment | ModalConfigPopover, ): ClosableModalReturnType { + const isContainer = containerOrConfig instanceof Element || containerOrConfig instanceof DocumentFragment; const id = getRandomId(); const ref = createRef(); + if (!isContainer && containerOrConfig?.autoClosePopovers) { + const openPopovers = ModalStore.getPopoversWithSameOpener(props.opener); + openPopovers.forEach((popover) => { + const popoverRef = popover.ref as MutableRefObject; + if (popoverRef.current) { + popoverRef.current.open = false; + } + }); + } ModalStore.addModal({ Component: Popover, props: { @@ -81,7 +114,7 @@ function showPopoverFn( }, }, ref, - container, + container: isContainer ? containerOrConfig : containerOrConfig?.container, id, }); diff --git a/packages/main/src/internal/ModalStore.ts b/packages/main/src/internal/ModalStore.ts index 31ffcd83a7f..e6727946d82 100644 --- a/packages/main/src/internal/ModalStore.ts +++ b/packages/main/src/internal/ModalStore.ts @@ -1,5 +1,6 @@ import { getCurrentRuntimeIndex } from '@ui5/webcomponents-base/dist/Runtimes.js'; import type { ComponentType, RefCallback, RefObject } from 'react'; +import type { PopoverPropTypes } from '../webComponents/Popover/index.js'; globalThis['@ui5/webcomponents-react'] ??= {}; const STORE_LOCATION = globalThis['@ui5/webcomponents-react']; @@ -60,4 +61,7 @@ export const ModalStore = { STORE_LOCATION[getStyleStoreSymbol()] = getSnapshot().filter((modal) => modal.id !== id); emitChange(); }, + getPopoversWithSameOpener(opener: PopoverPropTypes['opener']) { + return getSnapshot().filter((popover) => popover.props?.opener === opener); + }, }; From 61c59100a96d970751ced1416d35871bd186ed2e Mon Sep 17 00:00:00 2001 From: Lukas Harbarth Date: Wed, 28 Jan 2026 11:16:12 +0100 Subject: [PATCH 2/7] Update index.tsx --- packages/main/src/components/Modals/index.tsx | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/main/src/components/Modals/index.tsx b/packages/main/src/components/Modals/index.tsx index 83f41f0dd80..4642c691762 100644 --- a/packages/main/src/components/Modals/index.tsx +++ b/packages/main/src/components/Modals/index.tsx @@ -23,7 +23,9 @@ import { MessageBox } from '../MessageBox/index.js'; interface ModalConfig { /** - * Optional container where the component should be mounted. Defaults to `document.body`. + * Optional container where the component should be mounted. + * + * @default `document.body` */ container?: Element | DocumentFragment; } @@ -47,6 +49,16 @@ type ClosableModalReturnType = ModalReturnType & { close: () => void; }; +function autoClose(props: { opener?: PopoverPropTypes['opener'] }) { + const openPopovers = ModalStore.getPopoversWithSameOpener(props.opener); + openPopovers.forEach((popover) => { + const popoverRef = popover.ref as MutableRefObject; + if (popoverRef.current) { + popoverRef.current.open = false; + } + }); +} + function showDialogFn( props: DialogPropTypes, container?: Element | DocumentFragment, @@ -93,13 +105,7 @@ function showPopoverFn( const id = getRandomId(); const ref = createRef(); if (!isContainer && containerOrConfig?.autoClosePopovers) { - const openPopovers = ModalStore.getPopoversWithSameOpener(props.opener); - openPopovers.forEach((popover) => { - const popoverRef = popover.ref as MutableRefObject; - if (popoverRef.current) { - popoverRef.current.open = false; - } - }); + autoClose(props); } ModalStore.addModal({ Component: Popover, From 8b3f6ba56e468be19cd13c36f6544c3efb1b2275 Mon Sep 17 00:00:00 2001 From: Lukas Harbarth Date: Wed, 28 Jan 2026 11:19:59 +0100 Subject: [PATCH 3/7] overload all show methods --- packages/main/src/components/Modals/index.tsx | 53 ++++++++++++++++--- 1 file changed, 46 insertions(+), 7 deletions(-) diff --git a/packages/main/src/components/Modals/index.tsx b/packages/main/src/components/Modals/index.tsx index 4642c691762..2aa4317934f 100644 --- a/packages/main/src/components/Modals/index.tsx +++ b/packages/main/src/components/Modals/index.tsx @@ -59,10 +59,16 @@ function autoClose(props: { opener?: PopoverPropTypes['opener'] }) { }); } +function showDialogFn(props: DialogPropTypes, config?: ModalConfig): ClosableModalReturnType; function showDialogFn( props: DialogPropTypes, container?: Element | DocumentFragment, +): ClosableModalReturnType; +function showDialogFn( + props: DialogPropTypes, + containerOrConfig?: Element | DocumentFragment | ModalConfig, ): ClosableModalReturnType { + const isContainer = containerOrConfig instanceof Element || containerOrConfig instanceof DocumentFragment; const id = getRandomId(); const ref = createRef(); ModalStore.addModal({ @@ -78,7 +84,7 @@ function showDialogFn( }, }, ref, - container, + container: isContainer ? containerOrConfig : containerOrConfig?.container, id, }); @@ -134,12 +140,24 @@ function showPopoverFn( }; } +function showResponsivePopoverFn( + props: ResponsivePopoverPropTypes, + config?: ModalConfigPopover, +): ClosableModalReturnType; function showResponsivePopoverFn( props: ResponsivePopoverPropTypes, container?: Element | DocumentFragment, +): ClosableModalReturnType; +function showResponsivePopoverFn( + props: ResponsivePopoverPropTypes, + containerOrConfig?: Element | DocumentFragment | ModalConfigPopover, ): ClosableModalReturnType { + const isContainer = containerOrConfig instanceof Element || containerOrConfig instanceof DocumentFragment; const id = getRandomId(); const ref = createRef(); + if (!isContainer && containerOrConfig?.autoClosePopovers) { + autoClose(props); + } ModalStore.addModal({ Component: ResponsivePopover, props: { @@ -153,7 +171,7 @@ function showResponsivePopoverFn( }, }, ref, - container, + container: isContainer ? containerOrConfig : containerOrConfig?.container, id, }); @@ -167,9 +185,18 @@ function showResponsivePopoverFn( }; } -function showMenuFn(props: MenuPropTypes, container?: Element | DocumentFragment): ClosableModalReturnType { +function showMenuFn(props: MenuPropTypes, config?: ModalConfigPopover): ClosableModalReturnType; +function showMenuFn(props: MenuPropTypes, container?: Element | DocumentFragment): ClosableModalReturnType; +function showMenuFn( + props: MenuPropTypes, + containerOrConfig?: Element | DocumentFragment | ModalConfigPopover, +): ClosableModalReturnType { + const isContainer = containerOrConfig instanceof Element || containerOrConfig instanceof DocumentFragment; const id = getRandomId(); const ref = createRef(); + if (!isContainer && containerOrConfig?.autoClosePopovers) { + autoClose(props); + } ModalStore.addModal({ Component: Menu, props: { @@ -183,7 +210,7 @@ function showMenuFn(props: MenuPropTypes, container?: Element | DocumentFragment }, }, ref, - container, + container: isContainer ? containerOrConfig : containerOrConfig?.container, id, }); @@ -197,10 +224,16 @@ function showMenuFn(props: MenuPropTypes, container?: Element | DocumentFragment }; } +function showMessageBoxFn(props: MessageBoxPropTypes, config?: ModalConfig): ClosableModalReturnType; function showMessageBoxFn( props: MessageBoxPropTypes, container?: Element | DocumentFragment, +): ClosableModalReturnType; +function showMessageBoxFn( + props: MessageBoxPropTypes, + containerOrConfig?: Element | DocumentFragment | ModalConfig, ): ClosableModalReturnType { + const isContainer = containerOrConfig instanceof Element || containerOrConfig instanceof DocumentFragment; const id = getRandomId(); const ref = createRef(); ModalStore.addModal({ @@ -217,7 +250,7 @@ function showMessageBoxFn( }, }, ref, - container, + container: isContainer ? containerOrConfig : containerOrConfig?.container, id, }); @@ -231,7 +264,13 @@ function showMessageBoxFn( }; } -function showToastFn(props: ToastPropTypes, container?: Element | DocumentFragment): ModalReturnType { +function showToastFn(props: ToastPropTypes, config?: ModalConfig): ModalReturnType; +function showToastFn(props: ToastPropTypes, container?: Element | DocumentFragment): ModalReturnType; +function showToastFn( + props: ToastPropTypes, + containerOrConfig?: Element | DocumentFragment | ModalConfig, +): ModalReturnType { + const isContainer = containerOrConfig instanceof Element || containerOrConfig instanceof DocumentFragment; const ref = createRef(); const id = getRandomId(); ModalStore.addModal({ @@ -247,7 +286,7 @@ function showToastFn(props: ToastPropTypes, container?: Element | DocumentFragme }, }, ref, - container, + container: isContainer ? containerOrConfig : containerOrConfig?.container, id, }); From 64fd9d770a972ece834038edc826a7950c7d2eba Mon Sep 17 00:00:00 2001 From: Lukas Harbarth Date: Wed, 28 Jan 2026 11:57:53 +0100 Subject: [PATCH 4/7] add tests --- .../main/src/components/Modals/Modals.cy.tsx | 103 +++++++++++++----- 1 file changed, 76 insertions(+), 27 deletions(-) diff --git a/packages/main/src/components/Modals/Modals.cy.tsx b/packages/main/src/components/Modals/Modals.cy.tsx index 9784606f268..6b4a6144d47 100644 --- a/packages/main/src/components/Modals/Modals.cy.tsx +++ b/packages/main/src/components/Modals/Modals.cy.tsx @@ -45,16 +45,33 @@ describe('Modals - static helpers', () => { > Show Popover +
+ ); + }; + cy.mount(); - cy.mount(); + cy.findByText('Show Popover').click(); + cy.findByText('Popover Content').should('be.visible'); + cy.findByText('Show Popover').click(); + cy.get('[ui5-popover]').should('have.length', 2); - cy.findByText('Show Popover').click(); - cy.findByText('Popover Content').should('be.visible'); - cy.findByText('Close').click(); - cy.findByText('Dialog Content').should('not.exist'); - }; + cy.findByText('Show Popover 2').click(); + cy.findByText('Popover 2').should('be.visible'); + cy.get('[ui5-popover]').should('have.length', 1); }); it('showResponsivePopover', () => { @@ -63,27 +80,43 @@ describe('Modals - static helpers', () => { <> } />, + Modals.showResponsivePopover({ + opener: 'modals-show-responsive-popover', + children: 'ResponsivePopover Content', }); }} > - Show Popover + Show ResponsivePopover + +
+ ); + }; + cy.mount(); - cy.mount(); + cy.findByText('Show ResponsivePopover').click(); + cy.findByText('ResponsivePopover Content').should('be.visible'); + cy.findByText('Show ResponsivePopover').click(); + cy.get('[ui5-responsive-popover]').should('have.length', 2); - cy.findByText('Show Popover').click(); - cy.findByText('Popover Content').should('be.visible'); - cy.findByText('Close').click(); - cy.findByText('Dialog Content').should('not.exist'); - }; + cy.findByText('Show ResponsivePopover 2').click(); + cy.findByText('ResponsivePopover 2').should('be.visible'); + cy.get('[ui5-responsive-popover]').should('have.length', 1); }); it('showMenu', () => { @@ -92,26 +125,42 @@ describe('Modals - static helpers', () => { <> + ); + }; + cy.mount(); - cy.mount(); + cy.findByText('Show Menu').click(); + cy.get('[ui5-menu-item][text="MenuItem 1"]').should('be.visible'); + cy.findByText('Show Menu').click(); + cy.get('[ui5-menu]').should('have.length', 2); - cy.findByText('Show Menu').click(); - cy.findByText('MenuItem').should('be.visible'); - cy.findByText('MenuItem').click(); - cy.findByText('MenuItem').should('not.exist'); - }; + cy.findByText('Show Menu 2').click(); + cy.get('[ui5-menu-item][text="MenuItem 2"]').should('be.visible'); + cy.get('[ui5-menu]').should('have.length', 1); }); it('showMessageBox', () => { From a9877660758ff6acedcb5e00e7a868eb894b36fc Mon Sep 17 00:00:00 2001 From: Lukas Harbarth Date: Wed, 28 Jan 2026 12:26:03 +0100 Subject: [PATCH 5/7] docs --- .../main/src/components/Modals/Modals.mdx | 121 ++++++++++++++---- packages/main/src/components/Modals/index.tsx | 24 ++++ 2 files changed, 119 insertions(+), 26 deletions(-) diff --git a/packages/main/src/components/Modals/Modals.mdx b/packages/main/src/components/Modals/Modals.mdx index 830b66ddb38..b9dfc4b0d72 100644 --- a/packages/main/src/components/Modals/Modals.mdx +++ b/packages/main/src/components/Modals/Modals.mdx @@ -39,15 +39,26 @@ root.render( ```typescript import { Modals } from '@ui5/webcomponents-react/Modals'; +// Recommended: using config object +const { ref, close } = Modals.showDialog(props, config); + +// Legacy: using container directly const { ref, close } = Modals.showDialog(props, container); ``` **Parameters** -| Parameter | Description | -| ------------- | ------------------------------------------------------------------------------------- | -| `props` | All supported `Dialog` props (see table below). `open` will always be set to `true`. | -| _`container`_ | Optional container where the `Dialog` should be mounted. Defaults to `document.body`. | +| Parameter | Description | +| ------------- | ------------------------------------------------------------------------------------------------------- | +| `props` | All supported `Dialog` props (see table below). `open` will always be set to `true`. | +| _`config`_ | Optional configuration object. See config options below. | +| _`container`_ | _(deprecated)_ Optional container where the `Dialog` should be mounted. Use `config.container` instead. | + +**Config Options** _(since 2.19.0)_ + +| Property | Description | +| ----------- | -------------------------------------------------------------------------------------- | +| `container` | Optional container where the component should be mounted. Defaults to `document.body`. | **Return Value** @@ -71,15 +82,27 @@ The `showDialog` method returns an object with the following properties: ```typescript import { Modals } from '@ui5/webcomponents-react/Modals'; +// Recommended: using config object +const { ref, close } = Modals.showPopover(props, config); + +// Legacy: using container directly const { ref, close } = Modals.showPopover(props, container); ``` **Parameters** -| Parameter | Description | -| ------------- | -------------------------------------------------------------------------------------- | -| `props` | All supported `Popover` props (see table below).`open` will always be set to `true`. | -| _`container`_ | Optional container where the `Popover` should be mounted. Defaults to `document.body`. | +| Parameter | Description | +| ------------- | -------------------------------------------------------------------------------------------------------- | +| `props` | All supported `Popover` props (see table below). `open` will always be set to `true`. | +| _`config`_ | Optional configuration object. See config options below. | +| _`container`_ | _(deprecated)_ Optional container where the `Popover` should be mounted. Use `config.container` instead. | + +**Config Options** _(since 2.19.0)_ + +| Property | Description | +| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `container` | Optional container where the component should be mounted. Defaults to `document.body`. | +| `autoClosePopovers` | If set to `true`, opening a new Popover will automatically close all currently opened Popovers that share the same opener. Only affects Popover, Menu, and ResponsivePopover. | **Return Value** @@ -103,15 +126,27 @@ The `showPopover` method returns an object with the following properties: ```typescript import { Modals } from '@ui5/webcomponents-react/Modals'; +// Recommended: using config object +const { ref, close } = Modals.showResponsivePopover(props, config); + +// Legacy: using container directly const { ref, close } = Modals.showResponsivePopover(props, container); ``` **Parameters** -| Parameter | Description | -| ------------- | ------------------------------------------------------------------------------------------------ | -| `props` | All supported `ResponsivePopover` props (see table below). `open` will always be set to `true`. | -| _`container`_ | Optional container where the `ResponsivePopover` should be mounted. Defaults to `document.body`. | +| Parameter | Description | +| ------------- | ------------------------------------------------------------------------------------------------------------------ | +| `props` | All supported `ResponsivePopover` props (see table below). `open` will always be set to `true`. | +| _`config`_ | Optional configuration object. See config options below. | +| _`container`_ | _(deprecated)_ Optional container where the `ResponsivePopover` should be mounted. Use `config.container` instead. | + +**Config Options** _(since 2.19.0)_ + +| Property | Description | +| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `container` | Optional container where the component should be mounted. Defaults to `document.body`. | +| `autoClosePopovers` | If set to `true`, opening a new Popover will automatically close all currently opened Popovers that share the same opener. Only affects Popover, Menu, and ResponsivePopover. | **Return Value** @@ -137,19 +172,31 @@ The `showResponsivePopover` method returns an object with the following properti ```typescript import { Modals } from '@ui5/webcomponents-react/Modals'; +// Recommended: using config object +const { ref, close } = Modals.showMenu(props, config); + +// Legacy: using container directly const { ref, close } = Modals.showMenu(props, container); ``` **Parameters** -| Parameter | Description | -| ------------- | ----------------------------------------------------------------------------------- | -| `props` | All supported `Menu` props (see table below). `open` will always be set to `true`. | -| _`container`_ | Optional container where the `Menu` should be mounted. Defaults to `document.body`. | +| Parameter | Description | +| ------------- | ----------------------------------------------------------------------------------------------------- | +| `props` | All supported `Menu` props (see table below). `open` will always be set to `true`. | +| _`config`_ | Optional configuration object. See config options below. | +| _`container`_ | _(deprecated)_ Optional container where the `Menu` should be mounted. Use `config.container` instead. | + +**Config Options** _(since 2.19.0)_ + +| Property | Description | +| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `container` | Optional container where the component should be mounted. Defaults to `document.body`. | +| `autoClosePopovers` | If set to `true`, opening a new Popover will automatically close all currently opened Popovers that share the same opener. Only affects Popover, Menu, and ResponsivePopover. | **Return Value** -The `Menu` method returns an object with the following properties: +The `showMenu` method returns an object with the following properties: | Property | Description | | --------- | ---------------------------------------------------------------- | @@ -169,15 +216,26 @@ The `Menu` method returns an object with the following properties: ```typescript import { Modals } from '@ui5/webcomponents-react/Modals'; +// Recommended: using config object +const { ref, close } = Modals.showMessageBox(props, config); + +// Legacy: using container directly const { ref, close } = Modals.showMessageBox(props, container); ``` **Parameters** -| Parameter | Description | -| ------------- | ----------------------------------------------------------------------------------------- | -| `props` | All supported `MessageBox` props (see table below). `open` will always be set to `true`. | -| _`container`_ | Optional container where the `MessageBox` should be mounted. Defaults to `document.body`. | +| Parameter | Description | +| ------------- | ----------------------------------------------------------------------------------------------------------- | +| `props` | All supported `MessageBox` props (see table below). `open` will always be set to `true`. | +| _`config`_ | Optional configuration object. See config options below. | +| _`container`_ | _(deprecated)_ Optional container where the `MessageBox` should be mounted. Use `config.container` instead. | + +**Config Options** _(since 2.19.0)_ + +| Property | Description | +| ----------- | -------------------------------------------------------------------------------------- | +| `container` | Optional container where the component should be mounted. Defaults to `document.body`. | **Return Value** @@ -201,19 +259,30 @@ The `showMessageBox` method returns an object with the following properties: ```typescript import { Modals } from '@ui5/webcomponents-react/Modals'; +// Recommended: using config object +const { ref } = Modals.showToast(props, config); + +// Legacy: using container directly const { ref } = Modals.showToast(props, container); ``` **Parameters** -| Parameter | Description | -| ------------- | ----------------------------------------------------------------------------------- | -| `props` | All supported `Toast` props(see table below). | -| _`container`_ | Optional container where the `Toast` should be mounted.Defaults to `document.body`. | +| Parameter | Description | +| ------------- | ------------------------------------------------------------------------------------------------------ | +| `props` | All supported `Toast` props (see table below). | +| _`config`_ | Optional configuration object. See config options below. | +| _`container`_ | _(deprecated)_ Optional container where the `Toast` should be mounted. Use `config.container` instead. | + +**Config Options** _(since 2.19.0)_ + +| Property | Description | +| ----------- | -------------------------------------------------------------------------------------- | +| `container` | Optional container where the component should be mounted. Defaults to `document.body`. | **Return Value** -The`showToast` method returns an object with the following properties: +The `showToast` method returns an object with the following properties: | Property | Description | | -------- | ----------------------------------------------------------------- | diff --git a/packages/main/src/components/Modals/index.tsx b/packages/main/src/components/Modals/index.tsx index 2aa4317934f..f2b4ef34220 100644 --- a/packages/main/src/components/Modals/index.tsx +++ b/packages/main/src/components/Modals/index.tsx @@ -21,6 +21,9 @@ import { Toast } from '../../webComponents/Toast/index.js'; import type { MessageBoxPropTypes } from '../MessageBox/index.js'; import { MessageBox } from '../MessageBox/index.js'; +/** + * @since 2.19.0 + */ interface ModalConfig { /** * Optional container where the component should be mounted. @@ -30,6 +33,9 @@ interface ModalConfig { container?: Element | DocumentFragment; } +/** + * @since 2.19.0 + */ interface ModalConfigPopover extends ModalConfig { /** * If set to `true`, opening a new Popover will automatically close all currently opened Popovers that share the __same opener__. @@ -60,6 +66,9 @@ function autoClose(props: { opener?: PopoverPropTypes['opener'] }) { } function showDialogFn(props: DialogPropTypes, config?: ModalConfig): ClosableModalReturnType; +/** + * @deprecated Passing `container` directly is deprecated. Use the `config` object with `config.container` instead. + */ function showDialogFn( props: DialogPropTypes, container?: Element | DocumentFragment, @@ -99,6 +108,9 @@ function showDialogFn( } function showPopoverFn(props: PopoverPropTypes, config?: ModalConfigPopover): ClosableModalReturnType; +/** + * @deprecated Passing `container` directly is deprecated. Use the `config` object with `config.container` instead. + */ function showPopoverFn( props: PopoverPropTypes, container?: Element | DocumentFragment, @@ -144,6 +156,9 @@ function showResponsivePopoverFn( props: ResponsivePopoverPropTypes, config?: ModalConfigPopover, ): ClosableModalReturnType; +/** + * @deprecated Passing `container` directly is deprecated. Use the `config` object with `config.container` instead. + */ function showResponsivePopoverFn( props: ResponsivePopoverPropTypes, container?: Element | DocumentFragment, @@ -186,6 +201,9 @@ function showResponsivePopoverFn( } function showMenuFn(props: MenuPropTypes, config?: ModalConfigPopover): ClosableModalReturnType; +/** + * @deprecated Passing `container` directly is deprecated. Use the `config` object with `config.container` instead. + */ function showMenuFn(props: MenuPropTypes, container?: Element | DocumentFragment): ClosableModalReturnType; function showMenuFn( props: MenuPropTypes, @@ -225,6 +243,9 @@ function showMenuFn( } function showMessageBoxFn(props: MessageBoxPropTypes, config?: ModalConfig): ClosableModalReturnType; +/** + * @deprecated Passing `container` directly is deprecated. Use the `config` object with `config.container` instead. + */ function showMessageBoxFn( props: MessageBoxPropTypes, container?: Element | DocumentFragment, @@ -265,6 +286,9 @@ function showMessageBoxFn( } function showToastFn(props: ToastPropTypes, config?: ModalConfig): ModalReturnType; +/** + * @deprecated Passing `container` directly is deprecated. Use the `config` object with `config.container` instead. + */ function showToastFn(props: ToastPropTypes, container?: Element | DocumentFragment): ModalReturnType; function showToastFn( props: ToastPropTypes, From 6717c3047b5ee1595ce29840246156368ed0817d Mon Sep 17 00:00:00 2001 From: Lukas Harbarth Date: Wed, 28 Jan 2026 13:38:30 +0100 Subject: [PATCH 6/7] Update Modals.stories.tsx --- .../src/components/Modals/Modals.stories.tsx | 83 ++++++++++++------- 1 file changed, 54 insertions(+), 29 deletions(-) diff --git a/packages/main/src/components/Modals/Modals.stories.tsx b/packages/main/src/components/Modals/Modals.stories.tsx index e8becf2e388..3a7a63744c7 100644 --- a/packages/main/src/components/Modals/Modals.stories.tsx +++ b/packages/main/src/components/Modals/Modals.stories.tsx @@ -1,7 +1,8 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; -import { FlexBox, FlexBoxJustifyContent } from '@ui5/webcomponents-react'; -import { MessageBoxType } from '../../enums/index.js'; -import { Button, MenuItem } from '../../webComponents/index.js'; +import { useState } from 'react'; +import { FlexBoxDirection, FlexBoxJustifyContent, MessageBoxType } from '../../enums/index.js'; +import { Button, Label, MenuItem, Switch } from '../../webComponents/index.js'; +import { FlexBox } from '../FlexBox/index.js'; import { Modals } from './index.js'; const meta = { @@ -38,68 +39,92 @@ export const Dialog: Story = { export const Popover = { render: () => { + const [autoClosePopovers, setAutoClosePopovers] = useState(false); return ( - <> + + + setAutoClosePopovers(e.target.checked)} /> + + - + ); }, }; export const ResponsivePopover = { render: () => { + const [autoClosePopovers, setAutoClosePopovers] = useState(false); return ( - <> + + + setAutoClosePopovers(e.target.checked)} /> + + - + ); }, }; export const Menu = { render: () => { + const [autoClosePopovers, setAutoClosePopovers] = useState(false); return ( - <> + + + setAutoClosePopovers(e.target.checked)} /> + + - + ); }, }; From d15d4e09f4d85235d804bd0f98bac1b0b1a39d7a Mon Sep 17 00:00:00 2001 From: Lukas Harbarth Date: Fri, 30 Jan 2026 13:25:04 +0100 Subject: [PATCH 7/7] cleanup modals after unmount --- packages/main/src/components/Modals/index.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/main/src/components/Modals/index.tsx b/packages/main/src/components/Modals/index.tsx index f2b4ef34220..c783df457e7 100644 --- a/packages/main/src/components/Modals/index.tsx +++ b/packages/main/src/components/Modals/index.tsx @@ -1,7 +1,7 @@ 'use client'; import type { MutableRefObject } from 'react'; -import { createRef, useSyncExternalStore } from 'react'; +import { createRef, useEffect, useRef, useSyncExternalStore } from 'react'; import { createPortal } from 'react-dom'; import { getRandomId } from '../../internal/getRandomId.js'; import { ModalStore } from '../../internal/ModalStore.js'; @@ -331,6 +331,16 @@ function showToastFn( */ export function Modals() { const modals = useSyncExternalStore(ModalStore.subscribe, ModalStore.getSnapshot, ModalStore.getServerSnapshot); + const modalsRef = useRef(modals); + modalsRef.current = modals; + + useEffect(() => { + return () => { + modalsRef.current.forEach((modal) => { + ModalStore.removeModal(modal.id); + }); + }; + }, []); return ( <>