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:
5
.changeset/fn-3237-mobile-planning-viewport-fix.md
Normal file
5
.changeset/fn-3237-mobile-planning-viewport-fix.md
Normal 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.
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user