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..c519195 100644 --- a/nextjs-app/components/school/SchoolDetailShell.module.css +++ b/nextjs-app/components/school/SchoolDetailShell.module.css @@ -283,6 +283,21 @@ } +/* `position: sticky` with a z-index makes .sectionNav a stacking context, so + the jump sheet's own z-index only orders it INSIDE that context. Against the + fixed bottom tab bar (Navigation.module.css, z-index 1000) what counts is + .sectionNav's 10 — 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: above the bar, below + the comparison toast (2000), the fullscreen map (5000) and the info popover + (9999). This rule carries the z-index and nothing else; every other + declaration belongs to .sectionNav in both states. */ +.sectionNavSheetOpen { + z-index: 1100; +} + + .sectionNavBack { flex: none; display: inline-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 */} -