Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/sharp-ladybugs-clap.md
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 5 additions & 0 deletions packages/angular-standalone-test-app/src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: () =>
Expand Down
Original file line number Diff line number Diff line change
@@ -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:
'<ix-button (click)="openModal()">Show modal (auto-dismiss)</ix-button>',
})
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);
}
}
5 changes: 5 additions & 0 deletions packages/angular-test-app/src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -777,6 +778,10 @@ const routes: Routes = [
path: 'modal-by-instance',
component: ModalByInstance,
},
{
path: 'modal-close',
component: ModalClose,
},
{
path: 'modal-by-template',
component: ModalByTemplate,
Expand Down
2 changes: 2 additions & 0 deletions packages/angular-test-app/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -400,6 +401,7 @@ import WorkflowVertical from '../preview-examples/workflow-vertical';
ModalByInstanceContent,
ModalByInstance,
ModalByTemplate,
ModalClose,
ModalFormIxButtonSubmit,
ModalSizes,
PaginationAdvanced,
Expand Down
32 changes: 32 additions & 0 deletions packages/angular-test-app/src/preview-examples/modal-close.ts
Original file line number Diff line number Diff line change
@@ -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:
'<ix-button (click)="openModal()">Show modal (auto-dismiss)</ix-button>',
})
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);
}
}
34 changes: 34 additions & 0 deletions packages/angular/common/src/providers/modal/modal.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
);
});
11 changes: 11 additions & 0 deletions packages/angular/common/src/providers/modal/modal.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,17 @@ export class ModalService {
private injector: Injector
) {}

public close<TReason = any>(
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<TData = any, TReason = any>(config: ModalConfig<TData>) {
const context: ModalContext<TData> = {
close: null,
Expand Down
7 changes: 7 additions & 0 deletions packages/angular/src/providers/modal/modal.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,11 @@ export class ModalService extends BaseModalService {
): Promise<ModalInstance<TReason>> {
return super.open(config);
}

public close<TReason = any>(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this really needed? Will be inherited from base ModalService, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have implemented it similar to the open service, should changes be made there too?

instance: ModalInstance<TReason>,
reason?: TReason
): void {
super.close(instance, reason);
}
}
7 changes: 7 additions & 0 deletions packages/angular/standalone/src/providers/modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,11 @@ export class ModalService extends BaseModalService {
defineCustomElement();
return super.open(config);
}

public close<TReason = any>(
instance: ModalInstance<TReason>,
reason?: TReason
): void {
super.close(instance, reason);
}
}
96 changes: 96 additions & 0 deletions packages/html-test-app/src/preview-examples/modal-close.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<!--
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.
-->

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Modal example</title>
</head>
<body>
<ix-button>Show modal (auto-dismiss)</ix-button>
<template id="modal-example-template">
<ix-modal-header>Message headline</ix-modal-header>
<ix-modal-content>Message text lorem ipsum</ix-modal-content>
<ix-modal-footer>
<ix-button outline data-cancel>Cancel</ix-button>
<ix-button data-okay>OK</ix-button>
</ix-modal-footer>
</template>

<script type="module">
import { showModal, closeModal, dismissModal } from '@siemens/ix';

function createExampleModal() {
const name = 'modal-example';
window.customElements.define(
name,
class extends HTMLElement {
isInitalRender = false;

constructor() {
super();
}

connectedCallback() {
if (this.isInitalRender) {
return;
}

this.isInitalRender = true;
this.firstRender();
}

firstRender() {
const modalTemplate = document.getElementById(
'modal-example-template'
);
const template = modalTemplate.content.cloneNode(true);

const cancelButton = template.querySelector('[data-cancel]');
const okayButton = template.querySelector('[data-okay]');

cancelButton.addEventListener('click', () => {
dismissModal(this);
});
okayButton.addEventListener('click', () => {
closeModal(this);
});

this.append(template);
}
}
);

return name;
}

(async function () {
const exampleModalName = createExampleModal();

await window.customElements.whenDefined('ix-button');
const button = document.querySelector('ix-button');

button.addEventListener('click', async () => {
const customModal = document.createElement(exampleModalName);

const modal = await showModal({
content: customModal,
});

setTimeout(() => {
closeModal(customModal, 'closed after 5 seconds');
}, 5000);
});
})();
</script>
<script type="module" src="./init.js"></script>
</body>
</html>
2 changes: 2 additions & 0 deletions packages/react-test-app/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -597,6 +598,7 @@ ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
/>

<Route path="/preview/modal" element={<Modal />} />
<Route path="/preview/modal-close" element={<ModalClose />} />
<Route
path="/preview/pagination-advanced"
element={<PaginationAdvanced />}
Expand Down
63 changes: 63 additions & 0 deletions packages/react-test-app/src/preview-examples/modal-close.tsx
Original file line number Diff line number Diff line change
@@ -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<ModalRef>(null);

const close = () => {
modalRef.current?.close('close payload!');
};
const dismiss = () => {
modalRef.current?.dismiss('dismiss payload');
};

return (
<Modal ref={modalRef}>
<IxModalHeader onCloseClick={() => dismiss()}>
Message headline
</IxModalHeader>
<IxModalContent>Message text lorem ipsum</IxModalContent>
<IxModalFooter>
<IxButton outline onClick={() => dismiss()}>
Cancel
</IxButton>
<IxButton onClick={() => close()}>OK</IxButton>
</IxModalFooter>
</Modal>
);
}

export default () => {
async function show() {
const modalInstance = await showModal({
content: <CustomModal />,
});
setTimeout(() => {
dismissModal(modalInstance);
}, 5000);
}

return (
<>
<IxButton onClick={show}>Show modal (auto-dismiss)</IxButton>
</>
);
};
Loading
Loading