Revert "fix: harden automatic mobile boards"

This reverts commit d0b9348b86.
This commit is contained in:
ajnart committed 2026-08-13 19:55:19 +02:00
1 parent 967ed8dd73
commit e93293f8a9
22 files changed
+69 -491

No files matched your search

@@ -1,51 +0,0 @@
import { beforeEach, describe, expect, test, vi } from "vitest";
import { classifyGuardedNavigation, removeGuardHistoryEntry, replaceGuardedSameDocumentUrl } from "./_guarded-history";
describe("guarded board edit history", () => {
beforeEach(() => {
window.history.replaceState({ page: "board" }, "", "/boards/demo");
});
test("replaces the guard entry for same-document fragment navigation", () => {
const section = document.createElement("section");
section.id = "Media requests";
document.body.appendChild(section);
const scrollIntoView = vi.fn();
Object.defineProperty(section, "scrollIntoView", { value: scrollIntoView });
replaceGuardedSameDocumentUrl(new URL("/boards/demo#Media%20requests", window.location.href), {
key: "__guard",
id: "guard-id",
});
expect(window.location.hash).toBe("#Media%20requests");
expect(window.history.state).toStrictEqual({
page: "board",
__guard: "guard-id",
});
expect(scrollIntoView).toHaveBeenCalledOnce();
section.remove();
});
test("treats an explicit empty fragment as same-document navigation", () => {
const current = new URL(window.location.href);
const destination = new URL("#", current);
expect(classifyGuardedNavigation(destination, current)).toBe("same-document");
});
test("removes the guard while preserving the fragment when editing ends", () => {
window.history.replaceState({ page: "board", __guard: "guard-id" }, "", "/boards/demo#section");
const back = vi.spyOn(window.history, "back").mockImplementation(() => {
window.history.replaceState({ page: "board" }, "", "/boards/demo");
window.dispatchEvent(new PopStateEvent("popstate"));
});
removeGuardHistoryEntry("http://localhost:3000/boards/demo#section", "__guard");
expect(back).toHaveBeenCalledOnce();
expect(window.location.href).toBe("http://localhost:3000/boards/demo#section");
expect(window.history.state).toStrictEqual({ page: "board" });
});
});
@@ -1,58 +0,0 @@
const getHistoryState = () =>
window.history.state && typeof window.history.state === "object"
? (window.history.state as Record<string, unknown>)
: {};
interface GuardState {
key: string;
id: string;
}
export const classifyGuardedNavigation = (destination: URL, current: URL) => {
if (destination.href === current.href) return "same-url";
if (
destination.origin === current.origin &&
destination.pathname === current.pathname &&
destination.search === current.search
) {
return "same-document";
}
return "other-document";
};
export const replaceGuardedSameDocumentUrl = (destination: URL, guard: GuardState) => {
window.history.replaceState(
{
...getHistoryState(),
[guard.key]: guard.id,
},
document.title,
destination.href,
);
if (!destination.hash) {
window.scrollTo({ top: 0, left: 0 });
return;
}
let targetId = destination.hash.slice(1);
try {
targetId = decodeURIComponent(targetId);
} catch {
// Keep the encoded value when the fragment contains malformed escape sequences.
}
document.getElementById(targetId)?.scrollIntoView();
};
export const removeGuardHistoryEntry = (guardedUrl: string, guardStateKey: string) => {
window.addEventListener(
"popstate",
() => {
const state = { ...getHistoryState() };
delete state[guardStateKey];
window.history.replaceState(state, document.title, guardedUrl);
},
{ once: true },
);
window.history.back();
};
@@ -18,20 +18,6 @@ describe("resolveMobileItemPresentation", () => {
});
});
test("does not unmount apps that use the eager default", () => {
expect(
resolveMobileItemPresentation(
{ kind: "app", height: 1 },
{
unmountWhenOffscreen: true,
},
),
).toMatchObject({
eager: true,
unmountWhenOffscreen: false,
});
});
test("defaults widgets to full width without inventing a detail view", () => {
expect(resolveMobileItemPresentation({ kind: "calendar", height: 8 }, undefined)).toMatchObject({
width: 2,
@@ -8,7 +8,6 @@ export const resolveMobileItemPresentation = (
const supportsCompactSummary = mobile?.supportsCompactSummary === true;
const supportsDetails = mobile?.supportsDetailView === true;
const usesGenericSummary = supportsDetails && !supportsCompactSummary;
const eager = mobile?.eager ?? (item.kind === "app" || item.kind === "bookmarks");
return {
width: mobile?.width ?? (item.kind === "app" ? 1 : 2),
@@ -16,8 +15,8 @@ export const resolveMobileItemPresentation = (
displayMode: supportsCompactSummary || usesGenericSummary ? ("mobileSummary" as const) : ("default" as const),
supportsDetails,
usesGenericSummary,
eager,
unmountWhenOffscreen: eager ? false : (mobile?.unmountWhenOffscreen ?? false),
eager: mobile?.eager ?? (item.kind === "app" || item.kind === "bookmarks"),
unmountWhenOffscreen: mobile?.eager === true ? false : (mobile?.unmountWhenOffscreen ?? false),
};
};
@@ -12,7 +12,7 @@ describe("resolveMobileBoardViewport", () => {
resolveMobileBoardViewport({
deviceClass: "phone",
matchesMobileWidth: false,
hasMounted: true,
hasResolvedClientWidth: true,
}),
).toEqual({
deviceClass: "phone",
@@ -29,7 +29,7 @@ describe("resolveMobileBoardViewport", () => {
resolveMobileBoardViewport({
deviceClass: "tablet",
matchesMobileWidth,
hasMounted: true,
hasResolvedClientWidth: true,
}).isMobile,
).toBe(expectedIsMobile);
});
@@ -39,7 +39,7 @@ describe("resolveMobileBoardViewport", () => {
resolveMobileBoardViewport({
deviceClass: "tablet",
matchesMobileWidth: false,
hasMounted: false,
hasResolvedClientWidth: false,
}).isResolved,
).toBe(false);
});
@@ -52,7 +52,7 @@ describe("resolveMobileBoardViewport", () => {
resolveMobileBoardViewport({
deviceClass: "desktop",
matchesMobileWidth,
hasMounted: true,
hasResolvedClientWidth: true,
}),
).toMatchObject({
isMobile: expectedIsMobile,
@@ -73,9 +73,7 @@ describe("resolveIsAutomaticMobileBoard", () => {
expect(resolveIsAutomaticMobileBoard({ isMobileViewport, enableAutomaticMobileLayout })).toBe(expected);
},
);
});
describe("shouldShowMobileBoardViewportSkeleton", () => {
test("does not show the tablet resolution skeleton in legacy mode", () => {
expect(
shouldShowMobileBoardViewportSkeleton({
@@ -84,13 +82,4 @@ describe("shouldShowMobileBoardViewportSkeleton", () => {
}),
).toBe(false);
});
test("shows a skeleton while an automatic mobile viewport is unresolved", () => {
expect(
shouldShowMobileBoardViewportSkeleton({
enableAutomaticMobileLayout: true,
isResolved: false,
}),
).toBe(true);
});
});
@@ -26,15 +26,15 @@ const MobileBoardViewportContext = createContext<MobileBoardViewport>({
export const resolveMobileBoardViewport = ({
deviceClass,
matchesMobileWidth,
hasMounted,
hasResolvedClientWidth,
}: {
deviceClass: MobileBoardDeviceClass;
matchesMobileWidth: boolean;
hasMounted: boolean;
hasResolvedClientWidth: boolean;
}): MobileBoardViewport => ({
deviceClass,
isMobile: deviceClass === "phone" || matchesMobileWidth,
isResolved: deviceClass !== "tablet" || hasMounted,
isResolved: deviceClass !== "tablet" || hasResolvedClientWidth,
});
const MobileBoardViewportSkeleton = () => (
@@ -73,19 +73,19 @@ export const MobileBoardViewportProvider = ({
children,
}: PropsWithChildren<{ initialDeviceClass: MobileBoardDeviceClass }>) => {
const { enableAutomaticMobileLayout } = useSettings();
const [hasMounted, setHasMounted] = useState(false);
const [hasResolvedClientWidth, setHasResolvedClientWidth] = useState(false);
const matchesMobileWidth = useMediaQuery(mobileBoardMediaQuery, initialDeviceClass === "phone", {
getInitialValueInEffect: true,
});
useEffect(() => {
setHasMounted(true);
setHasResolvedClientWidth(true);
}, []);
const viewport = resolveMobileBoardViewport({
deviceClass: initialDeviceClass,
matchesMobileWidth,
hasMounted,
hasResolvedClientWidth,
});
return (