Files
rio/frontend/code/components/dialogContainer.ts
2025-01-31 00:46:43 +01:00

144 lines
4.7 KiB
TypeScript

import {
componentsById,
recursivelyDeleteComponent,
} from "../componentManagement";
import { ComponentId } from "../dataModels";
import { FullscreenPositioner, PopupManager } from "../popupManager";
import { callRemoteMethodDiscardResponse } from "../rpc";
import { ComponentBase, ComponentState } from "./componentBase";
export type DialogContainerState = ComponentState & {
_type_: "DialogContainer-builtin";
content?: ComponentId;
owning_component_id?: ComponentId;
is_modal?: boolean;
is_user_closable?: boolean;
};
export class DialogContainerComponent extends ComponentBase {
declare state: Required<DialogContainerState>;
private contentContainer: HTMLElement;
// Dialogs are displayed via a popup manager. While this isn't strictly
// necessary, this allows sharing the code for whether the dialog is modal,
// user-closable and general styling.
private popupManager: PopupManager;
// Used to restore the keyboard focus when the dialog is closed
private previouslyFocusedElement: Element | null;
createElement(): HTMLElement {
// Create the HTML elements
let element = document.createElement("div");
element.classList.add("rio-dialog-container");
this.contentContainer = document.createElement("div");
this.contentContainer.classList.add("rio-dialog-container-content");
// Set up the popup manager
this.popupManager = new PopupManager({
anchor: element,
content: this.contentContainer,
positioner: new FullscreenPositioner(),
modal: true,
userClosable: true,
onUserClose: this.onUserClose.bind(this),
});
// Open the popup manager once we're confident that all components have
// been created
requestAnimationFrame(() => {
this.previouslyFocusedElement = document.activeElement;
this.popupManager.isOpen = true;
});
return element;
}
onDestruction(): void {
// Chain up
super.onDestruction();
// Tell Python about it
callRemoteMethodDiscardResponse("dialogClosed", {
dialogRootComponentId: this.id,
});
// Rather than disappearing immediately, the dialog container would like
// to fade out its content. This doesn't work though, because the
// content is also deleted when the dialog container is. So create a
// copy of the container's HTML and animate that instead.
//
// Create the copy
let contentRootElement = this.contentContainer.firstElementChild!;
let phony = contentRootElement.cloneNode(true) as HTMLElement;
// Make sure it doesn't interfere with user inputs
phony.style.pointerEvents = "none";
phony.querySelectorAll("*").forEach((child) => {
(child as HTMLElement).style.pointerEvents = "none";
});
// Replace the content with the phony element
contentRootElement.remove();
this.contentContainer.appendChild(phony);
// Close the popup manager, thus starting the outgoing animation
this.popupManager.isOpen = false;
// Clean up after the animation is done
setTimeout(
() => {
this.popupManager.destroy();
},
// Make sure this matches or exceeds the CSS transition duration!
600
);
// Restore the keyboard focus
if (this.previouslyFocusedElement instanceof HTMLElement) {
this.previouslyFocusedElement.focus();
}
}
updateElement(
deltaState: DialogContainerState,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);
// Content
this.replaceOnlyChild(
latentComponents,
deltaState.content,
this.contentContainer
);
// Modal
if (deltaState.is_modal !== undefined) {
this.popupManager.modal = deltaState.is_modal;
}
// User closable
if (deltaState.is_user_closable !== undefined) {
this.popupManager.userClosable = deltaState.is_user_closable;
}
// Owning component
if (deltaState.owning_component_id !== undefined) {
let owningComponent =
componentsById[deltaState.owning_component_id]!;
owningComponent.registerChild(latentComponents, this);
}
}
private onUserClose(): void {
// Destroy the dialog container. This will trigger the destruction
// function above, thus informing Python and properly cleaning up.
recursivelyDeleteComponent(this);
}
}