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:
gsxdsm
2026-06-17 15:49:06 -07:00
parent e48df026c6
commit 98baddc3b6
2 changed files with 98 additions and 0 deletions

View File

@@ -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 ---- */

View File

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