MapLayersMenu

←→

Bouton flottant « Fond » avec la vignette du fond de carte en cours, et son menu — choix du fond par de vraies tuiles de la zone, et exagération du relief 3D.

stableÀ valider
Capture : MapLayersMenu
Capture du 19 octobre 2026, pas un rendu en direct

Installation

Copie le fichier dans ton projet, ainsi que les tokens qu’il utilise.

src/components/MapLayersMenu.tsx
import { useRef, useState } from 'react';
import { useDismiss } from '../hooks/useDismiss';
import { BASEMAPS, tileAt, type Basemap } from '../lib/mapOptions';
import { usePresence } from '../hooks/usePresence';
import type { Region } from '../lib/sources/types';
import { fmt } from '../lib/format';

interface Props {
  basemap: Basemap;
  onBasemap: (b: Basemap) => void;
  exaggeration: number;
  onExaggeration: (x: number) => void;
  /** point de la trace utilisé pour les vignettes (vraies tuiles de chaque fond) */
  center: { lat: number; lon: number };
  /** pays majoritaire de la trace et fonds qui y sont accessibles (vignettes des fonds assemblés) */
  region: { region: Region | null; basemaps: Basemap[] } | undefined;
}

const THUMB_ZOOM = 13;

/** Bouton flottant « Fond » et son menu de fonds de carte, façon Komoot. */
export default function MapLayersMenu({ basemap, onBasemap, exaggeration, onExaggeration, center, region }: Props) {
  const [open, setOpen] = useState(false);
  const root = useRef<HTMLDivElement>(null);
  useDismiss(open, root, () => setOpen(false));
  // reste affiché le temps de l'animation de fermeture
  const menu = usePresence(open, 150);

  const thumb = (id: Basemap) => tileAt(BASEMAPS.find((b) => b.id === id)!, center.lat, center.lon, THUMB_ZOOM, region?.basemaps.includes(id) ? region.region : null);

  return (
    <div ref={root} className="map-layers">
      <button
        className={`map-layers-btn${open ? ' open' : ''}`}
        onClick={() => setOpen((o) => !o)}
        aria-expanded={open}
        aria-haspopup="dialog"
        aria-controls="map-layers-menu"
        aria-label="Fond de carte et relief"
        title="Fond de carte et relief"
        style={{ backgroundImage: `url("${thumb(basemap)}")` }}
      >
        <span aria-hidden="true">Fond</span>
      </button>

      {menu.mounted && (
        <div id="map-layers-menu" className={`map-menu map-layers-menu${menu.closing ? ' closing' : ''}`} inert={menu.closing} role="dialog" aria-label="Fond de carte et relief">
          <div className="map-menu-head">
            <h2>Fond de carte et relief</h2>
            <button className="map-menu-close" onClick={() => setOpen(false)} aria-label="Fermer">
              <svg viewBox="0 0 20 20" width="20" height="20" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" aria-hidden="true">
                <path d="M5 5l10 10M15 5L5 15" />
              </svg>
            </button>
          </div>

          <h3>Fond de carte</h3>
          <div className="basemap-grid" role="radiogroup" aria-label="Fond de carte">
            {BASEMAPS.map((b) => (
              <button key={b.id} role="radio" aria-checked={b.id === basemap} className="basemap-tile" onClick={() => onBasemap(b.id)}>
                <img src={thumb(b.id)} alt="" loading="lazy" draggable={false} />
                <span>{b.label}</span>
              </button>
            ))}
          </div>

          <h3>Relief 3D</h3>
          <label className="relief-row">
            <span className="map-menu-text">
              <b>Exagération ×{fmt(exaggeration, 1)}</b>
              <small>Accentue le relief pour mieux lire les pentes</small>
            </span>
            <input type="range" min={1} max={3} step={0.1} value={exaggeration} onChange={(e) => onExaggeration(parseFloat(e.target.value))} />
          </label>
        </div>
      )}
    </div>
  );
}

Tokens : --panel --text --muted --line --accent --chip --shadow --ease-out

Utilisation

import MapLayersMenu from './components/MapLayersMenu';

<MapLayersMenu basemap={basemap} onBasemap={setBasemap} exaggeration={exaggeration} onExaggeration={setExaggeration} center={center} region={region} />

Anatomie

#ÉlémentRôle
1BoutonVignette du fond en cours, à la position de la trace, et le mot « Fond ».
2Fonds de carteGrille de vignettes en radiogroup, une vraie tuile de chaque fond à l’endroit de la trace.
3Relief 3DCurseur d’exagération, de ×1 à ×3.

Guidelines

  • SHOULD
    Montrer chaque fond par une tuile de l’endroit de la trace plutôt que par une image générique.

Référence de l’API

PropTypeDéfaut
basemapBasemaprequis
onBasemap(b: Basemap) => voidrequis
exaggerationnumberrequis
onExaggeration(x: number) => voidrequis
center
point de la trace utilisé pour les vignettes (vraies tuiles de chaque fond)
{ lat: number; lon: number }requis
region
pays majoritaire de la trace et fonds qui y sont accessibles (vignettes des fonds assemblés)
{ region: Region | null; basemaps: Basemap[] } | undefinedrequis