From 180d6e9b3eb654a85ba74eaac5dfbf9caaac329c Mon Sep 17 00:00:00 2001 From: Tudor Date: Tue, 22 Sep 2026 20:35:25 +0100 Subject: [PATCH] fix(web): lift the jump sheet above the bottom tab bar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On mobile the last item in "Jump to section" was painted over by the fixed bottom tab bar and could not be tapped. Reported as nearby schools missing from the menu; it was there, underneath the bar. The sticky nav sets `position: sticky` with `z-index: 10`, which makes it a stacking context. The sheet's own `z-index: 1600` therefore orders it only inside that context — against the tab bar (z-index 1000) the nav's 10 is what counts, so the bar wins. Verified on staging: every menu item returns itself from elementFromPoint except the last, which returns the tab bar. Latent rather than new. With five sections the list stopped just above the bar; "Nearby schools" made six, and the sixth is the first to reach it. Any section added later would have done the same. Lifted only while the sheet is open, and only to 1100 — above the bar, below the comparison toast (2000), the fullscreen map (5000) and the info popover (9999). The backdrop rises with it, so tapping over the bar now dismisses the sheet instead of navigating away. The journey asks what a thumb asks: for each item, whether it is the topmost element at its own centre. A bounding-box check cannot see this — the item is in the viewport and the right size, just underneath something. Confirmed to fail against current staging, naming "Nearby schools", before the fix. Co-Authored-By: Claude Opus 5 --- e2e/tests/journeys.spec.ts | 46 +++++++++++++++++++ .../school/SchoolDetailShell.module.css | 12 +++++ .../components/school/SchoolDetailShell.tsx | 5 +- 3 files changed, 62 insertions(+), 1 deletion(-) diff --git a/e2e/tests/journeys.spec.ts b/e2e/tests/journeys.spec.ts index 65d3812..e4060de 100644 --- a/e2e/tests/journeys.spec.ts +++ b/e2e/tests/journeys.spec.ts @@ -2826,6 +2826,52 @@ test('nearby schools link on to other schools and into compare', async ({ page } expect(Math.abs(offsetAfter - offsetBefore)).toBeLessThanOrEqual(8); }); +/** + * Every section in the mobile jump sheet can actually be reached. + * + * The sheet is a fixed bottom sheet, and the app has a fixed bottom tab bar. + * `position: sticky` with a z-index on the sticky nav makes it a stacking + * context, so the sheet's own z-index orders it only within that context — + * against the tab bar, the nav's value is what counts. The last item in the + * sheet was therefore painted over and untappable as soon as the list grew + * long enough to reach the bar, which adding "Nearby schools" is what did. + * + * Bounding boxes are not enough to catch this: the item is in the viewport and + * the right size, it is simply underneath something. So this asks the question + * a thumb asks — what is on top at this point. + */ +test('every section in the mobile jump sheet is tappable, not under the tab bar', async ({ page }) => { + await page.setViewportSize({ width: 390, height: 844 }); + await searchByName(page, 'Primary'); + await schoolLinks(page).first().click(); + await page.waitForURL(/\/school\//); + + // Scroll down so the sticky nav is docked and the sheet has somewhere to open. + await page.evaluate(() => window.scrollTo({ top: 1200 })); + // Two controls carry aria-haspopup: the mobile "Section" button and the + // desktop "All" one, which is display:none here but still in the DOM. + await page.locator('[aria-haspopup="menu"]:visible').click(); + + const sheet = page.locator('[role="menu"]'); + await expect(sheet).toBeVisible(); + + const covered = await sheet.evaluate((panel: HTMLElement) => + Array.from(panel.querySelectorAll('[role="menuitem"]')) + .map((el) => { + const box = el.getBoundingClientRect(); + const hit = document.elementFromPoint( + Math.round(box.left + box.width / 2), + Math.round(box.top + box.height / 2), + ); + return { label: (el as HTMLElement).innerText.trim().replace(/\s+/g, ' '), reachable: !!(hit && hit.closest('[role="menuitem"]')) }; + }) + .filter((item) => !item.reachable) + .map((item) => item.label), + ); + + expect(covered).toEqual([]); +}); + /** * The nearby-schools section at MOBILE.md's three reference widths. * diff --git a/nextjs-app/components/school/SchoolDetailShell.module.css b/nextjs-app/components/school/SchoolDetailShell.module.css index 1e0ee6f..dcc4f9c 100644 --- a/nextjs-app/components/school/SchoolDetailShell.module.css +++ b/nextjs-app/components/school/SchoolDetailShell.module.css @@ -270,11 +270,23 @@ position: sticky; top: 64px; /* global header height on desktop */ z-index: 10; + /* `position: sticky` with a z-index makes this a stacking context, so the + jump sheet's own z-index only orders it INSIDE here. Against the fixed + bottom tab bar (Navigation.module.css, z-index 1000) what counts is this + value — which is why the sheet's last item was painted over, and + untappable, once the list grew long enough to reach the bar. Lifted only + while the sheet is open, and only to 1100: below the comparison toast + (2000), the fullscreen map (5000) and the info popover (9999). */ background: var(--bg-card); border: 1px solid var(--border); border-top: none; border-radius: 0 0 10px 10px; padding: 0.5rem 0.75rem; +} + + +.sectionNavSheetOpen { + z-index: 1100; margin-bottom: 1rem; box-shadow: 0 2px 4px rgba(var(--shadow-rgb), 0.04); display: flex; diff --git a/nextjs-app/components/school/SchoolDetailShell.tsx b/nextjs-app/components/school/SchoolDetailShell.tsx index 04e7e52..ad81630 100644 --- a/nextjs-app/components/school/SchoolDetailShell.tsx +++ b/nextjs-app/components/school/SchoolDetailShell.tsx @@ -344,7 +344,10 @@ export function SchoolDetailShell({ {/* Sticky Section Navigation — docks under the global header */} -