fix(ui): align PWA display-mode listeners

This commit is contained in:
Dotta 2026-06-01 21:56:19 +00:00
parent 7ce96e36a0
commit cfcdf2dea9
2 changed files with 104 additions and 7 deletions

View file

@ -25,8 +25,47 @@ async function flushReact() {
});
}
function installMatchMedia(initialMatches: Record<string, boolean> = {}) {
type Listener = (event: MediaQueryListEvent) => void;
const queries = new Map<string, { matches: boolean; listeners: Set<Listener> }>();
function getQuery(query: string) {
let entry = queries.get(query);
if (!entry) {
entry = { matches: initialMatches[query] ?? false, listeners: new Set<Listener>() };
queries.set(query, entry);
}
return {
get matches() {
return entry.matches;
},
media: query,
addEventListener: (_type: "change", listener: Listener) => entry.listeners.add(listener),
removeEventListener: (_type: "change", listener: Listener) => entry.listeners.delete(listener),
addListener: (listener: Listener) => entry.listeners.add(listener),
removeListener: (listener: Listener) => entry.listeners.delete(listener),
} as MediaQueryList;
}
Object.defineProperty(window, "matchMedia", {
configurable: true,
value: (query: string) => getQuery(query),
});
return {
setMatches(query: string, matches: boolean) {
const entry = queries.get(query) ?? { matches: false, listeners: new Set<Listener>() };
entry.matches = matches;
queries.set(query, entry);
entry.listeners.forEach((listener) => listener({ matches, media: query } as MediaQueryListEvent));
},
};
}
describe("StandaloneBrowserControls", () => {
let container: HTMLDivElement;
const originalMatchMedia = window.matchMedia;
beforeEach(() => {
container = document.createElement("div");
@ -36,6 +75,11 @@ describe("StandaloneBrowserControls", () => {
afterEach(() => {
delete (navigator as Navigator & { standalone?: boolean }).standalone;
if (originalMatchMedia) {
Object.defineProperty(window, "matchMedia", { configurable: true, value: originalMatchMedia });
} else {
delete (window as Window & { matchMedia?: Window["matchMedia"] }).matchMedia;
}
container.remove();
document.body.innerHTML = "";
});
@ -62,4 +106,57 @@ describe("StandaloneBrowserControls", () => {
root.unmount();
});
});
it("hides controls in normal mobile browser mode", async () => {
Object.defineProperty(navigator, "standalone", { configurable: true, value: false });
installMatchMedia();
const root = createRoot(container);
await act(async () => {
root.render(
<TooltipProvider>
<ToastProvider>
<StandaloneBrowserControls mobile />
</ToastProvider>
</TooltipProvider>,
);
});
await flushReact();
expect(container.querySelector('[aria-label="Refresh"]')).toBeNull();
await act(async () => {
root.unmount();
});
});
it("responds to all chromeless display-mode media query changes", async () => {
Object.defineProperty(navigator, "standalone", { configurable: true, value: false });
const media = installMatchMedia();
const root = createRoot(container);
await act(async () => {
root.render(
<TooltipProvider>
<ToastProvider>
<StandaloneBrowserControls mobile />
</ToastProvider>
</TooltipProvider>,
);
});
await flushReact();
expect(container.querySelector('[aria-label="Refresh"]')).toBeNull();
await act(() => {
media.setMatches("(display-mode: fullscreen)", true);
});
await flushReact();
expect(container.querySelector('[aria-label="Refresh"]')).not.toBeNull();
await act(async () => {
root.unmount();
});
});
});

View file

@ -3,7 +3,7 @@ import { ExternalLink, RefreshCw, Share2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { useOptionalToastActions } from "../context/ToastContext";
import { isChromelessDisplayMode } from "../lib/pwa-display-mode";
import { CHROMELESS_DISPLAY_MODES, isChromelessDisplayMode } from "../lib/pwa-display-mode";
function ControlButton({
label,
@ -48,14 +48,14 @@ export function StandaloneBrowserControls({ mobile }: { mobile: boolean }) {
update();
if (typeof window.matchMedia !== "function") return;
const media = window.matchMedia("(display-mode: standalone)");
if (typeof media.addEventListener === "function") {
media.addEventListener("change", update);
return () => media.removeEventListener("change", update);
const mediaQueries = CHROMELESS_DISPLAY_MODES.map((mode) => window.matchMedia(`(display-mode: ${mode})`));
if (mediaQueries.every((media) => typeof media.addEventListener === "function")) {
mediaQueries.forEach((media) => media.addEventListener("change", update));
return () => mediaQueries.forEach((media) => media.removeEventListener("change", update));
}
media.addListener(update);
return () => media.removeListener(update);
mediaQueries.forEach((media) => media.addListener(update));
return () => mediaQueries.forEach((media) => media.removeListener(update));
}, [mobile]);
const refresh = useCallback(() => {