import Component from '../Component'; import { createFocusTrap, FocusTrap } from '../utils/focusTrap'; import type ModalManagerState from '../states/ModalManagerState'; import type Mithril from 'mithril'; interface IModalManagerAttrs { state: ModalManagerState; } /** * The `ModalManager` component manages a modal dialog. Only one modal dialog * can be shown at once; loading a new component into the ModalManager will * overwrite the previous one. */ export default class ModalManager extends Component { protected focusTrap: FocusTrap | undefined; /** * Whether a modal is currently shown by this modal manager. */ protected modalShown: boolean = false; view(vnode: Mithril.VnodeDOM): Mithril.Children { const modal = this.attrs.state.modal; const Tag = modal?.componentClass; return (
{!!Tag && ( )}
); } oncreate(vnode: Mithril.VnodeDOM): void { super.oncreate(vnode); // Ensure the modal state is notified about a closed modal, even when the // DOM-based Bootstrap JavaScript code triggered the closing of the modal, // e.g. via ESC key or a click on the modal backdrop. this.$().on('hidden.bs.modal', this.attrs.state.close.bind(this.attrs.state)); this.focusTrap = createFocusTrap(this.element as HTMLElement); } onupdate(vnode: Mithril.VnodeDOM): void { super.onupdate(vnode); requestAnimationFrame(() => { try { if (this.modalShown) this.focusTrap!.activate?.(); else this.focusTrap!.deactivate?.(); } catch { // We can expect errors to occur here due to the nature of mithril rendering } }); } animateShow(readyCallback: () => void): void { if (!this.attrs.state.modal) return; const dismissible = !!this.attrs.state.modal.componentClass.isDismissible; this.modalShown = true; // If we are opening this modal while another modal is already open, // the shown event will not run, because the modal is already open. // So, we need to manually trigger the readyCallback. if (this.$().hasClass('in')) { readyCallback(); return; } this.$() .one('shown.bs.modal', readyCallback) // @ts-expect-error: No typings available for Bootstrap modals. .modal({ backdrop: dismissible || 'static', keyboard: dismissible, }) .modal('show'); } animateHide(): void { // @ts-expect-error: No typings available for Bootstrap modals. this.$().modal('hide'); this.modalShown = false; } }