feat(FN-4060): fix mobile keyboard layout regression in QuickChatFAB compos
Fixes the keyboard-up composer gap in QuickChatFAB (Step 2), with companion regression tests covering the mobile keyboard layout and QuickChatFAB component behavior. Fusion-Task-Id: FN-4060 Fusion-Task-Lineage: 341b16b1-47db-4e23-b9fe-87cf0782da6a
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { loadAllAppCss } from "../test/cssFixture";
|
||||
|
||||
function extractMobileMediaBlocks(content: string): string {
|
||||
const blocks: string[] = [];
|
||||
const regex = /@media\s*\(\s*max-width:\s*768px\s*\)\s*\{/g;
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
while ((match = regex.exec(content)) !== null) {
|
||||
const startIdx = match.index + match[0].length;
|
||||
let braceCount = 1;
|
||||
let endIdx = startIdx;
|
||||
while (braceCount > 0 && endIdx < content.length) {
|
||||
if (content[endIdx] === "{") braceCount += 1;
|
||||
if (content[endIdx] === "}") braceCount -= 1;
|
||||
endIdx += 1;
|
||||
}
|
||||
if (braceCount === 0) {
|
||||
blocks.push(content.slice(startIdx, endIdx - 1));
|
||||
}
|
||||
}
|
||||
|
||||
return blocks.join("\n");
|
||||
}
|
||||
|
||||
describe("quick-chat mobile keyboard layout css", () => {
|
||||
const css = loadAllAppCss();
|
||||
const mobileCss = extractMobileMediaBlocks(css);
|
||||
|
||||
it("drops safe-area bottom inset from composer padding while keyboard-open class is active", () => {
|
||||
const keyboardOpenRule = /\.quick-chat-panel\.quick-chat-panel--keyboard-open\s+\.quick-chat-panel-input\s*\{[^}]*padding-bottom:\s*calc\(var\(--space-sm\)\s*\+\s*var\(--space-xs\)\)\s*;/m;
|
||||
expect(keyboardOpenRule.test(mobileCss)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -795,6 +795,11 @@
|
||||
padding-bottom: calc(var(--space-sm) + var(--space-xs) + env(safe-area-inset-bottom, 0px));
|
||||
}
|
||||
|
||||
.quick-chat-panel.quick-chat-panel--keyboard-open .quick-chat-panel-input {
|
||||
/* When keyboard is open, do not add home-indicator inset gap above it. */
|
||||
padding-bottom: calc(var(--space-sm) + var(--space-xs));
|
||||
}
|
||||
|
||||
.quick-chat-attachment-previews {
|
||||
padding: var(--space-sm) max(var(--space-md), env(safe-area-inset-left, 0px));
|
||||
}
|
||||
|
||||
@@ -882,7 +882,7 @@ export function QuickChatFAB({
|
||||
// directly on the panel DOM in a layout effect below — going through
|
||||
// React state introduces a per-event reconciliation lag that the human
|
||||
// eye reads as jank while the iOS keyboard is animating in.
|
||||
useMobileKeyboard({ enabled: isOpen });
|
||||
const { keyboardOpen } = useMobileKeyboard({ enabled: isOpen });
|
||||
const viewportMode = useViewportMode();
|
||||
const isMobile = viewportMode === "mobile";
|
||||
|
||||
@@ -2161,7 +2161,7 @@ export function QuickChatFAB({
|
||||
|
||||
{isOpen && (
|
||||
<div
|
||||
className="quick-chat-panel"
|
||||
className={`quick-chat-panel${isMobile && keyboardOpen ? " quick-chat-panel--keyboard-open" : ""}`}
|
||||
ref={panelRef}
|
||||
data-testid="quick-chat-panel"
|
||||
style={{
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { ChatSession } from "@fusion/core";
|
||||
import * as apiModule from "../../api";
|
||||
import { useAgents } from "../../hooks/useAgents";
|
||||
import { useViewportMode } from "../../hooks/useViewportMode";
|
||||
import { useMobileKeyboard } from "../../hooks/useMobileKeyboard";
|
||||
import { QuickChatFAB } from "../QuickChatFAB";
|
||||
|
||||
vi.mock("../../api", () => ({
|
||||
@@ -21,6 +22,7 @@ vi.mock("../../api", () => ({
|
||||
|
||||
vi.mock("../../hooks/useAgents", () => ({ useAgents: vi.fn() }));
|
||||
vi.mock("../../hooks/useViewportMode", () => ({ useViewportMode: vi.fn() }));
|
||||
vi.mock("../../hooks/useMobileKeyboard", () => ({ useMobileKeyboard: vi.fn() }));
|
||||
|
||||
const mockFetchResumeChatSession = vi.mocked(apiModule.fetchResumeChatSession);
|
||||
const mockFetchChatSessions = vi.mocked(apiModule.fetchChatSessions);
|
||||
@@ -32,6 +34,7 @@ const mockStreamChatResponse = vi.mocked(apiModule.streamChatResponse);
|
||||
const mockCancelChatResponse = vi.mocked(apiModule.cancelChatResponse);
|
||||
const mockUseAgents = vi.mocked(useAgents);
|
||||
const mockUseViewportMode = vi.mocked(useViewportMode);
|
||||
const mockUseMobileKeyboard = vi.mocked(useMobileKeyboard);
|
||||
|
||||
const agents: Agent[] = [
|
||||
{ id: "agent-001", name: "Agent One", role: "executor", state: "active", createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), metadata: {} },
|
||||
@@ -80,6 +83,12 @@ describe("QuickChatFAB session-first UX", () => {
|
||||
localStorage.clear();
|
||||
mockUseAgents.mockReturnValue({ agents, activeAgents: agents, stats: null, isLoading: false, loadAgents: vi.fn(), loadStats: vi.fn() });
|
||||
mockUseViewportMode.mockReturnValue("desktop");
|
||||
mockUseMobileKeyboard.mockReturnValue({
|
||||
keyboardOverlap: 0,
|
||||
viewportHeight: null,
|
||||
viewportOffsetTop: 0,
|
||||
keyboardOpen: false,
|
||||
});
|
||||
mockFetchResumeChatSession.mockResolvedValue({ session: modelSession });
|
||||
mockFetchChatMessages.mockResolvedValue({ messages: [] });
|
||||
mockFetchChatSessions.mockResolvedValue({ sessions: [modelSession, agentSession] });
|
||||
@@ -671,6 +680,24 @@ describe("QuickChatFAB session-first UX", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("applies keyboard-open panel class on mobile to remove composer safe-area gap", async () => {
|
||||
Object.defineProperty(window, "innerWidth", { configurable: true, value: 390 });
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
mockUseViewportMode.mockReturnValue("mobile");
|
||||
mockUseMobileKeyboard.mockReturnValue({
|
||||
keyboardOverlap: 160,
|
||||
viewportHeight: 500,
|
||||
viewportOffsetTop: 0,
|
||||
keyboardOpen: true,
|
||||
});
|
||||
|
||||
render(<QuickChatFAB addToast={vi.fn()} projectId="proj-1" />);
|
||||
fireEvent.click(screen.getByTestId("quick-chat-fab"));
|
||||
|
||||
const panel = await screen.findByTestId("quick-chat-panel");
|
||||
expect(panel).toHaveClass("quick-chat-panel--keyboard-open");
|
||||
});
|
||||
|
||||
it("FN-4040: mobile visibility restore re-anchors quick chat to latest", async () => {
|
||||
mockUseViewportMode.mockReturnValue("mobile");
|
||||
mockFetchChatMessages.mockResolvedValue({
|
||||
|
||||
Reference in New Issue
Block a user