feat(FN-3337): recover chat isGenerating state on session reload and fix Pl
This merge includes three features: FN-3337 fixes mobile keyboard handling in the PlanningModeModal by wiring in `useMobileKeyboard` and adding keyboard-aware CSS sizing, with regression tests; FN-3336 recovers the `isGenerating` chat state on session reload to prevent UI flickering; and FN-3338 fix Fusion-Task-Id: FN-3337
This commit is contained in:
@@ -356,6 +356,12 @@
|
||||
border-radius: 0;
|
||||
resize: none;
|
||||
}
|
||||
.modal.planning-modal[style*="--keyboard-overlap"] {
|
||||
height: var(--vv-height, 100dvh);
|
||||
max-height: var(--vv-height, 100dvh);
|
||||
transform: translateY(var(--vv-offset-top, 0px));
|
||||
will-change: transform;
|
||||
}
|
||||
.planning-modal-body--split {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import "./PlanningModeModal.css";
|
||||
import { useState, useCallback, useEffect, useRef, useMemo } from "react";
|
||||
import { useState, useCallback, useEffect, useRef, useMemo, type CSSProperties } from "react";
|
||||
import type { Task, PlanningQuestion, PlanningSummary } from "@fusion/core";
|
||||
import { getErrorMessage } from "@fusion/core";
|
||||
import {
|
||||
@@ -43,6 +43,7 @@ import { OnboardingDisclosure } from "./OnboardingDisclosure";
|
||||
import { useSessionLock } from "../hooks/useSessionLock";
|
||||
import { useAiSessionSync } from "../hooks/useAiSessionSync";
|
||||
import { useViewportMode } from "../hooks/useViewportMode";
|
||||
import { useMobileKeyboard } from "../hooks/useMobileKeyboard";
|
||||
import { getSessionTabId } from "../utils/getSessionTabId";
|
||||
|
||||
interface PlanningModeModalProps {
|
||||
@@ -186,6 +187,17 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
|
||||
useModalResizePersist(modalRef, isOpen, "fusion:planning-modal-size");
|
||||
const viewportMode = useViewportMode();
|
||||
|
||||
const { keyboardOverlap, viewportHeight, viewportOffsetTop, keyboardOpen } =
|
||||
useMobileKeyboard({ enabled: viewportMode === "mobile" });
|
||||
|
||||
const modalKeyboardStyle: CSSProperties = keyboardOpen
|
||||
? ({
|
||||
"--keyboard-overlap": `${keyboardOverlap}px`,
|
||||
"--vv-offset-top": `${viewportOffsetTop}px`,
|
||||
...(viewportHeight !== null ? { "--vv-height": `${viewportHeight}px` } : {}),
|
||||
} as CSSProperties)
|
||||
: {};
|
||||
|
||||
// Mirror streamingOutput into a ref so SSE handlers can read the latest
|
||||
// value without stale closure issues.
|
||||
useEffect(() => {
|
||||
@@ -1508,7 +1520,7 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<div className="modal modal-lg planning-modal" ref={modalRef}>
|
||||
<div className="modal modal-lg planning-modal" ref={modalRef} style={modalKeyboardStyle}>
|
||||
<div className="modal-header">
|
||||
<div className="detail-title-row">
|
||||
{mobileShowDetail && (
|
||||
|
||||
@@ -82,6 +82,28 @@ vi.mock("../../hooks/useConfirm", () => ({
|
||||
useConfirm: () => ({ confirm: mockConfirm }),
|
||||
}));
|
||||
|
||||
const mockUseViewportMode = vi.fn<() => "mobile" | "tablet" | "desktop">(() => "desktop");
|
||||
|
||||
vi.mock("../../hooks/useViewportMode", () => ({
|
||||
useViewportMode: () => mockUseViewportMode(),
|
||||
}));
|
||||
|
||||
const mockUseMobileKeyboard = vi.fn<() => {
|
||||
keyboardOverlap: number;
|
||||
viewportHeight: number | null;
|
||||
viewportOffsetTop: number;
|
||||
keyboardOpen: boolean;
|
||||
}>(() => ({
|
||||
keyboardOverlap: 0,
|
||||
viewportHeight: null,
|
||||
viewportOffsetTop: 0,
|
||||
keyboardOpen: false,
|
||||
}));
|
||||
|
||||
vi.mock("../../hooks/useMobileKeyboard", () => ({
|
||||
useMobileKeyboard: (...args: any[]) => mockUseMobileKeyboard(...args),
|
||||
}));
|
||||
|
||||
const mockTasks: Task[] = [
|
||||
{
|
||||
id: "FN-001",
|
||||
@@ -216,6 +238,7 @@ function getMediaBlocks(css: string, mediaQuery: string): string[] {
|
||||
}
|
||||
|
||||
function mockViewport(mode: "mobile" | "desktop" | "tablet") {
|
||||
mockUseViewportMode.mockReturnValue(mode);
|
||||
Object.defineProperty(window, "matchMedia", {
|
||||
writable: true,
|
||||
value: vi.fn().mockImplementation((query: string) => {
|
||||
@@ -3337,4 +3360,81 @@ describe("useSessionLock", () => {
|
||||
);
|
||||
});
|
||||
|
||||
describe("Mobile keyboard behavior (FN-3337)", () => {
|
||||
beforeEach(() => {
|
||||
mockUseViewportMode.mockReturnValue("desktop");
|
||||
mockUseMobileKeyboard.mockReturnValue({
|
||||
keyboardOverlap: 0,
|
||||
viewportHeight: null,
|
||||
viewportOffsetTop: 0,
|
||||
keyboardOpen: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("applies keyboard CSS variables when keyboard is open on mobile", () => {
|
||||
mockUseViewportMode.mockReturnValue("mobile");
|
||||
mockUseMobileKeyboard.mockReturnValue({
|
||||
keyboardOverlap: 300,
|
||||
viewportHeight: 400,
|
||||
viewportOffsetTop: 50,
|
||||
keyboardOpen: true,
|
||||
});
|
||||
|
||||
render(
|
||||
<PlanningModeModal
|
||||
isOpen={true}
|
||||
onClose={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
const modal = screen.getByRole("dialog").querySelector(".planning-modal");
|
||||
expect(modal).toBeTruthy();
|
||||
expect(modal!.getAttribute("style")).toContain("--keyboard-overlap");
|
||||
expect(modal!.getAttribute("style")).toContain("--vv-height");
|
||||
expect(modal!.getAttribute("style")).toContain("--vv-offset-top");
|
||||
});
|
||||
|
||||
it("does not apply keyboard CSS variables when keyboard is closed", () => {
|
||||
mockUseViewportMode.mockReturnValue("mobile");
|
||||
mockUseMobileKeyboard.mockReturnValue({
|
||||
keyboardOverlap: 0,
|
||||
viewportHeight: null,
|
||||
viewportOffsetTop: 0,
|
||||
keyboardOpen: false,
|
||||
});
|
||||
|
||||
render(
|
||||
<PlanningModeModal
|
||||
isOpen={true}
|
||||
onClose={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
const modal = screen.getByRole("dialog").querySelector(".planning-modal");
|
||||
expect(modal).toBeTruthy();
|
||||
expect(modal!.getAttribute("style")).toBeNull();
|
||||
});
|
||||
|
||||
it("does not apply keyboard CSS variables on desktop", () => {
|
||||
mockUseViewportMode.mockReturnValue("desktop");
|
||||
mockUseMobileKeyboard.mockReturnValue({
|
||||
keyboardOverlap: 0,
|
||||
viewportHeight: null,
|
||||
viewportOffsetTop: 0,
|
||||
keyboardOpen: false,
|
||||
});
|
||||
|
||||
render(
|
||||
<PlanningModeModal
|
||||
isOpen={true}
|
||||
onClose={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
const modal = screen.getByRole("dialog").querySelector(".planning-modal");
|
||||
expect(modal).toBeTruthy();
|
||||
expect(modal!.getAttribute("style")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user