FN-6516: preserve tablet chat sidebar width during typing
Keep tablet chat sidebars visible at the persisted width while the software keyboard is open. - Preserve the non-mobile sidebar inline width instead of forcing tablet keyboard sessions to the minimum width.\n- Extend ChatView tablet viewport tests for default, persisted custom, and collapsed-sidebar keyboard states.\n- Document the FN-6516 refinement in the tablet keyboard viewport solution note.\n\nFiles changed:\n .../ui-bugs/tablet-keyboard-viewport-mode-flip.md | 2 +\n packages/dashboard/app/components/ChatView.tsx | 9 ++--\n .../__tests__/ChatView.mobile-render.test.tsx | 55 ++++++++++++++++++----\n 3 files changed, 53 insertions(+), 13 deletions(-) Fusion-Task-Id: FN-6516 Fusion-Task-Lineage: 51927ebb-3a9e-4dc5-827a-33c76a142335
This commit is contained in:
@@ -44,6 +44,8 @@ This preserves landscape-phone behavior while preventing keyboard-driven height
|
||||
|
||||
ChatView also keeps a defense-in-depth CSS guard from FN-6210: `.chat-sidebar` has a non-mobile `max-width` matching `CHAT_SIDEBAR_MAX_WIDTH`, with the mobile media rule overriding it back to `100%`. That guard bounds the sidebar even if viewport-mode state is temporarily wrong and the inline sidebar width is removed.
|
||||
|
||||
FN-6516 refined the FN-6494 keyboard-open behavior: tablet chat sidebars remain visible at the user's current/persisted width while the software keyboard is open, rather than narrowing to the minimum width. Resize controls still stay disabled while typing, collapsed sidebars remain collapsed, and the FN-6210 `max-width` CSS guard remains the upper bound.
|
||||
|
||||
## Regression coverage
|
||||
|
||||
Cover the invariant rather than the single repro:
|
||||
|
||||
@@ -3132,11 +3132,12 @@ 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.
|
||||
* 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. The user's persisted width remains untouched and returns when the keyboard closes; mobile keeps CSS-driven one-pane sizing.
|
||||
*
|
||||
* FNXC:ChatTabletKeyboard 2026-06-16-22:59:
|
||||
* FN-6516 refines the tablet keyboard behavior: keep the sidebar at the same persisted width while the keyboard is open instead of narrowing to the minimum. The FN-6210 CSS max-width guard remains the upper bound, and resize controls still stay disabled while typing.
|
||||
*/
|
||||
const sidebarInlineStyle: React.CSSProperties | undefined = isMobile
|
||||
? undefined
|
||||
: { width: `${tabletKeyboardOpen ? Math.min(sidebarWidth, CHAT_SIDEBAR_MIN_WIDTH) : sidebarWidth}px` };
|
||||
const sidebarInlineStyle: React.CSSProperties | undefined = isMobile ? undefined : { width: `${sidebarWidth}px` };
|
||||
|
||||
return (
|
||||
<div className="chat-view">
|
||||
|
||||
@@ -347,7 +347,7 @@ describe("FN-5997 mobile chat message pane rendering", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps the tablet sidebar visible but narrower while the software keyboard is open, and restores width when closed", async () => {
|
||||
it("keeps the tablet sidebar at the same width while the software keyboard is open", async () => {
|
||||
const restoreMatchMedia = mockViewportMode("tablet");
|
||||
const visualViewport = mockVisualViewport({ width: 900, height: 1112 });
|
||||
try {
|
||||
@@ -369,20 +369,57 @@ describe("FN-5997 mobile chat message pane rendering", () => {
|
||||
});
|
||||
await setVisualViewportHeight(visualViewport, 560);
|
||||
|
||||
await waitFor(() => expect(sidebar.style.width).toBe("180px"));
|
||||
await waitFor(() => expect(screen.queryByRole("separator", { name: "Resize chat sidebar" })).toBeNull());
|
||||
expect(sidebar.style.width).toBe("280px");
|
||||
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();
|
||||
|
||||
await act(async () => {
|
||||
input.blur();
|
||||
});
|
||||
await setVisualViewportHeight(visualViewport, 1112);
|
||||
|
||||
await waitFor(() => expect(sidebar.style.width).toBe("280px"));
|
||||
await waitFor(() => expect(screen.getByRole("separator", { name: "Resize chat sidebar" })).toBeInTheDocument());
|
||||
expect(sidebar.style.width).toBe("280px");
|
||||
expect(sidebar).not.toHaveClass("chat-sidebar--hidden");
|
||||
} finally {
|
||||
restoreMatchMedia.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps a persisted custom tablet sidebar width while the software keyboard is open", async () => {
|
||||
const restoreMatchMedia = mockViewportMode("tablet");
|
||||
const visualViewport = mockVisualViewport({ width: 900, height: 1112 });
|
||||
localStorage.setItem("fusion:chat-sidebar-width", "360");
|
||||
try {
|
||||
setupChat({
|
||||
sessions: [activeSession],
|
||||
filteredSessions: [activeSession],
|
||||
activeSession,
|
||||
});
|
||||
await renderWithCss(<ChatView projectId="proj-123" addToast={vi.fn()} />);
|
||||
|
||||
const sidebar = getSidebar();
|
||||
await waitFor(() => expect(sidebar.style.width).toBe("360px"));
|
||||
expect(sidebar).not.toHaveClass("chat-sidebar--hidden");
|
||||
|
||||
const input = screen.getByTestId("chat-input") as HTMLTextAreaElement;
|
||||
await act(async () => {
|
||||
input.focus();
|
||||
});
|
||||
await setVisualViewportHeight(visualViewport, 560);
|
||||
|
||||
await waitFor(() => expect(screen.queryByRole("separator", { name: "Resize chat sidebar" })).toBeNull());
|
||||
expect(sidebar.style.width).toBe("360px");
|
||||
expect(sidebar).not.toHaveClass("chat-sidebar--hidden");
|
||||
|
||||
await act(async () => {
|
||||
input.blur();
|
||||
});
|
||||
await setVisualViewportHeight(visualViewport, 1112);
|
||||
|
||||
await waitFor(() => expect(screen.getByRole("separator", { name: "Resize chat sidebar" })).toBeInTheDocument());
|
||||
expect(sidebar.style.width).toBe("360px");
|
||||
expect(sidebar).not.toHaveClass("chat-sidebar--hidden");
|
||||
expect(screen.getByRole("separator", { name: "Resize chat sidebar" })).toBeInTheDocument();
|
||||
} finally {
|
||||
restoreMatchMedia.mockRestore();
|
||||
}
|
||||
@@ -429,9 +466,9 @@ describe("FN-5997 mobile chat message pane rendering", () => {
|
||||
});
|
||||
await setVisualViewportHeight(visualViewport, 560);
|
||||
|
||||
await waitFor(() => expect(sidebar.style.width).toBe("180px"));
|
||||
await waitFor(() => expect(screen.queryByRole("separator", { name: "Resize chat sidebar" })).toBeNull());
|
||||
expect(sidebar.style.width).toBe("280px");
|
||||
expect(sidebar).toHaveClass("chat-sidebar--hidden");
|
||||
expect(screen.queryByRole("separator", { name: "Resize chat sidebar" })).toBeNull();
|
||||
|
||||
await act(async () => {
|
||||
input.blur();
|
||||
|
||||
Reference in New Issue
Block a user