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:
5
.changeset/fix-planning-modal-mobile-keyboard.md
Normal file
5
.changeset/fix-planning-modal-mobile-keyboard.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix Planning Mode modal being pushed up when virtual keyboard opens on mobile. The modal now uses `useMobileKeyboard` to track viewport changes and adjusts its height via CSS variables instead of relying on `100dvh`.
|
||||||
@@ -356,6 +356,12 @@
|
|||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
resize: none;
|
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 {
|
.planning-modal-body--split {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import "./PlanningModeModal.css";
|
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 type { Task, PlanningQuestion, PlanningSummary } from "@fusion/core";
|
||||||
import { getErrorMessage } from "@fusion/core";
|
import { getErrorMessage } from "@fusion/core";
|
||||||
import {
|
import {
|
||||||
@@ -43,6 +43,7 @@ import { OnboardingDisclosure } from "./OnboardingDisclosure";
|
|||||||
import { useSessionLock } from "../hooks/useSessionLock";
|
import { useSessionLock } from "../hooks/useSessionLock";
|
||||||
import { useAiSessionSync } from "../hooks/useAiSessionSync";
|
import { useAiSessionSync } from "../hooks/useAiSessionSync";
|
||||||
import { useViewportMode } from "../hooks/useViewportMode";
|
import { useViewportMode } from "../hooks/useViewportMode";
|
||||||
|
import { useMobileKeyboard } from "../hooks/useMobileKeyboard";
|
||||||
import { getSessionTabId } from "../utils/getSessionTabId";
|
import { getSessionTabId } from "../utils/getSessionTabId";
|
||||||
|
|
||||||
interface PlanningModeModalProps {
|
interface PlanningModeModalProps {
|
||||||
@@ -186,6 +187,17 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
|
|||||||
useModalResizePersist(modalRef, isOpen, "fusion:planning-modal-size");
|
useModalResizePersist(modalRef, isOpen, "fusion:planning-modal-size");
|
||||||
const viewportMode = useViewportMode();
|
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
|
// Mirror streamingOutput into a ref so SSE handlers can read the latest
|
||||||
// value without stale closure issues.
|
// value without stale closure issues.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -1508,7 +1520,7 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
|
|||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
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="modal-header">
|
||||||
<div className="detail-title-row">
|
<div className="detail-title-row">
|
||||||
{mobileShowDetail && (
|
{mobileShowDetail && (
|
||||||
|
|||||||
@@ -82,6 +82,28 @@ vi.mock("../../hooks/useConfirm", () => ({
|
|||||||
useConfirm: () => ({ confirm: mockConfirm }),
|
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[] = [
|
const mockTasks: Task[] = [
|
||||||
{
|
{
|
||||||
id: "FN-001",
|
id: "FN-001",
|
||||||
@@ -216,6 +238,7 @@ function getMediaBlocks(css: string, mediaQuery: string): string[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function mockViewport(mode: "mobile" | "desktop" | "tablet") {
|
function mockViewport(mode: "mobile" | "desktop" | "tablet") {
|
||||||
|
mockUseViewportMode.mockReturnValue(mode);
|
||||||
Object.defineProperty(window, "matchMedia", {
|
Object.defineProperty(window, "matchMedia", {
|
||||||
writable: true,
|
writable: true,
|
||||||
value: vi.fn().mockImplementation((query: string) => {
|
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