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");