FN-6595: fix mobile Command Center scrolling
Keep Command Center content scrollable inside the mobile dashboard shell. - Let the Command Center fill the flex parent without forcing the page taller than the viewport. - Make the tab panel the vertical scroll owner while keeping the header and tabs pinned. - Add regression coverage for the scroll-owner contract across mobile and non-mobile breakpoints. Files changed: .../components/command-center/CommandCenter.css | 20 ++++++ .../__tests__/CommandCenter.mobile-scroll.test.tsx | 78 ++++++++++++++++++++++ 2 files changed, 98 insertions(+) Fusion-Task-Id: FN-6595 Fusion-Task-Lineage: 1d069dff-60af-4756-9ad4-27a48dad0e86
This commit is contained in:
@@ -4,13 +4,21 @@
|
||||
|
||||
.command-center {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: var(--space-4, 1rem);
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
padding: var(--space-4, 1rem);
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:CommandCenter 2026-06-17-00:00:
|
||||
The Command Center must remain scrollable on mobile inside the overflow-hidden .project-content flex parent. Keep the header and tablist pinned while .cc-tabpanel owns vertical scrolling and safe-area bottom clearance.
|
||||
*/
|
||||
.cc-header {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3, 0.75rem);
|
||||
@@ -27,6 +35,7 @@
|
||||
/* ---- Tabs ---- */
|
||||
.cc-tablist {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-1, 0.25rem);
|
||||
border-bottom: 1px solid var(--border-subtle, rgba(127, 127, 127, 0.25));
|
||||
@@ -54,7 +63,18 @@
|
||||
}
|
||||
|
||||
.cc-tabpanel {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
outline: none;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.cc-tabpanel {
|
||||
padding-bottom: calc(var(--space-4, 1rem) + env(safe-area-inset-bottom, 0px) + var(--standalone-bottom-gap));
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Overview ---- */
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import "@testing-library/jest-dom";
|
||||
import { loadStylesCss } from "../../../test/cssFixture";
|
||||
import { CommandCenter } from "../CommandCenter";
|
||||
|
||||
function injectCommandCenterCss() {
|
||||
document.head.querySelector("style[data-testid='fn-6595-css']")?.remove();
|
||||
const style = document.createElement("style");
|
||||
style.setAttribute("data-testid", "fn-6595-css");
|
||||
style.textContent = [
|
||||
loadStylesCss(),
|
||||
readFileSync(join(__dirname, "..", "CommandCenter.css"), "utf-8"),
|
||||
].join("\n");
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
function mockMobileMatchMedia(matchesMobile: boolean) {
|
||||
Object.defineProperty(window, "matchMedia", {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: vi.fn().mockImplementation((query: string) => ({
|
||||
matches: matchesMobile && query.includes("max-width: 768px"),
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
function assertScrollOwnerContract(panel: HTMLElement) {
|
||||
const shell = screen.getByTestId("command-center");
|
||||
const header = shell.querySelector(".cc-header") as HTMLElement;
|
||||
const tablist = screen.getByRole("tablist");
|
||||
|
||||
const shellStyle = window.getComputedStyle(shell);
|
||||
const panelStyle = window.getComputedStyle(panel);
|
||||
|
||||
expect(shellStyle.flexGrow).toBe("1");
|
||||
expect(shellStyle.minHeight).toBe("0px");
|
||||
expect(panelStyle.minHeight).toBe("0px");
|
||||
expect(panelStyle.overflowY).toBe("auto");
|
||||
expect(window.getComputedStyle(header).flexShrink).toBe("0");
|
||||
expect(window.getComputedStyle(tablist).flexShrink).toBe("0");
|
||||
}
|
||||
|
||||
describe("CommandCenter mobile scroll regression (FN-6595)", () => {
|
||||
beforeEach(() => {
|
||||
injectCommandCenterCss();
|
||||
mockMobileMatchMedia(true);
|
||||
});
|
||||
|
||||
it("keeps the tabpanel as the mobile scroll owner with pinned header and tabs", () => {
|
||||
render(<CommandCenter />);
|
||||
|
||||
const overviewPanel = screen.getByTestId("command-center-panel-overview");
|
||||
expect(screen.getByTestId("command-center-empty")).toBeTruthy();
|
||||
assertScrollOwnerContract(overviewPanel);
|
||||
|
||||
fireEvent.click(screen.getByTestId("command-center-tab-tokens"));
|
||||
const tokensPanel = screen.getByTestId("command-center-panel-tokens");
|
||||
expect(tokensPanel).toBe(screen.getByRole("tabpanel"));
|
||||
assertScrollOwnerContract(tokensPanel);
|
||||
});
|
||||
|
||||
it("keeps the same flex-fill scroll-owner contract outside the mobile breakpoint", () => {
|
||||
mockMobileMatchMedia(false);
|
||||
render(<CommandCenter />);
|
||||
|
||||
assertScrollOwnerContract(screen.getByTestId("command-center-panel-overview"));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user