Fix pa11y issues with the map

This commit is contained in:
Stefan Bethke 2026-10-17 10:50:38 +02:00
commit f1b8c42ab6
6 changed files with 94 additions and 21 deletions

View file

@ -88,6 +88,33 @@
}
}
/* The credit line on an opaque background, so its contrast doesn't depend on the map below.
As specific as Leaflet's own semi-transparent rule plus one, which it would lose to otherwise. */
.tour-map .leaflet-container .leaflet-control-attribution {
background: #fff;
color: #333;
a {
color: #0b5394;
}
}
/* Leaflet blends tiles with plus-lighter to hide seams while they fade in; the map doesn't fade
tiles (tour-map.js), and axe, which pa11y uses, fails on that blend mode. */
.tour-map .leaflet-container img.leaflet-tile {
mix-blend-mode: normal;
}
.tour-map .leaflet-bar a {
color: #111;
}
/* The zoom icons (SVG, see tour-map.js) centred in their buttons. */
.tour-map .leaflet-control-zoom a {
display: grid;
place-items: center;
}
/* Dark map tiles to match the dark colour scheme. */
@media (prefers-color-scheme: dark) {
.tour-map .leaflet-tile-pane {

View file

@ -66,11 +66,29 @@ const popup = (tour) => {
return content;
};
for (const element of document.querySelectorAll('[data-tour-map]')) {
// Leaflet makes markers buttons, but only gives them a title, which isn't an accessible name.
const named = (marker, label) =>
marker.on('add', () => marker.getElement().setAttribute('aria-label', label));
const createMap = (element) => {
const { origin, tours } = JSON.parse(element.dataset.tourMap);
// Show the map container first: Leaflet needs its size.
element.classList.add('tour-map--ready');
const map = L.map(element.querySelector('.tour-map__canvas'), { scrollWheelZoom: false });
const map = L.map(element.querySelector('.tour-map__canvas'), {
scrollWheelZoom: false,
zoomControl: false,
// No fading in of tiles, see the tile blend mode in _tour-map.css.
fadeAnimation: false,
});
// Plus and minus as icons rather than characters, with names in the page's language.
L.control
.zoom({
zoomInText: '<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><path d="M7 2h2v5h5v2H9v5H7V9H2V7h5z" fill="currentColor"/></svg>',
zoomInTitle: element.dataset.labelZoomIn,
zoomOutText: '<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><path d="M2 7h12v2H2z" fill="currentColor"/></svg>',
zoomOutTitle: element.dataset.labelZoomOut,
})
.addTo(map);
// Without the flag icon, so the credit line is plain text.
map.attributionControl.setPrefix('<a href="https://leafletjs.com">Leaflet</a>');
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
@ -95,22 +113,45 @@ for (const element of document.querySelectorAll('[data-tour-map]')) {
.addTo(map);
points.forEach((point) => bounds.extend(point));
}
L.marker([tour.lat, tour.lon], { icon: markerIcon(tour.color, tour.icon), title: tour.title })
named(L.marker([tour.lat, tour.lon], { icon: markerIcon(tour.color, tour.icon), title: tour.title }), tour.title)
.bindPopup(popup(tour), POPUP)
.addTo(map);
bounds.extend([tour.lat, tour.lon]);
}
if (origin.lat !== undefined) {
L.marker([origin.lat, origin.lon], {
icon: markerIcon('', origin.icon, 'origin'),
title: origin.name,
zIndexOffset: 1000,
})
named(
L.marker([origin.lat, origin.lon], {
icon: markerIcon('', origin.icon, 'origin'),
title: origin.name,
zIndexOffset: 1000,
}),
origin.name,
)
.bindPopup(popup({ title: origin.name }), POPUP)
.addTo(map);
bounds.extend([origin.lat, origin.lon]);
}
map.fitBounds(bounds, { padding: [24, 24], maxZoom: 16 });
};
// The map is only created when it comes near the visible part of the page: visitors who don't
// scroll down load no map tiles, and accessibility checkers that look at the page as loaded
// don't see the tiles, which axe wrongly takes to overlap the text above the map.
const nearView = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (!entry.isIntersecting) continue;
nearView.unobserve(entry.target);
createMap(entry.target);
}
},
{ rootMargin: '200px' },
);
for (const element of document.querySelectorAll('[data-tour-map]')) {
// Shows the (still empty) map area at its final size, so the page doesn't jump later on.
element.classList.add('tour-map--ready');
nearView.observe(element);
}