commit eeebd3a4190826f2b2b46008cc7580fd2de57ee5
parent 1d684d1b1216cbbc566e7ba68bd22587702a14f1
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Thu, 24 Sep 2026 19:21:36 -0400
channels: the rack is its own stacking context
Audit finding (mobile deck-over-thead, 390x640): the scroll region was
`relative` with no z-index, so it formed no stacking context and every
layer inside it — the z-30 thead, the pinned cells — competed with the
page itself. Below md the selection deck pins to the screen at z-20, and
the thead scrolling under it painted over the deck's first line
("18 selected", the tier select). Before-shot:
p-shots-c-before/mobile-deck-over-thead.png.
`isolate` on the region orders the whole ladder inside it; the deck,
later in the DOM and positioned, paints over the region as a unit.
Also in this commit, both rack specs wait for hydration (the stations
are disabled until mount; --thead-h, --rack-w and the popover's
onToggle only exist after it) — mid-run 2 measured the group header
and clicked Advanced on server HTML. The deck-over-thead shot uses a
640 px window so the header can reach the deck at all.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
3 files changed, 33 insertions(+), 14 deletions(-)
diff --git a/editor/app/channels/components/ChannelsRack.tsx b/editor/app/channels/components/ChannelsRack.tsx
@@ -209,11 +209,16 @@ export function ChannelsRack({
Slug cells pin to its left, and the sixteen columns move underneath
them. The table itself must NOT clip (`overflow-hidden` would make it
the sticky ancestor and nothing would pin) — the rounded corners are
- the region's. */}
+ the region's.
+ `isolate` makes the region its own stacking context, so the whole
+ ladder (rackLayout.ts) is ordered INSIDE it: without it the pinned
+ cells and the z-30 thead competed with the page itself, and below
+ md the thead scrolling under the screen-pinned selection deck
+ (z-20) painted over it. */}
<div
ref={regionRef}
data-testid="channels-rack"
- className="relative -mx-4 min-h-0 flex-1 overflow-auto border-y border-border md:mx-0 md:rounded-md md:border"
+ className="relative isolate -mx-4 min-h-0 flex-1 overflow-auto border-y border-border md:mx-0 md:rounded-md md:border"
>
<ChannelsTable
rows={channels}
diff --git a/editor/e2e/channels-rack-audit.spec.ts b/editor/e2e/channels-rack-audit.spec.ts
@@ -75,6 +75,10 @@ async function open(page: Page) {
await seed();
await page.goto("/channels?site=alpha");
await expect(page.getByRole("link", { name: "rack-16" })).toBeVisible();
+ // HYDRATED, not just painted: the stations are disabled until mount, and the
+ // rack's measured custom properties (--thead-h, --rack-w) and every
+ // onToggle exist only from then on. A click before it lands on server HTML.
+ await expect(page.getByLabel("sync group News")).toBeEnabled();
}
for (const vp of VIEWPORTS) {
@@ -109,16 +113,21 @@ for (const vp of VIEWPORTS) {
});
// Below md the deck pins to the SCREEN while the document scrolls: park
- // the column header just above the bottom edge, under the deck, to see
- // which paints on top.
+ // the column header under the deck's top edge to see which paints on top.
+ // A shorter window, because at 844 px the header sits above where the
+ // deck starts before the page has scrolled at all.
test(`${vp.name} deck open over the column header`, async ({ page }) => {
+ await page.setViewportSize({ width: vp.width, height: 640 });
await open(page);
await page.getByLabel("select all channels").check();
- await expect(page.getByLabel("channel priority bulk")).toBeVisible();
+ const deck = page.getByLabel("channel priority bulk");
+ await expect(deck).toBeVisible();
await page.evaluate(() => {
- const thead = document.querySelector("thead")!;
- const top = thead.getBoundingClientRect().top + window.scrollY;
- window.scrollTo(0, Math.max(0, top - window.innerHeight + 60));
+ const deckTop = document
+ .querySelector('[aria-label="channel priority bulk"]')!
+ .getBoundingClientRect().top;
+ const theadTop = document.querySelector("thead")!.getBoundingClientRect().top;
+ window.scrollBy(0, theadTop - (deckTop + 12));
});
await page.waitForTimeout(250);
await shot(page, `${vp.name}-deck-over-thead`);
diff --git a/editor/e2e/channels-rack-layers.spec.ts b/editor/e2e/channels-rack-layers.spec.ts
@@ -19,6 +19,14 @@ const EXTRA = Array.from(
(_, i) => `rack-${String(i + 1).padStart(2, "0")}`,
);
+// Seeded, loaded and HYDRATED: the stations are disabled until mount, and the
+// measured --thead-h / --rack-w and the popover's onToggle exist only after it.
+async function open(page: Page) {
+ await seed();
+ await page.goto("/channels?site=alpha");
+ await expect(page.getByLabel("sync group News")).toBeEnabled();
+}
+
async function seed() {
await resetData("two-slow-channels");
for (const slug of EXTRA) await writeChannelConfig(slug);
@@ -58,8 +66,7 @@ test.use({ viewport: { width: 1280, height: 720 } });
test("scrolled to the bottom-right, the pinned group header paints over the pinned identity cells", async ({
page,
}) => {
- await seed();
- await page.goto("/channels?site=alpha");
+ await open(page);
const region = page.getByTestId("channels-rack");
await expect(region).toBeVisible();
@@ -140,8 +147,7 @@ test("scrolled to the bottom-right, the pinned group header paints over the pinn
test("the Advanced priority popover paints over the rows, pins and headers around it", async ({
page,
}) => {
- await seed();
- await page.goto("/channels?site=alpha");
+ await open(page);
// A row with rows under it: the panel overlays them, and their pinned
// identity cells, which is the overlap the ladder orders.
@@ -163,8 +169,7 @@ test("the Advanced priority popover paints over the rows, pins and headers aroun
test("an Advanced popover opened on the last row scrolls itself into view", async ({
page,
}) => {
- await seed();
- await page.goto("/channels?site=alpha");
+ await open(page);
const region = page.getByTestId("channels-rack");
// slow-a sorts last: its panel opens past the region's bottom edge.
await page.getByLabel("advanced priority for slow-a").click();