mirror of
https://github.com/flarum/core.git
synced 2025-10-27 21:46:17 +01:00
We need to specify a unique key for each modal so that the modals are fully destroyed and recreated. For instance, this fixes the signup modal being empty with OAuth register flows.
100 lines
2.9 KiB
TypeScript
100 lines
2.9 KiB
TypeScript
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<IModalManagerAttrs> {
|
|
protected focusTrap: FocusTrap | undefined;
|
|
|
|
/**
|
|
* Whether a modal is currently shown by this modal manager.
|
|
*/
|
|
protected modalShown: boolean = false;
|
|
|
|
view(vnode: Mithril.VnodeDOM<IModalManagerAttrs, this>): Mithril.Children {
|
|
const modal = this.attrs.state.modal;
|
|
const Tag = modal?.componentClass;
|
|
|
|
return (
|
|
<div className="ModalManager modal fade">
|
|
{!!Tag && (
|
|
<Tag
|
|
key={modal?.key}
|
|
{...modal.attrs}
|
|
animateShow={this.animateShow.bind(this)}
|
|
animateHide={this.animateHide.bind(this)}
|
|
state={this.attrs.state}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
oncreate(vnode: Mithril.VnodeDOM<IModalManagerAttrs, this>): 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<IModalManagerAttrs, this>): 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;
|
|
}
|
|
}
|