FN-6210: cap chat sidebar width during keyboard flicker
Keep the chat sidebar bounded when tablet keyboard viewport changes briefly report mobile sizing. - Add a desktop max-width guard to the chat sidebar with a mobile override. - Cover simulated tablet keyboard viewport-mode flicker in the mobile render tests. - Document the ChatView sidebar defense alongside the tablet viewport-mode solution. Files changed: .../ui-bugs/tablet-keyboard-viewport-mode-flip.md | 3 ++ packages/dashboard/app/components/ChatView.css | 2 + .../__tests__/ChatView.mobile-render.test.tsx | 53 ++++++++++++++++++++++ 3 files changed, 58 insertions(+) Fusion-Task-Id: FN-6210 Fusion-Task-Lineage: b3aca9d5-dc16-45e2-8f2c-5c2d51c7d021
This commit is contained in:
@@ -42,6 +42,8 @@ Keep the exported `MOBILE_MEDIA_QUERY` string unchanged for listener compatibili
|
||||
|
||||
This preserves landscape-phone behavior while preventing keyboard-driven height shrink from changing tablet/desktop viewport mode. Direct breakpoint consumers (`Board`, `WorkflowNodeEditor`, and `SessionTerminal`) should subscribe to `MOBILE_MEDIA_QUERY` for reactivity but recompute state with `isMobileViewport()` rather than reading `.matches` as the final decision.
|
||||
|
||||
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.
|
||||
|
||||
## Regression coverage
|
||||
|
||||
Cover the invariant rather than the single repro:
|
||||
@@ -52,3 +54,4 @@ Cover the invariant rather than the single repro:
|
||||
- Portrait phone width stays `mobile` regardless of height.
|
||||
- Undefined/zero `window.screen` does not throw and falls back to width-only detection.
|
||||
- Component-local mobile hooks such as `SessionTerminal` also use the guarded predicate.
|
||||
- ChatView sidebar CSS remains bounded at the sidebar max width during simulated viewport-mode flicker while the keyboard is open.
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
/* Sidebar */
|
||||
.chat-sidebar {
|
||||
min-width: 0;
|
||||
max-width: 500px; /* FN-6210: defensive cap — matches CHAT_SIDEBAR_MAX_WIDTH */
|
||||
border-right: 1px solid var(--border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -1725,6 +1726,7 @@
|
||||
.chat-sidebar {
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
max-width: 100%; /* FN-6210: mobile sidebar spans full viewport */
|
||||
height: 100%;
|
||||
max-height: none;
|
||||
border-right: none;
|
||||
|
||||
@@ -386,6 +386,59 @@ describe("FN-5997 mobile chat message pane rendering", () => {
|
||||
}
|
||||
});
|
||||
|
||||
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");
|
||||
const visualViewport = mockVisualViewport({ width: 900, height: 1112 });
|
||||
try {
|
||||
setupChat({
|
||||
sessions: [activeSession],
|
||||
filteredSessions: [activeSession],
|
||||
activeSession,
|
||||
});
|
||||
await renderWithCss(<ChatView projectId="proj-123" addToast={vi.fn()} />);
|
||||
|
||||
const sidebar = getSidebar();
|
||||
expect(sidebar).not.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, 400);
|
||||
|
||||
// Simulate the FN-6213 bug scenario where viewport mode transiently
|
||||
// resolves to mobile on a tablet while the keyboard has shrunk height.
|
||||
Object.defineProperty(window, "screen", { configurable: true, value: { width: 390, height: 844 } });
|
||||
restoreMatchMedia.mockImplementation((query: string) => ({
|
||||
matches:
|
||||
query.includes("max-width: 768px") ||
|
||||
query.includes("max-height: 480px"),
|
||||
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"));
|
||||
});
|
||||
|
||||
await waitFor(() => expect(sidebar.style.width).toBe(""));
|
||||
const maxWidth = parseInt(getComputedStyle(sidebar).maxWidth, 10);
|
||||
expect(maxWidth).toBeLessThanOrEqual(500);
|
||||
expect(sidebar.offsetWidth).toBeLessThanOrEqual(500);
|
||||
} finally {
|
||||
restoreMatchMedia.mockRestore();
|
||||
if (originalScreenDescriptor) {
|
||||
Object.defineProperty(window, "screen", originalScreenDescriptor);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps the desktop sidebar fixed even if visualViewport shrinks while the composer is focused", async () => {
|
||||
const restoreMatchMedia = mockViewportMode("desktop");
|
||||
const visualViewport = mockVisualViewport({ width: 1280, height: 900 });
|
||||
|
||||
Reference in New Issue
Block a user