From 0c7e873eb1ff19c306a05d5ec6ccedbf552ae283 Mon Sep 17 00:00:00 2001
From: jay23606 <4602056+jay23606@users.noreply.github.com>
Date: Fri, 23 Oct 2026 12:34:55 -0400
Subject: [PATCH] fix(map): skip flyTo and zoom animations when reduced motion
is preferre
---
src/components/map/map-view.test.tsx | 29 ++++++++++++++++++++++++
src/components/map/map-view.tsx | 33 +++++++++++++++++++++++-----
2 files changed, 57 insertions(+), 5 deletions(-)
diff --git a/src/components/map/map-view.test.tsx b/src/components/map/map-view.test.tsx
index d081f5d..dbd8269 100644
--- a/src/components/map/map-view.test.tsx
+++ b/src/components/map/map-view.test.tsx
@@ -3,6 +3,8 @@ import { afterEach, describe, expect, it, vi } from "vitest";
import MapView from "@/components/map/map-view";
+const { flyTo } = vi.hoisted(() => ({ flyTo: vi.fn() }));
+
vi.mock("react-leaflet", async () => {
const React = await import("react");
const map = {
@@ -15,6 +17,7 @@ vi.mock("react-leaflet", async () => {
getZoom: () => 18,
getContainer: () => document.createElement("div"),
fitBounds: vi.fn(),
+ flyTo,
scrollWheelZoom: { disable: vi.fn(), enable: vi.fn(), enabled: () => false },
on: vi.fn(),
off: vi.fn(),
@@ -88,4 +91,30 @@ describe("MapView configuration", () => {
screen.getByRole("button", { name: /Heathrow Airport, Airport/ }),
).toBeTruthy();
});
+
+ it("flies to the user without animation when prefers-reduced-motion is set", () => {
+ vi.stubEnv("NEXT_PUBLIC_MAPTILER_KEY", "test-key");
+ vi.stubGlobal(
+ "ResizeObserver",
+ class {
+ observe() {}
+ disconnect() {}
+ },
+ );
+ vi.stubGlobal("matchMedia", (query: string) => ({
+ matches: query === "(prefers-reduced-motion: reduce)",
+ media: query,
+ addEventListener: () => undefined,
+ removeEventListener: () => undefined,
+ }));
+ flyTo.mockClear();
+
+ render();
+
+ expect(flyTo).toHaveBeenCalledWith(
+ [19.2, 72.9],
+ 14,
+ expect.objectContaining({ animate: false }),
+ );
+ });
});
diff --git a/src/components/map/map-view.tsx b/src/components/map/map-view.tsx
index 63385d6..c0ae184 100644
--- a/src/components/map/map-view.tsx
+++ b/src/components/map/map-view.tsx
@@ -77,6 +77,14 @@ function pinIcon(color: string): L.DivIcon {
});
}
+/** True when the visitor has asked for reduced motion. Guarded for SSR and jsdom. */
+function prefersReducedMotion(): boolean {
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
+ return false;
+ }
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
+}
+
function boundsToBbox(bounds: L.LatLngBounds): [number, number, number, number] {
return [bounds.getWest(), bounds.getSouth(), bounds.getEast(), bounds.getNorth()];
}
@@ -152,7 +160,10 @@ function ClusteredMarkers({ places }: { places: Place[] }) {
index.getClusterExpansionZoom(clusterId),
18,
);
- map.flyTo([lat, lng], expansionZoom, { duration: 0.5 });
+ map.flyTo([lat, lng], expansionZoom, {
+ duration: 0.5,
+ animate: !prefersReducedMotion(),
+ });
},
keydown: (e) => {
if (e.originalEvent.key !== "Enter" && e.originalEvent.key !== " ")
@@ -162,7 +173,10 @@ function ClusteredMarkers({ places }: { places: Place[] }) {
index.getClusterExpansionZoom(clusterId),
18,
);
- map.flyTo([lat, lng], expansionZoom, { duration: 0.5 });
+ map.flyTo([lat, lng], expansionZoom, {
+ duration: 0.5,
+ animate: !prefersReducedMotion(),
+ });
},
}}
/>
@@ -263,7 +277,7 @@ function ClosePopupOnTrigger({ trigger }: { trigger: number }) {
function FlyToUser({ lat, lng }: { lat: number; lng: number }) {
const map = useMap();
useEffect(() => {
- map.flyTo([lat, lng], 14, { duration: 1 });
+ map.flyTo([lat, lng], 14, { duration: 1, animate: !prefersReducedMotion() });
}, [map, lat, lng]);
return null;
}
@@ -272,7 +286,7 @@ function FlyToUser({ lat, lng }: { lat: number; lng: number }) {
function FlyToPlace({ id, lat, lng }: { id: string; lat: number; lng: number }) {
const map = useMap();
useEffect(() => {
- map.flyTo([lat, lng], 15, { duration: 1 });
+ map.flyTo([lat, lng], 15, { duration: 1, animate: !prefersReducedMotion() });
}, [map, id, lat, lng]);
return null;
}
@@ -307,7 +321,12 @@ function FlyToBounds({ bounds }: { bounds: Bounds }) {
[bounds.minLat, bounds.minLng],
[bounds.maxLat, bounds.maxLng],
],
- { padding: [48, 48], duration: 1, maxZoom: 15 },
+ {
+ padding: [48, 48],
+ duration: 1,
+ maxZoom: 15,
+ animate: !prefersReducedMotion(),
+ },
);
}, [map, bounds]);
return null;
@@ -458,6 +477,7 @@ export default function MapView({
}
const focusPlace = focusId ? places.find((place) => place.id === focusId) : undefined;
+ const reduceMotion = prefersReducedMotion();
// Same colorful MapTiler "streets" style in both themes — every dark-mode
// map style (CARTO, MapTiler, Mapbox alike) is conventionally muted for
@@ -486,6 +506,9 @@ export default function MapView({
zoomControl={interactive}
attributionControl={interactive}
closePopupOnClick={false}
+ zoomAnimation={!reduceMotion}
+ fadeAnimation={!reduceMotion}
+ markerZoomAnimation={!reduceMotion}
className="size-full"
>