mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-14 01:50:39 +09:00
fix(ui): align PWA display-mode listeners
This commit is contained in:
parent
7ce96e36a0
commit
cfcdf2dea9
2 changed files with 104 additions and 7 deletions
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue