refactor(header): move --hdr-h measurement into PageShell (S7 review)
Addresses the S7 mobile-fix code-review findings: - (altitude) The header-height→content-pad contract lived across two files with an implicit DOM coupling: Header measured itself and wrote --hdr-h onto `el.parentElement`, assuming PageShell rendered it as a direct child of the padded column. Move the whole contract into PageShell, which owns BOTH the header slot and the padded content wrapper: Header is now forwardRef and PageShell attaches a ref (via cloneElement), measures the header with a ResizeObserver, and writes --hdr-h straight onto the content wrapper that consumes it — no parent assumption, producer and consumer together. - (simplification) The `+ 8` gap is now documented as the header's own gap-y-2 row gap reused below it, not a bare magic number. - (test-coverage) PageShell.dom.test.tsx: stub ResizeObserver + the header's offset metrics and assert --hdr-h is published onto the content wrapper (and absent with no header). Re-verified live at 440 (mobile, no overlap) and desktop (one row). FE gate: tsc / knip / lint clean, 220 vitest pass.
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
// @vitest-environment jsdom
|
||||
//
|
||||
// E5 S7: PageShell measures the floating header and publishes its real height as `--hdr-h` onto the
|
||||
// content wrapper, so a header that flex-wraps to 2–3 rows on a narrow window never overlaps the
|
||||
// content below it. jsdom does no layout (offsetHeight is 0) and has no ResizeObserver, so we stub
|
||||
// both: a ResizeObserver that hands us its callback, and offset metrics on the measured header.
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { render, cleanup } from "@testing-library/react";
|
||||
import PageShell from "./PageShell";
|
||||
|
||||
vi.mock("./PageScroller", () => ({
|
||||
default: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
}));
|
||||
|
||||
let roCb: (() => void) | null = null;
|
||||
beforeEach(() => {
|
||||
roCb = null;
|
||||
vi.stubGlobal(
|
||||
"ResizeObserver",
|
||||
class {
|
||||
constructor(cb: () => void) {
|
||||
roCb = cb;
|
||||
}
|
||||
observe() {}
|
||||
disconnect() {}
|
||||
},
|
||||
);
|
||||
});
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
describe("PageShell --hdr-h (E5 S7)", () => {
|
||||
it("publishes the measured header height onto the content wrapper", () => {
|
||||
const { container } = render(
|
||||
<PageShell header={<div data-testid="hdr">H</div>}>content</PageShell>,
|
||||
);
|
||||
const content = container.querySelector('[class*="pt-(--hdr-h)"]') as HTMLElement;
|
||||
expect(content).toBeTruthy();
|
||||
// Give the header a real (multi-row) box, then fire the ResizeObserver callback the effect made.
|
||||
const hdr = container.querySelector('[data-testid="hdr"]') as HTMLElement;
|
||||
Object.defineProperty(hdr, "offsetTop", { value: 12, configurable: true });
|
||||
Object.defineProperty(hdr, "offsetHeight", { value: 96, configurable: true });
|
||||
roCb?.();
|
||||
expect(content.style.getPropertyValue("--hdr-h")).toBe("116px"); // 12 + 96 + 8
|
||||
});
|
||||
|
||||
it("adds no `--hdr-h` pad when there is no header", () => {
|
||||
const { container } = render(<PageShell>content</PageShell>);
|
||||
expect(container.querySelector('[class*="pt-(--hdr-h)"]')).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user