presentation/widgets/overlay.widget.ts

presentation/widgets/overlay.widget.ts is a file in Bane's Lab Site. 88 lines of code and 14 definitions.

import {
    CLOSING_CLASS,
    ESCAPE_KEY,
    OVERLAY_CLOSE_MS,
    SCROLL_LOCK_CLASS,
} from "#configuration/constants/overlay.constants";
import { OVERLAY_DISMISSED, OVERLAY_REQUESTED } from "#core/ids/overlay.ids";
import { emitEvent, subscribeEvent } from "#core/buses/base.bus";
import type { Disposer } from "#types/base.types";
import { OPEN_CLASS } from "#configuration/constants/element.constants";
import type { OverlayRequestedEvent } from "#types/event.types";
import { createCloseButton } from "#presentation/components/overlay.component";
import { createElement } from "#core/factories/element.factory";

const createDialog = function createDialog(event: OverlayRequestedEvent): HTMLElement {
    const header = createElement("div", {
        children: [createElement("span", { className: "overlay-title", text: event.title }), createCloseButton()],
        className: "overlay-header",
    });
    const body = createElement("div", { children: [event.content], className: "overlay-body" });
    return createElement("div", {
        attributes: { "aria-label": event.title, "aria-modal": "true", role: "dialog" },
        children: [header, body],
        className: "overlay-dialog",
    });
};

const createBackdrop = function createBackdrop(dialog: HTMLElement): HTMLElement {
    const backdrop = createElement("div", { children: [dialog], className: "overlay-backdrop" });
    backdrop.addEventListener("click", (click) => {
        if (click.target === backdrop) {
            emitEvent({ name: OVERLAY_DISMISSED });
        }
    });
    return backdrop;
};

const onEscape = function onEscape(key: KeyboardEvent): void {
    if (key.key === ESCAPE_KEY) {
        emitEvent({ name: OVERLAY_DISMISSED });
    }
};

const reveal = function reveal(overlay: HTMLElement): void {
    requestAnimationFrame(() => {
        requestAnimationFrame(() => {
            overlay.classList.add(OPEN_CLASS);
        });
    });
};

const teardown = function teardown(overlay: HTMLElement): void {
    setTimeout(() => {
        overlay.remove();
        document.body.classList.remove(SCROLL_LOCK_CLASS);
    }, OVERLAY_CLOSE_MS);
};

const open = function open(event: OverlayRequestedEvent): HTMLElement {
    const overlay = createElement("div", { children: [createBackdrop(createDialog(event))], className: "overlay" });
    document.body.classList.add(SCROLL_LOCK_CLASS);
    document.body.append(overlay);
    document.addEventListener("keydown", onEscape);
    reveal(overlay);
    return overlay;
};

const close = function close(overlay: HTMLElement): void {
    overlay.classList.remove(OPEN_CLASS);
    overlay.classList.add(CLOSING_CLASS);
    document.removeEventListener("keydown", onEscape);
    teardown(overlay);
};

export const mountOverlay = function mountOverlay(): Disposer {
    let current: HTMLElement | null = null;
    const disposers = [
        subscribeEvent(OVERLAY_REQUESTED, (event) => {
            if (current !== null) {
                close(current);
            }
            current = open(event);
        }),
        subscribeEvent(OVERLAY_DISMISSED, () => {
            if (current !== null) {
                close(current);
                current = null;
            }
        }),
    ];
    return () => {
        for (const dispose of disposers) {
            dispose();
        }
    };
};