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', () => {
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/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)} />
+
+
- >
+
);
},
};
diff --git a/packages/main/src/components/Modals/index.tsx b/packages/main/src/components/Modals/index.tsx
index 7e1b931f64d..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';
@@ -21,6 +21,32 @@ 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.
+ *
+ * @default `document.body`
+ */
+ 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__.
+ *
+ * __Note:__
+ * - This only affects `Popover`, `Menu`, and `ResponsivePopover`.
+ * - Only closes open Popovers with the __same opener__.
+ */
+ autoClosePopovers?: boolean;
+}
+
type ModalReturnType = {
ref: MutableRefObject;
};
@@ -29,10 +55,29 @@ 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, config?: ModalConfig): ClosableModalReturnType;
+/**
+ * @deprecated Passing `container` directly is deprecated. Use the `config` object with `config.container` instead.
+ */
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({
@@ -48,7 +93,7 @@ function showDialogFn(
},
},
ref,
- container,
+ container: isContainer ? containerOrConfig : containerOrConfig?.container,
id,
});
@@ -62,12 +107,24 @@ 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,
+): 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) {
+ autoClose(props);
+ }
ModalStore.addModal({
Component: Popover,
props: {
@@ -81,7 +138,7 @@ function showPopoverFn(
},
},
ref,
- container,
+ container: isContainer ? containerOrConfig : containerOrConfig?.container,
id,
});
@@ -95,12 +152,27 @@ function showPopoverFn(
};
}
+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,
+): 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: {
@@ -114,7 +186,7 @@ function showResponsivePopoverFn(
},
},
ref,
- container,
+ container: isContainer ? containerOrConfig : containerOrConfig?.container,
id,
});
@@ -128,9 +200,21 @@ function showResponsivePopoverFn(
};
}
-function showMenuFn(props: MenuPropTypes, container?: Element | DocumentFragment): ClosableModalReturnType {
+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,
+ 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: {
@@ -144,7 +228,7 @@ function showMenuFn(props: MenuPropTypes, container?: Element | DocumentFragment
},
},
ref,
- container,
+ container: isContainer ? containerOrConfig : containerOrConfig?.container,
id,
});
@@ -158,10 +242,19 @@ function showMenuFn(props: MenuPropTypes, container?: Element | DocumentFragment
};
}
+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,
+): 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({
@@ -178,7 +271,7 @@ function showMessageBoxFn(
},
},
ref,
- container,
+ container: isContainer ? containerOrConfig : containerOrConfig?.container,
id,
});
@@ -192,7 +285,16 @@ function showMessageBoxFn(
};
}
-function showToastFn(props: ToastPropTypes, container?: Element | DocumentFragment): ModalReturnType {
+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,
+ containerOrConfig?: Element | DocumentFragment | ModalConfig,
+): ModalReturnType {
+ const isContainer = containerOrConfig instanceof Element || containerOrConfig instanceof DocumentFragment;
const ref = createRef();
const id = getRandomId();
ModalStore.addModal({
@@ -208,7 +310,7 @@ function showToastFn(props: ToastPropTypes, container?: Element | DocumentFragme
},
},
ref,
- container,
+ container: isContainer ? containerOrConfig : containerOrConfig?.container,
id,
});
@@ -229,6 +331,16 @@ function showToastFn(props: ToastPropTypes, container?: Element | DocumentFragme
*/
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 (
<>
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);
+ },
};