Compare commits

..
Author SHA1 Message Date
TudorandClaude Fable 5 a00cbe9161 fix(search): stop the mobile sort dropdown overflowing the viewport
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 9m37s
PR Checks / Backend Smoke (pull_request) Successful in 5s
PR Checks / Build Backend (no push) (pull_request) Successful in 10s
PR Checks / Build Frontend (no push) (pull_request) Successful in 44s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 9s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 1m3s
On a location search the results header shows the view toggle and the
sort <select> side by side. The select sizes to its widest option
('Highest Reading, Writing & Maths %', ~273px), so on a phone its right
edge ran ~46px past the viewport and was clipped off-screen.

On mobile let the select flex into the remaining space with min-width:0
so its label truncates instead of overflowing, and keep the view toggle
from shrinking. Verified live at 390px: the select now sits fully within
the viewport.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 13:23:29 +01:00
4 changed files with 17 additions and 71 deletions
-25
View File
@@ -75,31 +75,6 @@ test('school hero map opens fullscreen on mobile without the Fullscreen API', as
await expect(openMap).toBeVisible();
});
test('results map fullscreen falls back to an overlay on iOS', async ({ page }) => {
// Same iOS gap as the hero map: no Element.requestFullscreen, so the results
// map's fullscreen button must fall back to a CSS overlay.
await page.setViewportSize({ width: 390, height: 844 });
await page.addInitScript(() => {
// @ts-expect-error deliberate API removal
delete Element.prototype.requestFullscreen;
});
await searchByName(page, 'B1 1BB');
await expect(schoolLinks(page).first()).toBeVisible({ timeout: 15_000 });
// Switch to the map view, then open the map fullscreen.
await page.getByRole('button', { name: 'Map', exact: true }).click();
const openFs = page.getByRole('button', { name: 'View map fullscreen' });
await expect(openFs).toBeVisible({ timeout: 15_000 });
await openFs.click();
// The button flips to its exit state once the overlay is up.
const exitFs = page.getByRole('button', { name: 'Exit fullscreen' });
await expect(exitFs).toBeVisible();
await exitFs.click();
await expect(openFs).toBeVisible();
});
test('comparing two schools shows both side by side', async ({ page }) => {
// Collect two school URNs from search results, then load the share URL
await searchByName(page, 'primary');
+10
View File
@@ -369,6 +369,16 @@
.viewToggle {
justify-content: center;
flex-shrink: 0;
}
/* The sort <select> sizes to its widest option ("Highest Reading, Writing
& Maths %"), which overflows a phone viewport — beside the view toggle it
ran off the right edge. Let it flex into the remaining space and shrink;
the selected label truncates instead of pushing past the screen. */
.sortSelect {
flex: 1;
min-width: 0;
}
.mapViewContainer {
@@ -10,15 +10,6 @@
height: 100dvh;
}
/* Fallback fullscreen (iOS Safari — no Element.requestFullscreen): the API
can't promote the element, so pin it over the page ourselves. Above the
comparison toast (3000) and the bottom nav; below modals (9999+). */
.mapWrapper.fsFallback {
position: fixed;
inset: 0;
z-index: 5000;
}
.fullscreenBtn {
position: absolute;
top: 0.625rem;
+7 -37
View File
@@ -33,52 +33,22 @@ interface SchoolMapProps {
export function SchoolMap({ schools, center, zoom = 13, referencePoint, onMarkerClick, nationalAvgRwm, laAverages }: SchoolMapProps) {
const wrapperRef = useRef<HTMLDivElement>(null);
const [nativeFullscreen, setNativeFullscreen] = useState(false);
// iOS Safari has no Element.requestFullscreen — fall back to a fixed-position
// overlay driven by state instead of the Fullscreen API.
const [fallbackFullscreen, setFallbackFullscreen] = useState(false);
const isFullscreen = nativeFullscreen || fallbackFullscreen;
const [isFullscreen, setIsFullscreen] = useState(false);
// Sync state with browser fullscreen events (e.g. Escape key)
useEffect(() => {
const onFsChange = () => setNativeFullscreen(!!document.fullscreenElement);
const onFsChange = () => setIsFullscreen(!!document.fullscreenElement);
document.addEventListener('fullscreenchange', onFsChange);
return () => document.removeEventListener('fullscreenchange', onFsChange);
}, []);
// Lock body scroll while the fallback overlay is up.
useEffect(() => {
if (!fallbackFullscreen) return;
const prev = document.body.style.overflow;
document.body.style.overflow = 'hidden';
return () => { document.body.style.overflow = prev; };
}, [fallbackFullscreen]);
// Leaflet re-measures on window resize (trackResize). Native fullscreen fires
// one; the CSS fallback overlay changes size without a resize event, so nudge
// Leaflet after the layout settles or the map fills only part of the screen.
useEffect(() => {
const id = requestAnimationFrame(() => window.dispatchEvent(new Event('resize')));
return () => cancelAnimationFrame(id);
}, [isFullscreen]);
const toggleFullscreen = useCallback(() => {
if (document.fullscreenElement) {
document.exitFullscreen().catch(() => {});
return;
}
if (fallbackFullscreen) {
setFallbackFullscreen(false);
return;
}
const el = wrapperRef.current;
if (!el) return;
if (el.requestFullscreen) {
el.requestFullscreen().catch(() => setFallbackFullscreen(true));
if (!document.fullscreenElement) {
wrapperRef.current?.requestFullscreen();
} else {
setFallbackFullscreen(true);
document.exitFullscreen();
}
}, [fallbackFullscreen]);
}, []);
// Calculate center if not provided
const mapCenter: [number, number] = center || (() => {
@@ -94,7 +64,7 @@ export function SchoolMap({ schools, center, zoom = 13, referencePoint, onMarker
})();
return (
<div ref={wrapperRef} className={`${styles.mapWrapper} ${isFullscreen ? styles.fullscreen : ''} ${fallbackFullscreen ? styles.fsFallback : ''}`}>
<div ref={wrapperRef} className={`${styles.mapWrapper} ${isFullscreen ? styles.fullscreen : ''}`}>
<button
className={styles.fullscreenBtn}
onClick={toggleFullscreen}