diff --git a/.changeset/sharp-ladybugs-clap.md b/.changeset/sharp-ladybugs-clap.md
new file mode 100644
index 00000000000..53aed58ebcd
--- /dev/null
+++ b/.changeset/sharp-ladybugs-clap.md
@@ -0,0 +1,7 @@
+---
+"@siemens/ix-angular": patch
+"@siemens/ix-react": patch
+"@siemens/ix-vue": patch
+---
+
+Add dismissModal function to allow programmatic closing of **ix-modal**s.
diff --git a/packages/angular-standalone-test-app/src/app/app.routes.ts b/packages/angular-standalone-test-app/src/app/app.routes.ts
index 92c115c6cbb..9dd52db63e6 100644
--- a/packages/angular-standalone-test-app/src/app/app.routes.ts
+++ b/packages/angular-standalone-test-app/src/app/app.routes.ts
@@ -947,6 +947,11 @@ export const routes: Routes = [
(m) => m.default
),
},
+ {
+ path: 'modal-close',
+ loadComponent: () =>
+ import('../preview-examples/modal-close').then((m) => m.default),
+ },
{
path: 'modal-by-template',
loadComponent: () =>
diff --git a/packages/angular-standalone-test-app/src/preview-examples/modal-close.ts b/packages/angular-standalone-test-app/src/preview-examples/modal-close.ts
new file mode 100644
index 00000000000..ba77b2c80d0
--- /dev/null
+++ b/packages/angular-standalone-test-app/src/preview-examples/modal-close.ts
@@ -0,0 +1,35 @@
+/*
+ * SPDX-FileCopyrightText: 2025 Siemens AG
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import { Component } from '@angular/core';
+import { IxButton } from '@siemens/ix-angular/standalone';
+
+import { ModalService } from '@siemens/ix-angular';
+import ModalByInstanceExample from './modal-by-instance-content';
+
+@Component({
+ standalone: true,
+ selector: 'app-example',
+ imports: [IxButton],
+ template:
+ 'Show modal (auto-dismiss)',
+})
+export default class ModalClose {
+ constructor(private readonly modalService: ModalService) {}
+
+ async openModal() {
+ const modalInstance = await this.modalService.open({
+ content: ModalByInstanceExample,
+ data: 'Some data',
+ });
+ setTimeout(() => {
+ this.modalService.close(modalInstance, 'closed after 5 seconds');
+ }, 5000);
+ }
+}
diff --git a/packages/angular-test-app/src/app/app-routing.module.ts b/packages/angular-test-app/src/app/app-routing.module.ts
index 49b4c9bda8e..b7e1bf5e3f8 100644
--- a/packages/angular-test-app/src/app/app-routing.module.ts
+++ b/packages/angular-test-app/src/app/app-routing.module.ts
@@ -162,6 +162,7 @@ import Message from '../preview-examples/message';
import MessageBar from '../preview-examples/message-bar';
import MessageBarRemoval from '../preview-examples/message-bar-removal';
import ModalByInstance from '../preview-examples/modal-by-instance';
+import ModalClose from '../preview-examples/modal-close';
import ModalByInstanceContent from '../preview-examples/modal-by-instance-content';
import ModalByTemplate from '../preview-examples/modal-by-template';
import ModalFormIxButtonSubmit from '../preview-examples/modal-form-ix-button-submit';
@@ -777,6 +778,10 @@ const routes: Routes = [
path: 'modal-by-instance',
component: ModalByInstance,
},
+ {
+ path: 'modal-close',
+ component: ModalClose,
+ },
{
path: 'modal-by-template',
component: ModalByTemplate,
diff --git a/packages/angular-test-app/src/app/app.module.ts b/packages/angular-test-app/src/app/app.module.ts
index 2ee1ed16b30..bd5b651a6fb 100644
--- a/packages/angular-test-app/src/app/app.module.ts
+++ b/packages/angular-test-app/src/app/app.module.ts
@@ -173,6 +173,7 @@ import MessageBarRemoval from '../preview-examples/message-bar-removal';
import ModalByInstance from '../preview-examples/modal-by-instance';
import ModalByInstanceContent from '../preview-examples/modal-by-instance-content';
import ModalByTemplate from '../preview-examples/modal-by-template';
+import ModalClose from '../preview-examples/modal-close';
import ModalFormIxButtonSubmit from '../preview-examples/modal-form-ix-button-submit';
import ModalSizes from '../preview-examples/modal-sizes';
import NumberInput from '../preview-examples/number-input';
@@ -400,6 +401,7 @@ import WorkflowVertical from '../preview-examples/workflow-vertical';
ModalByInstanceContent,
ModalByInstance,
ModalByTemplate,
+ ModalClose,
ModalFormIxButtonSubmit,
ModalSizes,
PaginationAdvanced,
diff --git a/packages/angular-test-app/src/preview-examples/modal-close.ts b/packages/angular-test-app/src/preview-examples/modal-close.ts
new file mode 100644
index 00000000000..4c2199731ea
--- /dev/null
+++ b/packages/angular-test-app/src/preview-examples/modal-close.ts
@@ -0,0 +1,32 @@
+/*
+ * SPDX-FileCopyrightText: 2025 Siemens AG
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import { Component } from '@angular/core';
+import { ModalService } from '@siemens/ix-angular';
+import ModalByInstanceExample from './modal-by-instance-content';
+
+@Component({
+ standalone: false,
+ selector: 'app-example',
+ template:
+ 'Show modal (auto-dismiss)',
+})
+export default class ModalClose {
+ constructor(private readonly modalService: ModalService) {}
+
+ async openModal() {
+ const modalInstance = await this.modalService.open({
+ content: ModalByInstanceExample,
+ data: 'Some data',
+ });
+ setTimeout(() => {
+ this.modalService.close(modalInstance, 'closed after 5 seconds');
+ }, 5000);
+ }
+}
diff --git a/packages/angular/common/src/providers/modal/modal.service.spec.ts b/packages/angular/common/src/providers/modal/modal.service.spec.ts
index 560bb5b523b..dbe8a6914ac 100644
--- a/packages/angular/common/src/providers/modal/modal.service.spec.ts
+++ b/packages/angular/common/src/providers/modal/modal.service.spec.ts
@@ -103,3 +103,37 @@ test('should create modal by component typ', async () => {
modalElement: { type: 'html-element' },
});
});
+
+test('should close modal instance with reason and mark as closed', () => {
+ const closeModalMock = jest.fn(function (
+ this: { closed?: boolean },
+ reason?: any
+ ) {
+ if (typeof reason !== 'undefined') {
+ this.closed = true;
+ }
+ });
+ const instance = {
+ htmlElement: {
+ closeModal: closeModalMock,
+ closed: false,
+ },
+ };
+ const modalService = new ModalService({} as any, {} as any, {} as any);
+ const reason = 'user-dismissed';
+ modalService.close(instance, reason);
+ expect(closeModalMock).toHaveBeenCalledWith(reason);
+ expect(instance.htmlElement.closed).toBe(true);
+});
+
+test('should throw TypeError if instance cannot be closed', () => {
+ const instance = {
+ htmlElement: {},
+ };
+ const modalService = new ModalService({} as any, {} as any, {} as any);
+
+ expect(() => modalService.close(instance)).toThrow(TypeError);
+ expect(() => modalService.close(instance)).toThrow(
+ 'Invalid modal instance: cannot close'
+ );
+});
diff --git a/packages/angular/common/src/providers/modal/modal.service.ts b/packages/angular/common/src/providers/modal/modal.service.ts
index c129d7fbef9..fb315a93242 100644
--- a/packages/angular/common/src/providers/modal/modal.service.ts
+++ b/packages/angular/common/src/providers/modal/modal.service.ts
@@ -36,6 +36,17 @@ export class ModalService {
private injector: Injector
) {}
+ public close(
+ instance: { htmlElement: any },
+ reason?: TReason
+ ): void {
+ if (typeof instance?.htmlElement?.closeModal === 'function') {
+ instance.htmlElement.closeModal(reason);
+ } else {
+ throw new TypeError('Invalid modal instance: cannot close');
+ }
+ }
+
public async open(config: ModalConfig) {
const context: ModalContext = {
close: null,
diff --git a/packages/angular/src/providers/modal/modal.service.ts b/packages/angular/src/providers/modal/modal.service.ts
index b7dc73eb9d1..6ba79396f92 100644
--- a/packages/angular/src/providers/modal/modal.service.ts
+++ b/packages/angular/src/providers/modal/modal.service.ts
@@ -41,4 +41,11 @@ export class ModalService extends BaseModalService {
): Promise> {
return super.open(config);
}
+
+ public close(
+ instance: ModalInstance,
+ reason?: TReason
+ ): void {
+ super.close(instance, reason);
+ }
}
diff --git a/packages/angular/standalone/src/providers/modal.ts b/packages/angular/standalone/src/providers/modal.ts
index 3e92b6270b0..2df8450e326 100644
--- a/packages/angular/standalone/src/providers/modal.ts
+++ b/packages/angular/standalone/src/providers/modal.ts
@@ -37,4 +37,11 @@ export class ModalService extends BaseModalService {
defineCustomElement();
return super.open(config);
}
+
+ public close(
+ instance: ModalInstance,
+ reason?: TReason
+ ): void {
+ super.close(instance, reason);
+ }
}
diff --git a/packages/html-test-app/src/preview-examples/modal-close.html b/packages/html-test-app/src/preview-examples/modal-close.html
new file mode 100644
index 00000000000..a15a597fb24
--- /dev/null
+++ b/packages/html-test-app/src/preview-examples/modal-close.html
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
+ Modal example
+
+
+ Show modal (auto-dismiss)
+
+ Message headline
+ Message text lorem ipsum
+
+ Cancel
+ OK
+
+
+
+
+
+
+
diff --git a/packages/react-test-app/src/main.tsx b/packages/react-test-app/src/main.tsx
index 1f79c6c63df..67ddcdc3be0 100644
--- a/packages/react-test-app/src/main.tsx
+++ b/packages/react-test-app/src/main.tsx
@@ -161,6 +161,7 @@ import Message from './preview-examples/message';
import MessageBar from './preview-examples/message-bar';
import MessageBarRemoval from './preview-examples/message-bar-removal.tsx';
import Modal from './preview-examples/modal';
+import ModalClose from './preview-examples/modal-close.tsx';
import ModalFormIxButtonSubmit from './preview-examples/modal-form-ix-button-submit.tsx';
import ModalSizes from './preview-examples/modal-sizes';
import NumberInput from './preview-examples/number-input';
@@ -597,6 +598,7 @@ ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
/>
} />
+ } />
}
diff --git a/packages/react-test-app/src/preview-examples/modal-close.tsx b/packages/react-test-app/src/preview-examples/modal-close.tsx
new file mode 100644
index 00000000000..eb94dc301ac
--- /dev/null
+++ b/packages/react-test-app/src/preview-examples/modal-close.tsx
@@ -0,0 +1,63 @@
+/*
+ * SPDX-FileCopyrightText: 2025 Siemens AG
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import {
+ IxButton,
+ IxModalContent,
+ IxModalFooter,
+ IxModalHeader,
+ Modal,
+ ModalRef,
+ showModal,
+ dismissModal,
+} from '@siemens/ix-react';
+import { useRef } from 'react';
+
+function CustomModal() {
+ const modalRef = useRef(null);
+
+ const close = () => {
+ modalRef.current?.close('close payload!');
+ };
+ const dismiss = () => {
+ modalRef.current?.dismiss('dismiss payload');
+ };
+
+ return (
+
+ dismiss()}>
+ Message headline
+
+ Message text lorem ipsum
+
+ dismiss()}>
+ Cancel
+
+ close()}>OK
+
+
+ );
+}
+
+export default () => {
+ async function show() {
+ const modalInstance = await showModal({
+ content: ,
+ });
+ setTimeout(() => {
+ dismissModal(modalInstance);
+ }, 5000);
+ }
+
+ return (
+ <>
+ Show modal (auto-dismiss)
+ >
+ );
+};
diff --git a/packages/react/src/modal/index.ts b/packages/react/src/modal/index.ts
index 6d2303aef71..a77fdc833f6 100644
--- a/packages/react/src/modal/index.ts
+++ b/packages/react/src/modal/index.ts
@@ -10,6 +10,9 @@
import {
ModalConfig as IxModalConfig,
showModal as _showModal,
+ dismissModal as _dismissModal,
+ closeModal as _closeModal,
+ ModalInstance as IxModalInstance,
} from '@siemens/ix';
export * from './modal';
@@ -22,3 +25,18 @@ export async function showModal(
) {
return _showModal(config);
}
+
+export function dismissModal(modalInstance: IxModalInstance) {
+ if (modalInstance?.htmlElement) {
+ _dismissModal(modalInstance.htmlElement);
+ }
+}
+
+export function closeModal(
+ modalInstance: IxModalInstance,
+ reason?: T
+) {
+ if (modalInstance?.htmlElement) {
+ _closeModal(modalInstance.htmlElement, reason);
+ }
+}
diff --git a/packages/vue-test-app/src/Root.vue b/packages/vue-test-app/src/Root.vue
index a1b83a40123..33a55166407 100644
--- a/packages/vue-test-app/src/Root.vue
+++ b/packages/vue-test-app/src/Root.vue
@@ -164,6 +164,7 @@ import MessageBarRemoval from './preview-examples/message-bar-removal.vue';
import Message from './preview-examples/message.vue';
import ModalSizes from './preview-examples/modal-sizes.vue';
import ModalExample from './preview-examples/modal.vue';
+import ModalClose from './preview-examples/modal-close.vue';
import NumberInputDisabled from './preview-examples/number-input-disabled.vue';
import NumberInputLabel from './preview-examples/number-input-label.vue';
import NumberInputReadOnly from './preview-examples/number-input-readonly.vue';
@@ -404,6 +405,7 @@ const routes: any = {
'/preview/settings': Settings,
'/preview/kpi': Kpi,
'/preview/modal': ModalExample,
+ '/preview/modal-close': ModalClose,
'/preview/modal-form-ix-button-submit': ModalFormIxButtonSubmit,
'/preview/number-input': NumberInput,
'/preview/number-input-disabled': NumberInputDisabled,
diff --git a/packages/vue-test-app/src/preview-examples/modal-close.vue b/packages/vue-test-app/src/preview-examples/modal-close.vue
new file mode 100644
index 00000000000..bdec6f3e466
--- /dev/null
+++ b/packages/vue-test-app/src/preview-examples/modal-close.vue
@@ -0,0 +1,48 @@
+
+
+
+
+
+ Show modal (auto-dismiss)
+
diff --git a/packages/vue/src/modal/index.ts b/packages/vue/src/modal/index.ts
index 8dbf931be0d..18f5bf6511a 100644
--- a/packages/vue/src/modal/index.ts
+++ b/packages/vue/src/modal/index.ts
@@ -1,6 +1,9 @@
import {
ModalConfig as IxModalConfig,
showModal as _showModal,
+ dismissModal as _dismissModal,
+ closeModal as _closeModal,
+ ModalInstance as IxModalInstance,
} from '@siemens/ix';
import { VNode } from 'vue';
@@ -18,3 +21,18 @@ export async function showModal(
) {
return _showModal(config);
}
+
+export function dismissModal(modalInstance: IxModalInstance) {
+ if (modalInstance?.htmlElement) {
+ _dismissModal(modalInstance.htmlElement);
+ }
+}
+
+export function closeModal(
+ modalInstance: IxModalInstance,
+ reason?: T
+) {
+ if (modalInstance?.htmlElement) {
+ _closeModal(modalInstance.htmlElement, reason);
+ }
+}