diff --git a/src/client/components/BaseModal.ts b/src/client/components/BaseModal.ts index 18a5efd55..22a3302c4 100644 --- a/src/client/components/BaseModal.ts +++ b/src/client/components/BaseModal.ts @@ -1,4 +1,4 @@ -import { html, LitElement, TemplateResult } from "lit"; +import { html, LitElement, nothing, TemplateResult } from "lit"; import { property, query, state } from "lit/decorators.js"; import { modalRouter } from "../ModalRouter"; import "./baseComponents/Modal"; @@ -115,8 +115,13 @@ export abstract class BaseModal extends LitElement { render(): TemplateResult { const cfg = this.modalConfig(); const tabs = cfg.tabs ?? []; - const body = this.renderBody(this.activeTab); - const headerSlot = this.renderHeaderSlot(); + // Keep the lightweight modal shell mounted so open() can address it, but + // defer potentially expensive modal contents until they are visible. + const shouldRenderContent = this.inline || this.isModalOpen; + const body = shouldRenderContent + ? this.renderBody(this.activeTab) + : nothing; + const headerSlot = shouldRenderContent ? this.renderHeaderSlot() : null; return html` Header`; + } + + protected renderBody(): TemplateResult { + this.bodyRenderCount++; + return html`Body`; + } +} + +if (!customElements.get("lazy-body-test-modal")) { + customElements.define("lazy-body-test-modal", LazyBodyTestModal); +} + +describe("BaseModal lazy rendering", () => { + let modal: LazyBodyTestModal | undefined; + + afterEach(() => { + modal?.remove(); + modal = undefined; + }); + + it("does not render modal content until opened", async () => { + modal = document.createElement("lazy-body-test-modal") as LazyBodyTestModal; + document.body.appendChild(modal); + await modal.updateComplete; + + const modalShell = modal.querySelector("o-modal"); + expect(modalShell).not.toBeNull(); + expect(modal.bodyRenderCount).toBe(0); + expect(modal.headerRenderCount).toBe(0); + expect(modal.querySelector('[data-testid="body"]')).toBeNull(); + + modal.open(); + await modal.updateComplete; + + expect(modal.bodyRenderCount).toBe(1); + expect(modal.headerRenderCount).toBe(1); + expect(modal.querySelector('[data-testid="body"]')).not.toBeNull(); + + modal.close(); + await modal.updateComplete; + + expect(modal.querySelector('[data-testid="body"]')).toBeNull(); + expect(modal.querySelector('[data-testid="header"]')).toBeNull(); + expect(modal.querySelector("o-modal")).toBe(modalShell); + }); + + it("renders inline content without being opened", async () => { + modal = document.createElement("lazy-body-test-modal") as LazyBodyTestModal; + modal.setAttribute("inline", ""); + document.body.appendChild(modal); + await modal.updateComplete; + + expect(modal.bodyRenderCount).toBe(1); + expect(modal.headerRenderCount).toBe(1); + expect(modal.querySelector('[data-testid="body"]')).not.toBeNull(); + expect(modal.querySelector('[data-testid="header"]')).not.toBeNull(); + }); +}); diff --git a/tests/client/LeaderboardModal.test.ts b/tests/client/LeaderboardModal.test.ts index 2bef31f1b..a7e9ec748 100644 --- a/tests/client/LeaderboardModal.test.ts +++ b/tests/client/LeaderboardModal.test.ts @@ -133,6 +133,7 @@ describe("LeaderboardModal", () => { customElements.define("leaderboard-modal", LeaderboardModal); } modal = document.createElement("leaderboard-modal") as LeaderboardModal; + modal.inline = true; document.body.appendChild(modal); await modal.updateComplete; }); @@ -304,6 +305,7 @@ describe("LeaderboardModal", () => { }); it("should close on Escape when open", () => { + modal.inline = false; const mockModalEl = { open: vi.fn(), close: vi.fn() }; Object.defineProperty(modal, "modalEl", { get: () => mockModalEl,