feat(FN-3237): fix mobile viewport teardown when closing planning mode

Merges FN-3237 to stabilize the mobile viewport teardown flow when closing the planning modal, ensuring the viewport state resets correctly. Adds test coverage for the mobile planning close path and includes a changeset for the patch release.

Fusion-Task-Id: FN-3237
This commit is contained in:
Fusion
2026-05-04 05:44:36 -07:00
committed by gsxdsm
parent 09050d33fe
commit 08d655abc4
3 changed files with 61 additions and 3 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Fix a mobile dashboard regression where closing Planning Mode after keyboard/visualViewport changes could leave board/list content shifted or clipped. Planning Mode now performs mobile viewport teardown (blur + top snap) on close so control returns cleanly to the dashboard.

View File

@@ -1178,6 +1178,22 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
// Close the modal without abandoning the active server session. Sessions
// remain in the list and can be resumed later. Only an explicit Delete
// (from the sidebar) cancels and removes a session.
const resetMobileViewportAfterClose = useCallback(() => {
if (viewportMode !== "mobile") {
return;
}
const activeElement = document.activeElement;
if (activeElement instanceof HTMLElement) {
activeElement.blur();
}
window.scrollTo(0, 0);
requestAnimationFrame(() => {
window.scrollTo(0, 0);
});
}, [viewportMode]);
const handleClose = useCallback(() => {
// Save the in-progress draft so the next open restores it.
if (initialPlan && view.type === "initial") {
@@ -1200,8 +1216,9 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
streamConnectionRef.current = null;
setIsReconnecting(false);
setIsRetrying(false);
resetMobileViewportAfterClose();
onClose();
}, [flushDraftAndSummarize, initialPlan, onClose, projectId, view.type]);
}, [flushDraftAndSummarize, initialPlan, onClose, projectId, resetMobileViewportAfterClose, view.type]);
// Handle escape key to close
useEffect(() => {
@@ -1475,12 +1492,12 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
currentSessionIdRef.current = null;
setLockSessionId(null);
setSelectedSessionId(null);
onClose();
handleClose();
} catch (err) {
setError(getErrorMessage(err) || "Failed to create tasks");
setView({ type: "breakdown", sessionId: view.sessionId, subtasks: view.subtasks, dirty: view.dirty });
}
}, [broadcastCompleted, view, onTasksCreated, onClose, projectId]);
}, [broadcastCompleted, handleClose, view, onTasksCreated, projectId]);
const handleBack = useCallback(() => {
if (view.type === "question" && responseHistory.length > 0) {

View File

@@ -349,6 +349,42 @@ describe("PlanningModeModal", () => {
expect(screen.queryByText("Planning Mode")).toBeNull();
});
it("mobile close path blurs focused input and resets viewport scroll", () => {
mockViewport("mobile");
const scrollToSpy = vi.spyOn(window, "scrollTo").mockImplementation(() => undefined);
const rafSpy = vi
.spyOn(window, "requestAnimationFrame")
.mockImplementation((callback: FrameRequestCallback) => {
callback(0);
return 1;
});
render(
<PlanningModeModal
isOpen={true}
onClose={mockOnClose}
onTaskCreated={mockOnTaskCreated}
onTasksCreated={vi.fn()}
tasks={mockTasks}
/>
);
const textarea = screen.getByPlaceholderText(/e.g., Build a user authentication/) as HTMLTextAreaElement;
act(() => {
textarea.focus();
});
const blurSpy = vi.spyOn(textarea, "blur");
act(() => {
fireEvent.click(screen.getByRole("button", { name: "Close" }));
});
expect(blurSpy).toHaveBeenCalledTimes(1);
expect(scrollToSpy).toHaveBeenCalledWith(0, 0);
expect(rafSpy).toHaveBeenCalled();
expect(mockOnClose).toHaveBeenCalledTimes(1);
});
it("hides send to background button in initial state", () => {
render(
<PlanningModeModal