Compare commits

...
Author SHA1 Message Date
TudorandClaude Fable 5 b2dc4d0779 fix(map): results map fullscreen falls back to an overlay on iOS
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 45s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 10s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 1m4s
The results-view map's fullscreen button called requestFullscreen(),
which iOS Safari doesn't implement (fullscreen is video-only there), so
tapping it did nothing on iPhones — the same gap already fixed for the
school hero map.

When the Fullscreen API is missing or its promise rejects, fall back to
a fixed-position overlay (.fsFallback, z-index 5000) driven by state,
locking body scroll while open. Leaflet re-measures on window resize, so
dispatch a resize when fullscreen toggles (the CSS overlay fires none) or
the map would fill only part of the screen. Native fullscreen is
unchanged.

New e2e journey deletes Element.requestFullscreen on a mobile viewport,
opens the results map fullscreen, and asserts the exit control appears
then releases; it fails against current production, reproducing the bug.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 14:09:37 +01:00
tudor 5c39131b50 Merge pull request 'chore: remove the Ofsted Parent View feature end to end' (#13) from chore/remove-parent-view into main
Deploy (staging -> E2E gate -> production) / Build Backend (FastAPI) (push) Successful in 20s
Deploy (staging -> E2E gate -> production) / Build Frontend (Next.js) (push) Successful in 47s
Deploy (staging -> E2E gate -> production) / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 1m14s
Deploy (staging -> E2E gate -> production) / Deploy to Staging (push) Successful in 1s
Deploy (staging -> E2E gate -> production) / E2E Journeys against Staging (push) Failing after 1m7s
Deploy (staging -> E2E gate -> production) / Promote to Production (push) Has been skipped
Reviewed-on: #13
2026-07-06 08:31:00 +00:00
3 changed files with 72 additions and 8 deletions
+25
View File
@@ -75,6 +75,31 @@ test('school hero map opens fullscreen on mobile without the Fullscreen API', as
await expect(openMap).toBeVisible(); 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 }) => { test('comparing two schools shows both side by side', async ({ page }) => {
// Collect two school URNs from search results, then load the share URL // Collect two school URNs from search results, then load the share URL
await searchByName(page, 'primary'); await searchByName(page, 'primary');
@@ -10,6 +10,15 @@
height: 100dvh; 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 { .fullscreenBtn {
position: absolute; position: absolute;
top: 0.625rem; top: 0.625rem;
+38 -8
View File
@@ -33,22 +33,52 @@ interface SchoolMapProps {
export function SchoolMap({ schools, center, zoom = 13, referencePoint, onMarkerClick, nationalAvgRwm, laAverages }: SchoolMapProps) { export function SchoolMap({ schools, center, zoom = 13, referencePoint, onMarkerClick, nationalAvgRwm, laAverages }: SchoolMapProps) {
const wrapperRef = useRef<HTMLDivElement>(null); const wrapperRef = useRef<HTMLDivElement>(null);
const [isFullscreen, setIsFullscreen] = useState(false); 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;
// Sync state with browser fullscreen events (e.g. Escape key) // Sync state with browser fullscreen events (e.g. Escape key)
useEffect(() => { useEffect(() => {
const onFsChange = () => setIsFullscreen(!!document.fullscreenElement); const onFsChange = () => setNativeFullscreen(!!document.fullscreenElement);
document.addEventListener('fullscreenchange', onFsChange); document.addEventListener('fullscreenchange', onFsChange);
return () => document.removeEventListener('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(() => { const toggleFullscreen = useCallback(() => {
if (!document.fullscreenElement) { if (document.fullscreenElement) {
wrapperRef.current?.requestFullscreen(); document.exitFullscreen().catch(() => {});
} else { return;
document.exitFullscreen();
} }
}, []); if (fallbackFullscreen) {
setFallbackFullscreen(false);
return;
}
const el = wrapperRef.current;
if (!el) return;
if (el.requestFullscreen) {
el.requestFullscreen().catch(() => setFallbackFullscreen(true));
} else {
setFallbackFullscreen(true);
}
}, [fallbackFullscreen]);
// Calculate center if not provided // Calculate center if not provided
const mapCenter: [number, number] = center || (() => { const mapCenter: [number, number] = center || (() => {
@@ -64,7 +94,7 @@ export function SchoolMap({ schools, center, zoom = 13, referencePoint, onMarker
})(); })();
return ( return (
<div ref={wrapperRef} className={`${styles.mapWrapper} ${isFullscreen ? styles.fullscreen : ''}`}> <div ref={wrapperRef} className={`${styles.mapWrapper} ${isFullscreen ? styles.fullscreen : ''} ${fallbackFullscreen ? styles.fsFallback : ''}`}>
<button <button
className={styles.fullscreenBtn} className={styles.fullscreenBtn}
onClick={toggleFullscreen} onClick={toggleFullscreen}