diff --git a/.changeset/pwa-refresh-menu.md b/.changeset/pwa-refresh-menu.md new file mode 100644 index 0000000..8308575 --- /dev/null +++ b/.changeset/pwa-refresh-menu.md @@ -0,0 +1,5 @@ +--- +"@jmfederico/pi-web": patch +--- + +Fix the PWA refresh control menu so its reload options are visible when opened from the compact header button. diff --git a/src/client/src/components/actionMenu.test.ts b/src/client/src/components/actionMenu.test.ts new file mode 100644 index 0000000..ced599e --- /dev/null +++ b/src/client/src/components/actionMenu.test.ts @@ -0,0 +1,27 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { actionMenuPanelStyle } from "./actionMenu"; + +describe("actionMenuPanelStyle", () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it("can constrain menus to the viewport for compact shadow-root controls", () => { + vi.stubGlobal("window", { innerWidth: 400, innerHeight: 800 }); + vi.stubGlobal("HTMLElement", FakeHTMLElement); + + const target = new FakeHTMLElement({ top: 10, right: 390, bottom: 46, left: 354 }); + + expect(actionMenuPanelStyle(target, { constrainTo: "viewport" })).toBe("top: 46px; max-height: 754px; right: 10px; max-width: 390px;"); + }); +}); + +class FakeHTMLElement extends EventTarget { + constructor(private readonly rect: { top: number; right: number; bottom: number; left: number }) { + super(); + } + + getBoundingClientRect(): { top: number; right: number; bottom: number; left: number } { + return this.rect; + } +} diff --git a/src/client/src/components/actionMenu.ts b/src/client/src/components/actionMenu.ts index dac8a8c..5d46c2d 100644 --- a/src/client/src/components/actionMenu.ts +++ b/src/client/src/components/actionMenu.ts @@ -8,10 +8,14 @@ interface ActionMenuRect { left: number; } -export function actionMenuPanelStyle(target: EventTarget | null): string { +interface ActionMenuPanelStyleOptions { + constrainTo?: "host" | "viewport"; +} + +export function actionMenuPanelStyle(target: EventTarget | null, options: ActionMenuPanelStyleOptions = {}): string { if (typeof HTMLElement === "undefined" || typeof window === "undefined" || !(target instanceof HTMLElement)) return ""; const trigger = target.getBoundingClientRect(); - const bounds = actionMenuBounds(target); + const bounds = options.constrainTo === "viewport" ? viewportBounds() : actionMenuBounds(target); const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; const leftBound = Math.max(0, bounds.left); @@ -35,6 +39,10 @@ export function actionMenuPanelStyle(target: EventTarget | null): string { function actionMenuBounds(target: HTMLElement): ActionMenuRect { const root = target.getRootNode(); if (typeof ShadowRoot !== "undefined" && root instanceof ShadowRoot && root.host instanceof HTMLElement) return root.host.getBoundingClientRect(); + return viewportBounds(); +} + +function viewportBounds(): ActionMenuRect { return { top: 0, right: window.innerWidth, bottom: window.innerHeight, left: 0 }; } diff --git a/src/client/src/components/appShell/AppRefreshControl.ts b/src/client/src/components/appShell/AppRefreshControl.ts index 8bda17a..624abbd 100644 --- a/src/client/src/components/appShell/AppRefreshControl.ts +++ b/src/client/src/components/appShell/AppRefreshControl.ts @@ -112,7 +112,7 @@ export class AppRefreshControl extends LitElement { }; private openMenu(target: EventTarget | null): void { - this.menuStyle = actionMenuPanelStyle(target); + this.menuStyle = actionMenuPanelStyle(target, { constrainTo: "viewport" }); this.menuOpen = true; }