Aller au contenu
Sylvain Joffraud l4w
Retour

Edge marker Leaflet : caché sous les contrôles, intappable

Pattern UX classique dans une app cartographique avec une cible mobile (objet à retrouver, contact à rejoindre, point d’intérêt à proximité) : quand la cible sort du viewport visible, afficher un petit edge marker sur le bord de la carte, dans la direction de la cible, avec sa distance. Tap → recentre.

Math : intersection du segment [user, cible] avec la bbox du viewport, clamp à l’intérieur avec un padding pour ne pas déborder.

function segmentBoundsIntersection(map, userLatLng, targetLatLng) {
  const u = map.latLngToContainerPoint(userLatLng);
  const t = map.latLngToContainerPoint(targetLatLng);
  const size = map.getSize();
  // ... calcul du t∈[0,1] de l'intersection segment×rectangle
  const PAD = 24;
  const cx = Math.max(PAD, Math.min(size.x - PAD, intersectX));
  const cy = Math.max(PAD, Math.min(size.y - PAD, intersectY));
  return map.containerPointToLatLng([cx, cy]);
}

Test fonctionnel : on zoome, la cible sort du viewport, l’edge marker apparaît. On clique pour recentrer.

Symptôme

Test E2E qui devrait passer, mais le click sur .edge-marker timeout :

locator resolved to <div class="edge-marker">241m</div>
attempting click action
element is visible, enabled and stable
<a class="" title="Recentrer..." aria-label="Recentrer...">📍</a> from
  <div class="leaflet-control-container">…</div> subtree intercepts pointer events
retrying click action ...

Le browser confirme : un autre élément (un control Leaflet) intercepte les pointer events au-dessus du marker. En manuel sur le device, on ne voit même pas l’edge marker à cet endroit — il est caché derrière le bouton de zoom.

Cause

Hiérarchie des z-index Leaflet par défaut :

Élémentz-index
.leaflet-pane (markers, polylines)200-600
.leaflet-marker-pane600
.leaflet-control-container1000

Les contrôles Leaflet (zoom, attribution, custom buttons) sont systématiquement au-dessus des markers. Et ils capturent les pointer events pour empêcher la map de drag dessus.

Quand la cible est en direction nord-ouest, le segment user→cible traverse le coin top-left du viewport. L’edge marker s’y place. C’est exactement la zone où vivent les controls Leaflet (zoom + 3-4 boutons custom = ~60px × 200px).

Résultat : edge marker placé sous les contrôles → invisible (caché) et intappable (events interceptés).

Le clamp PAD = 24 ne suffit pas : 24px de padding ne sort pas de la zone des controls qui font 60px de large.

Solution : padding asymétrique + zones d’évitement

Au lieu d’un PAD uniforme, identifier les zones où vivent les UI elements et y interdire le placement du marker. Quand le marker y atterrit, on shift sur l’axe approprié pour préserver la direction visuelle (l’utilisateur doit toujours sentir « la cible est par là-bas »).

const PAD = 24;
let cx = Math.max(PAD, Math.min(size.x - PAD, px));
let cy = Math.max(PAD, Math.min(size.y - PAD, py));

// Zones d'évitement (en pixels) :
const CTRL_W = 60, CTRL_H = 215;    // controls Leaflet top-left
const OVERLAY_H = 60;                // notre overlay distance top-centre

if (cx < CTRL_W && cy < CTRL_H) cx = CTRL_W + 6;    // shift à droite des contrôles
if (cy < OVERLAY_H)             cy = OVERLAY_H + 6; // shift sous l'overlay distance

// re-clamp dans la viewport au cas où on a shifté hors bounds
cx = Math.min(size.x - PAD, cx);
cy = Math.min(size.y - PAD, cy);

Le shift cx = CTRL_W + 6 est volontairement directionnel : on push horizontalement pour libérer X tout en gardant Y le plus possible (la direction « haut » reste perceptible). Si on shiftait diagonalement, on perdrait la sémantique « la cible est au nord-ouest ».

Lessons learned

#leaflet #frontend #ux #mapping #z-index #playwright


Partager cet article :

Article précédent
Un POC sans tests n’est pas un départ, c’est une direction
Article suivant
Leaflet : `marker.remove()` ne nettoie pas son `LayerGroup`