@@ -23,6 +23,7 @@ let mapInstance: import('leaflet').Map | null = null;
2323let leafletRef: typeof import (' leaflet' ) | null = null ;
2424const markerMap = new Map <string , import (' leaflet' ).Marker >();
2525let openPopupNodeId: string | null = null ;
26+ let slidingWindowHandler: ((e : FocusEvent ) => void ) | null = null ;
2627
2728// --- Tile style config ---
2829interface TileLayerConfig { url: string ; options: Record <string , unknown >; }
@@ -62,6 +63,7 @@ const MAP_STYLES: MapStyle[] = [
6263];
6364
6465const STORAGE_KEY = ' pcd-map-style' ;
66+ const SLIDING_WINDOW_MARGIN = 0.28 ; // 28% dead zone inset from each edge
6567let activeTileLayers: import (' leaflet' ).TileLayer [] = [];
6668let themeTransitionTimer: number | null = null ;
6769
@@ -137,6 +139,35 @@ function closeList() {
137139 listOpen .value = false ;
138140}
139141
142+ function panToKeepInView(lat : number , lng : number ): void {
143+ if (! mapInstance ) return ;
144+ if (selectedNode .value !== null ) return ;
145+ if (openPopupNodeId !== null ) return ;
146+
147+ const reduceMotion = window .matchMedia (' (prefers-reduced-motion: reduce)' ).matches ;
148+ const containerPoint = mapInstance .latLngToContainerPoint ([lat , lng ]);
149+ const size = mapInstance .getSize ();
150+
151+ const mx = size .x * SLIDING_WINDOW_MARGIN ;
152+ const my = size .y * SLIDING_WINDOW_MARGIN ;
153+
154+ let dx = 0 ;
155+ let dy = 0 ;
156+
157+ if (containerPoint .x < mx ) dx = containerPoint .x - mx ;
158+ else if (containerPoint .x > size .x - mx ) dx = containerPoint .x - (size .x - mx );
159+
160+ if (containerPoint .y < my ) dy = containerPoint .y - my ;
161+ else if (containerPoint .y > size .y - my ) dy = containerPoint .y - (size .y - my );
162+
163+ if (dx !== 0 || dy !== 0 ) {
164+ mapInstance .panBy ([dx , dy ], {
165+ animate: ! reduceMotion ,
166+ duration: reduceMotion ? 0 : 0.3 ,
167+ });
168+ }
169+ }
170+
140171function onNodeSelect(node : Node ) {
141172 selectedNode .value = null ;
142173
@@ -385,6 +416,22 @@ onMounted(async () => {
385416 });
386417 });
387418
419+ // Sliding window: pan just enough to keep focused markers in the safe zone
420+ slidingWindowHandler = (e : FocusEvent ) => {
421+ const target = e .target as HTMLElement ;
422+ if (! target .classList .contains (' marker-node' )) return ;
423+
424+ let foundNode: Node | undefined ;
425+ markerMap .forEach ((marker , id ) => {
426+ if (marker .getElement () === target ) {
427+ foundNode = props .nodes .find (n => n .id === id );
428+ }
429+ });
430+
431+ if (foundNode ) panToKeepInView (foundNode .lat , foundNode .lng );
432+ };
433+ map .getContainer ().addEventListener (' focusin' , slidingWindowHandler );
434+
388435 // Move focus into popup content when it opens
389436 map .on (' popupopen' , (e ) => {
390437 const container = e .popup .getElement ();
@@ -442,6 +489,10 @@ onUnmounted(() => {
442489 }
443490 document .documentElement .classList .remove (' theme-transition' );
444491 document .removeEventListener (' keydown' , handleKeydown );
492+ if (slidingWindowHandler && mapInstance ) {
493+ mapInstance .getContainer ().removeEventListener (' focusin' , slidingWindowHandler );
494+ slidingWindowHandler = null ;
495+ }
445496 mapInstance ?.remove ();
446497});
447498 </script >
0 commit comments