Merge pull request 'fix(school): make hero map fullscreen work on iOS Safari' (#10) from fix/hero-map-ios-fullscreen into main
Deploy (staging -> E2E gate -> production) / Build Backend (FastAPI) (push) Successful in 13s
Deploy (staging -> E2E gate -> production) / Build Frontend (Next.js) (push) Successful in 52s
Deploy (staging -> E2E gate -> production) / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 12s
Deploy (staging -> E2E gate -> production) / Deploy to Staging (push) Successful in 1s
Deploy (staging -> E2E gate -> production) / E2E Journeys against Staging (push) Successful in 34s
Deploy (staging -> E2E gate -> production) / Promote to Production (push) Successful in 8s

Reviewed-on: #10
This commit was merged in pull request #10.
This commit is contained in:
2026-07-05 20:52:32 +00:00
3 changed files with 63 additions and 4 deletions
+25
View File
@@ -50,6 +50,31 @@ test('school detail page renders name and performance data', async ({ page }) =>
await expect(page.locator('canvas:visible').first()).toBeVisible({ timeout: 15_000 }); await expect(page.locator('canvas:visible').first()).toBeVisible({ timeout: 15_000 });
}); });
test('school hero map opens fullscreen on mobile without the Fullscreen API', async ({ page }) => {
// iOS Safari has no Element.requestFullscreen; the map must fall back to a
// CSS overlay. Simulate that by removing the API before any page script runs.
await page.setViewportSize({ width: 390, height: 844 });
await page.addInitScript(() => {
// @ts-expect-error deliberate API removal
delete Element.prototype.requestFullscreen;
});
await searchByName(page, 'primary');
const firstSchool = schoolLinks(page).first();
await expect(firstSchool).toBeVisible({ timeout: 15_000 });
await firstSchool.click();
await page.waitForURL(/\/school\//);
const openMap = page.getByRole('button', { name: 'Open full map' });
await expect(openMap).toBeVisible({ timeout: 15_000 });
await openMap.click();
const closeMap = page.getByRole('button', { name: 'Close map' });
await expect(closeMap).toBeVisible();
await closeMap.click();
await expect(openMap).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');
@@ -34,6 +34,15 @@
background: #fff; background: #fff;
} }
/* 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 everything else except modals (9999+). */
.wrapper[data-fs-fallback] {
position: fixed;
inset: 0;
z-index: 5000;
}
.skeleton { .skeleton {
width: 100%; width: 100%;
height: 100%; height: 100%;
+29 -4
View File
@@ -29,25 +29,50 @@ interface SchoolHeroMapProps {
export const SchoolHeroMap = forwardRef<SchoolHeroMapHandle, SchoolHeroMapProps>( export const SchoolHeroMap = forwardRef<SchoolHeroMapHandle, SchoolHeroMapProps>(
function SchoolHeroMap({ lat, lng }, ref) { function SchoolHeroMap({ lat, lng }, ref) {
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;
const open = useCallback(() => { const open = useCallback(() => {
wrapperRef.current?.requestFullscreen?.().catch(() => {}); const el = wrapperRef.current;
if (!el) return;
if (el.requestFullscreen) {
el.requestFullscreen().catch(() => setFallbackFullscreen(true));
} else {
setFallbackFullscreen(true);
}
}, []); }, []);
const close = useCallback(() => { const close = useCallback(() => {
if (document.fullscreenElement) document.exitFullscreen().catch(() => {}); if (document.fullscreenElement) document.exitFullscreen().catch(() => {});
setFallbackFullscreen(false);
}, []); }, []);
useImperativeHandle(ref, () => ({ open }), [open]); useImperativeHandle(ref, () => ({ open }), [open]);
useEffect(() => { useEffect(() => {
const onChange = () => setIsFullscreen(!!document.fullscreenElement); const onChange = () => setNativeFullscreen(!!document.fullscreenElement);
document.addEventListener('fullscreenchange', onChange); document.addEventListener('fullscreenchange', onChange);
return () => document.removeEventListener('fullscreenchange', onChange); return () => document.removeEventListener('fullscreenchange', onChange);
}, []); }, []);
// The fallback overlay sits on top of the page rather than replacing it,
// so lock body scroll while it is up.
useEffect(() => {
if (!fallbackFullscreen) return;
const prev = document.body.style.overflow;
document.body.style.overflow = 'hidden';
return () => { document.body.style.overflow = prev; };
}, [fallbackFullscreen]);
return ( return (
<div ref={wrapperRef} className={styles.wrapper} data-fullscreen={isFullscreen || undefined}> <div
ref={wrapperRef}
className={styles.wrapper}
data-fullscreen={isFullscreen || undefined}
data-fs-fallback={fallbackFullscreen || undefined}
>
<LeafletHeroMap lat={lat} lng={lng} interactive={isFullscreen} /> <LeafletHeroMap lat={lat} lng={lng} interactive={isFullscreen} />
{isFullscreen ? ( {isFullscreen ? (