fix(web): lift the mobile jump sheet above the bottom tab bar #154

Merged
tudor merged 2 commits from fix/jump-sheet-under-bottom-bar into main 2026-09-22 19:55:00 +00:00
3 changed files with 62 additions and 1 deletions
Showing only changes of commit 180d6e9b3e - Show all commits

No files matched your search

+46
View File
@@ -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.
*
@@ -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;
@@ -344,7 +344,10 @@ export function SchoolDetailShell({
</header>
{/* Sticky Section Navigation — docks under the global header */}
<nav className={styles.sectionNav} aria-label="Page sections">
<nav
className={`${styles.sectionNav}${sectionsOpen ? ` ${styles.sectionNavSheetOpen}` : ''}`}
aria-label="Page sections"
>
<button onClick={scrollToTop} className={styles.sectionNavBack} aria-label="Back to top">
<span aria-hidden="true">↑</span>
<span className={styles.sectionNavBackLabel}>Top</span>