From a15b4caace3ebeee21293434df37c1768af5fc93 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Tue, 16 Jun 2026 18:43:48 -0700 Subject: [PATCH] FN-6494: keep tablet chat sidebar visible Keep tablet chat navigation available while software keyboards reduce the viewport. - Stop auto-hiding the tablet chat sidebar when the keyboard opens. - Bound the sidebar to its minimum width during tablet keyboard-open layout without overwriting the saved width. - Cover visible and user-collapsed tablet sidebar behavior in mobile render tests. - Add a patch changeset for the published Fusion package. Files changed: .changeset/fn-6494-tablet-chat-sidebar.md | 5 ++ packages/dashboard/app/components/ChatView.tsx | 34 +++-------- .../__tests__/ChatView.mobile-render.test.tsx | 70 ++++++++++++++++++++-- 3 files changed, 79 insertions(+), 30 deletions(-) Fusion-Task-Id: FN-6494 Fusion-Task-Lineage: 618cccc3-5102-4e9c-ac0e-6c7a44742940 --- .changeset/fn-6494-tablet-chat-sidebar.md | 5 ++ .../dashboard/app/components/ChatView.tsx | 34 +++------ .../__tests__/ChatView.mobile-render.test.tsx | 70 +++++++++++++++++-- 3 files changed, 79 insertions(+), 30 deletions(-) create mode 100644 .changeset/fn-6494-tablet-chat-sidebar.md diff --git a/.changeset/fn-6494-tablet-chat-sidebar.md b/.changeset/fn-6494-tablet-chat-sidebar.md new file mode 100644 index 0000000000..0e5867c2a9 --- /dev/null +++ b/.changeset/fn-6494-tablet-chat-sidebar.md @@ -0,0 +1,5 @@ +--- +"@runfusion/fusion": patch +--- + +Keep the chat sidebar visible at a compact bounded width when a tablet software keyboard opens, then restore the previous width when the keyboard closes. diff --git a/packages/dashboard/app/components/ChatView.tsx b/packages/dashboard/app/components/ChatView.tsx index ca4313a9f8..88d8cf4413 100644 --- a/packages/dashboard/app/components/ChatView.tsx +++ b/packages/dashboard/app/components/ChatView.tsx @@ -1105,7 +1105,6 @@ export function ChatView({ projectId, addToast, experimentalFeatures }: ChatView // (which would swallow the next real tap and make the button look dead). const handledSendTouchRef = useRef(false); const handledSendTouchTimerRef = useRef(null); - const tabletKeyboardSidebarVisibilityRef = useRef(null); const mode = useViewportMode(); const isMobile = mode === "mobile"; const isTablet = mode === "tablet"; @@ -1222,29 +1221,6 @@ export function ChatView({ projectId, addToast, experimentalFeatures }: ChatView }); const tabletKeyboardOpen = isTablet && keyboardOpen; - useEffect(() => { - if (!isTablet) { - tabletKeyboardSidebarVisibilityRef.current = null; - return; - } - - if (keyboardOpen) { - setSidebarVisible((currentSidebarVisible) => { - if (tabletKeyboardSidebarVisibilityRef.current === null) { - tabletKeyboardSidebarVisibilityRef.current = currentSidebarVisible; - } - return currentSidebarVisible ? false : currentSidebarVisible; - }); - return; - } - - if (tabletKeyboardSidebarVisibilityRef.current !== null) { - const shouldRestoreSidebar = tabletKeyboardSidebarVisibilityRef.current; - tabletKeyboardSidebarVisibilityRef.current = null; - setSidebarVisible(shouldRestoreSidebar); - } - }, [isTablet, keyboardOpen]); - const filteredSkills = useMemo(() => { const normalizedFilter = skillFilter.trim().toLowerCase(); const matchingSkills = normalizedFilter @@ -3050,12 +3026,20 @@ export function ChatView({ projectId, addToast, experimentalFeatures }: ChatView ); + /** + * FNXC:ChatTabletKeyboard 2026-06-16-17:46: + * FN-6494 reverses the FN-6178/FN-6210 tablet-keyboard auto-hide: a visible chat sidebar must stay visible while the software keyboard is up, but use the minimum bounded width so the session list is not too wide in the reduced viewport. The user's persisted width remains untouched and returns when the keyboard closes; mobile keeps CSS-driven one-pane sizing. + */ + const sidebarInlineStyle: React.CSSProperties | undefined = isMobile + ? undefined + : { width: `${tabletKeyboardOpen ? Math.min(sidebarWidth, CHAT_SIDEBAR_MIN_WIDTH) : sidebarWidth}px` }; + return (
{/* Sidebar */}
{chatRoomsEnabled && (
diff --git a/packages/dashboard/app/components/__tests__/ChatView.mobile-render.test.tsx b/packages/dashboard/app/components/__tests__/ChatView.mobile-render.test.tsx index 638c9de5b7..920720bfb1 100644 --- a/packages/dashboard/app/components/__tests__/ChatView.mobile-render.test.tsx +++ b/packages/dashboard/app/components/__tests__/ChatView.mobile-render.test.tsx @@ -347,7 +347,7 @@ describe("FN-5997 mobile chat message pane rendering", () => { } }); - it("auto-hides the tablet sidebar while the software keyboard is open and restores it when closed", async () => { + it("keeps the tablet sidebar visible but narrower while the software keyboard is open, and restores width when closed", async () => { const restoreMatchMedia = mockViewportMode("tablet"); const visualViewport = mockVisualViewport({ width: 900, height: 1112 }); try { @@ -369,23 +369,83 @@ describe("FN-5997 mobile chat message pane rendering", () => { }); await setVisualViewportHeight(visualViewport, 560); - await waitFor(() => expect(sidebar).toHaveClass("chat-sidebar--hidden")); + await waitFor(() => expect(sidebar.style.width).toBe("180px")); + expect(sidebar).not.toHaveClass("chat-sidebar--hidden"); + expect(Number.parseInt(sidebar.style.width, 10)).toBeLessThan(280); + expect(Number.parseInt(sidebar.style.width, 10)).toBeLessThanOrEqual(280); expect(screen.queryByRole("separator", { name: "Resize chat sidebar" })).toBeNull(); - expect(sidebar.style.width).toBe("280px"); await act(async () => { input.blur(); }); await setVisualViewportHeight(visualViewport, 1112); - await waitFor(() => expect(sidebar).not.toHaveClass("chat-sidebar--hidden")); - expect(sidebar.style.width).toBe("280px"); + await waitFor(() => expect(sidebar.style.width).toBe("280px")); + expect(sidebar).not.toHaveClass("chat-sidebar--hidden"); expect(screen.getByRole("separator", { name: "Resize chat sidebar" })).toBeInTheDocument(); } finally { restoreMatchMedia.mockRestore(); } }); + it("keeps a user-collapsed sidebar collapsed across tablet keyboard open and close", async () => { + const restoreMatchMedia = mockViewportMode("mobile"); + const visualViewport = mockVisualViewport({ width: 900, height: 1112 }); + try { + setupChat({ + sessions: [activeSession], + filteredSessions: [activeSession], + activeSession, + }); + await renderWithCss(); + + const sidebar = getSidebar(); + await act(async () => { + screen.getByTestId(`chat-session-${activeSession.id}`).click(); + }); + expect(sidebar).toHaveClass("chat-sidebar--hidden"); + + Object.defineProperty(window, "innerWidth", { value: 900, configurable: true }); + restoreMatchMedia.mockImplementation((query: string) => ({ + matches: query.includes("min-width: 769px") && query.includes("max-width: 1024px"), + media: query, + onchange: null, + addListener: vi.fn(), + removeListener: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + })); + await act(async () => { + window.dispatchEvent(new Event("resize")); + }); + + expect(sidebar).toHaveClass("chat-sidebar--hidden"); + expect(sidebar.style.width).toBe("280px"); + + const input = screen.getByTestId("chat-input") as HTMLTextAreaElement; + await act(async () => { + input.focus(); + }); + await setVisualViewportHeight(visualViewport, 560); + + await waitFor(() => expect(sidebar.style.width).toBe("180px")); + expect(sidebar).toHaveClass("chat-sidebar--hidden"); + expect(screen.queryByRole("separator", { name: "Resize chat sidebar" })).toBeNull(); + + await act(async () => { + input.blur(); + }); + await setVisualViewportHeight(visualViewport, 1112); + + await waitFor(() => expect(sidebar.style.width).toBe("280px")); + expect(sidebar).toHaveClass("chat-sidebar--hidden"); + expect(screen.queryByRole("separator", { name: "Resize chat sidebar" })).toBeNull(); + } finally { + restoreMatchMedia.mockRestore(); + } + }); + it("keeps sidebar width bounded even if viewport mode flickers to mobile during keyboard-open on tablet", async () => { const restoreMatchMedia = mockViewportMode("tablet"); const originalScreenDescriptor = Object.getOwnPropertyDescriptor(window, "screen");