Preserve the board lane position when mobile users return from full-panel task detail. - Capture and restore board, column, project-content, and document scroll offsets for board detail navigation. - Retry board scroll restoration across bounded animation frames after remount and hydration. - Cover mobile and desktop Back to board behavior with focused tests and document the mobile scroll contract. - Add a patch changeset for the published CLI package. Files changed: .changeset/fn-7258-mobile-board-scroll.md | 7 ++ docs/dashboard-guide.md | 2 + .../__tests__/navigation-history.test.tsx | 111 ++++++++++++++++++--- .../hooks/__tests__/useBoardScrollRestore.test.ts | 43 +++++++- .../dashboard/app/hooks/useBoardScrollRestore.ts | 45 ++++++--- .../utils/__tests__/boardScrollSnapshot.test.ts | 91 +++++++++++++++-- .../dashboard/app/utils/boardScrollSnapshot.ts | 41 +++++++- 7 files changed, 305 insertions(+), 35 deletions(-) Fusion-Task-Id: FN-7258 Fusion-Task-Lineage: 97faf031-4244-4117-9948-45491c364134 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
77 lines
2.8 KiB
TypeScript
77 lines
2.8 KiB
TypeScript
/*
|
|
FNXC:BoardNavigation 2026-06-24-00:00:
|
|
Preserves horizontal board scroll and per-column vertical scroll across a board → task-detail → back-to-board round trip. capture() snapshots before opening detail; requestRestore() schedules a restore that fires (double requestAnimationFrame, after the board remounts) once the view returns to "board". Extracted from AppInner.
|
|
|
|
FNXC:BoardNavigation 2026-06-29-20:45:
|
|
Mobile Back-to-board must restore the clicked-card board position after the full-panel detail unmounts. Retry the restore for a bounded sequence of animation frames because mobile board layout stabilization and workflow-board hydration can temporarily leave #board unavailable or reset its offsets after the first post-return frame.
|
|
*/
|
|
|
|
import { useCallback, useEffect, useRef } from "react";
|
|
import {
|
|
captureBoardScrollSnapshot,
|
|
restoreBoardScrollSnapshot,
|
|
type BoardScrollSnapshot,
|
|
} from "../utils/boardScrollSnapshot";
|
|
import type { TaskView } from "./useViewState";
|
|
|
|
const MAX_RESTORE_ATTEMPTS = 6;
|
|
|
|
export interface UseBoardScrollRestoreResult {
|
|
capture: () => void;
|
|
requestRestore: () => void;
|
|
}
|
|
|
|
export function useBoardScrollRestore(taskView: TaskView): UseBoardScrollRestoreResult {
|
|
const boardScrollSnapshotRef = useRef<BoardScrollSnapshot | null>(null);
|
|
const pendingBoardScrollRestoreRef = useRef(false);
|
|
|
|
const capture = useCallback(() => {
|
|
boardScrollSnapshotRef.current = captureBoardScrollSnapshot();
|
|
}, []);
|
|
|
|
const requestRestore = useCallback(() => {
|
|
pendingBoardScrollRestoreRef.current = true;
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (taskView !== "board" || !pendingBoardScrollRestoreRef.current) return;
|
|
const scheduleFrame = typeof window.requestAnimationFrame === "function"
|
|
? window.requestAnimationFrame.bind(window)
|
|
: ((callback: FrameRequestCallback) => window.setTimeout(() => callback(performance.now()), 0));
|
|
const cancelFrame = typeof window.cancelAnimationFrame === "function"
|
|
? window.cancelAnimationFrame.bind(window)
|
|
: window.clearTimeout.bind(window);
|
|
const frameIds: number[] = [];
|
|
let attempts = 0;
|
|
let cancelled = false;
|
|
|
|
const schedule = (callback: FrameRequestCallback) => {
|
|
const id = scheduleFrame(callback);
|
|
frameIds.push(id);
|
|
};
|
|
|
|
const attemptRestore = () => {
|
|
if (cancelled || !pendingBoardScrollRestoreRef.current) return;
|
|
attempts += 1;
|
|
const restored = restoreBoardScrollSnapshot(boardScrollSnapshotRef.current);
|
|
if (restored) {
|
|
pendingBoardScrollRestoreRef.current = false;
|
|
return;
|
|
}
|
|
if (attempts < MAX_RESTORE_ATTEMPTS) {
|
|
schedule(attemptRestore);
|
|
}
|
|
};
|
|
|
|
schedule(() => {
|
|
schedule(attemptRestore);
|
|
});
|
|
return () => {
|
|
cancelled = true;
|
|
frameIds.forEach(cancelFrame);
|
|
};
|
|
}, [taskView]);
|
|
|
|
return { capture, requestRestore };
|
|
}
|