Extract three AppInner state clusters into hooks: - useMainPanelTaskDetail: the main-panel task-detail snapshot + initial tab; setTask accepts the SetStateAction updater form so the embedded detail can patch the snapshot on task updates. - useBoardScrollRestore: the board scroll snapshot refs, capture, and the double-requestAnimationFrame restore effect keyed on taskView; exposes requestRestore for App to schedule a restore on detail close. - usePoppedOutTasks: the popped-out task-detail windows (dedupe-by-id popOut, close-by-id). App keeps the navigation-history composition (openTaskDetailInMainPanel / closeTaskDetailMainPanel) and now consumes the hooks' primitives. Behavior-preserving; App.test.tsx identical (5 pre-existing failures, none introduced). Verified by typecheck, eslint, and 5 renderHook tests. Completes U6 (App.tsx module-breakup plan).
58 lines
2.1 KiB
TypeScript
58 lines
2.1 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.
|
|
*/
|
|
|
|
import { useCallback, useEffect, useRef } from "react";
|
|
import {
|
|
captureBoardScrollSnapshot,
|
|
restoreBoardScrollSnapshot,
|
|
type BoardScrollSnapshot,
|
|
} from "../utils/boardScrollSnapshot";
|
|
import type { TaskView } from "./useViewState";
|
|
|
|
export interface UseBoardScrollRestoreResult {
|
|
capture: () => void;
|
|
requestRestore: () => void;
|
|
}
|
|
|
|
export function useBoardScrollRestore(taskView: TaskView): UseBoardScrollRestoreResult {
|
|
const boardScrollSnapshotRef = useRef<BoardScrollSnapshot | null>(null);
|
|
const pendingBoardScrollRestoreRef = useRef(false);
|
|
|
|
const restore = useCallback(() => {
|
|
if (restoreBoardScrollSnapshot(boardScrollSnapshotRef.current)) {
|
|
pendingBoardScrollRestoreRef.current = 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);
|
|
let firstFrame = 0;
|
|
let secondFrame = 0;
|
|
firstFrame = scheduleFrame(() => {
|
|
secondFrame = scheduleFrame(restore);
|
|
});
|
|
return () => {
|
|
cancelFrame(firstFrame);
|
|
cancelFrame(secondFrame);
|
|
};
|
|
}, [restore, taskView]);
|
|
|
|
return { capture, requestRestore };
|
|
}
|