Skip to content

feat(a11y): add descriptive alt text and ARIA labels to map markers#130

Merged
vgpastor merged 5 commits intoGlobalEmergency:mainfrom
rdcrampton:feat/accessible-map-markers
Mar 25, 2026
Merged

feat(a11y): add descriptive alt text and ARIA labels to map markers#130
vgpastor merged 5 commits intoGlobalEmergency:mainfrom
rdcrampton:feat/accessible-map-markers

Conversation

@rdcrampton
Copy link
Copy Markdown
Contributor

Summary

Adds accessibility attributes to the map markers, popups, and status indicators so that screen reader users can identify and interact with AED locations on the map.

Changes across two files:

MapView.tsx

  • AED markers now have aria-label, title, and alt attributes including the defibrillator name (e.g. "DEA: Hospital La Paz")
  • Search location marker labelled with drag instructions for screen readers
  • Map container wrapped with role="region" and a descriptive aria-label
  • "Ver detalles" button includes the AED name in its aria-label
  • Loading indicator uses role="status" with aria-live="polite"
  • Error indicator uses role="alert"
  • Decorative SVGs and icons marked aria-hidden="true"
  • AED icons are now created per-name and cached to maintain performance

ClusterMarker.tsx

  • Cluster icons have role="button", aria-label, and title describing the count (e.g. "Grupo de 42 desfibriladores. Haz clic para ampliar.")
  • Visual-only elements marked aria-hidden="true"

Test plan

  • Verify markers show descriptive tooltips on hover (native title attribute)
  • Test with VoiceOver or NVDA to confirm AED markers are announced by name
  • Test cluster markers are announced with count
  • Confirm loading and error states are announced by screen readers
  • Check that no visual rendering has changed
  • Run npm run lint:fix and npm run build with no errors

Closes #116

- Add aria-label and title to AED markers with the defibrillator name
- Add aria-label to search location marker with drag instructions
- Add aria-label to cluster markers describing the count
- Add role="region" and aria-label to the map container
- Add aria-label to "Ver detalles" popup button with AED name
- Add role="status" and aria-live="polite" to loading indicator
- Add role="alert" to error indicator
- Mark decorative SVGs and visual elements with aria-hidden="true"
- Add alt prop to react-leaflet Marker components
- Cache AED icons per name to maintain performance

Closes GlobalEmergency#116
@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Mar 15, 2026

@rdcrampton is attempting to deploy a commit to the vgpastor's projects Team on Vercel.

A member of the Team first needs to authorize it.

- Add role="button", aria-label, and title to spiderfy cluster icons
- Hide visual count from screen readers with aria-hidden
- Label describes overlapping markers and the click action
@rdcrampton
Copy link
Copy Markdown
Contributor Author

VoiceOver testing completed (macOS, Safari)

Tested locally with 500 seeded AEDs across Madrid. All ARIA attributes are working as expected:

  • AED markers: Announced as "DEA: [name]" — correctly identified as buttons
  • Cluster markers: Announced with count, e.g. "Grupo de 42 desfibriladores. Haz clic para ampliar."
  • Spiderfy clusters: Announced as "Grupo de [N] desfibriladores superpuestos. Haz clic para separar."
  • Map container: Identified as region "Mapa interactivo de desfibriladores (DEA)"
  • Loading state: Announced via aria-live="polite"
  • Decorative elements: SVG icons and visual cluster backgrounds correctly hidden from screen reader with aria-hidden="true"

No visual rendering changes observed. Markers display and interact identically to before.

@vgpastor
Copy link
Copy Markdown
Contributor

Hi @rdcrampton thanks for your efforts

I will review this weekend in details yours commits.

Many thanks!!!!

…ility

- Revert per-name aedIconCache (memory leak: 35k+ DivIcon objects) to
  single shared aedIcon — use Leaflet-native `alt` and `title` props
  on <Marker> for per-marker accessibility (zero perf cost)
- Remove role="button"/tabindex="0" from divIcon HTML (Leaflet handles
  marker interactivity — inner elements caused duplicate tab stops)
- Fix ClusterMarker: move aria-label/title from cached divIcon HTML to
  <Marker> props (bucket caching made labels show wrong counts)
- Add prefers-reduced-motion media query to disable animations for
  users with vestibular/motion sensitivity
- Add sr-only CSS class + aria-live region for AED count announcements
- Mark decorative MapPin icon in popup as aria-hidden

Co-Authored-By: rdcrampton <17489362+rdcrampton@users.noreply.github.com>
@vgpastor
Copy link
Copy Markdown
Contributor

vgpastor commented Mar 25, 2026

Hey @rdcrampton! 👋

First of all, thank you so much for this contribution — accessibility is something we really care about and it's great to see someone tackling it. Your VoiceOver testing was really thorough and we appreciate the effort!

I've pushed a commit on top of yours with a few adjustments. Let me explain the reasoning so it makes sense:

🔧 Reverted the per-name aedIconCache → back to single shared aedIcon

This was the main thing. Our map is optimized for 3M+ points (see PR #122), and the aedIconCache created a new L.DivIcon for every unique AED name. With 35,000+ DEAs, that's 35k+ objects that never get garbage collected — a significant memory leak.

The fix: We kept your alt prop on <Marker> (which Leaflet handles natively at zero perf cost) and added title for browser tooltips. Same screen reader experience, zero memory overhead. Win-win! 🎯

🔧 Removed role="button" / tabindex="0" from divIcon HTML

Leaflet already makes markers interactive and keyboard-navigable through its own container element. Adding these inside the divIcon HTML was creating duplicate tab stops — users would tab to the same marker twice. We kept your aria-hidden="true" on decorative SVGs though, that was spot on.

🔧 Moved cluster aria-label/title from HTML to <Marker> props

This one was subtle — ClusterMarker uses a bucketing/caching system (counts 20-24 all share the same cached icon). Your aria-label with the exact count was baked into the cached HTML, so a cluster of 23 DEAs could end up with a label saying "22 desfibriladores" if 22 was cached first. Moving it to the <Marker> component's title and alt props fixes this since those are set per-instance, not per-icon.

✨ Added extras

While we were at it, I added a few more a11y improvements:

  • prefers-reduced-motion media query in globals.css — respects users with vestibular/motion sensitivity by disabling animations (the pulse on the search marker, spinners, etc.)
  • sr-only live region — announces the AED count to screen readers when markers load ("42 desfibriladores encontrados en esta zona")
  • aria-hidden on the MapPin icon in popups (decorative)
  • title on the search location marker for tooltip on hover

✅ What we kept from your PR (great stuff!)

  • role="region" + aria-label on the map container
  • role="status" + aria-live="polite" on loading indicator
  • role="alert" on error indicator
  • aria-hidden="true" on all decorative SVGs and icons
  • aria-label on the "Ver detalles" button
  • alt on search/AED markers
  • Spiderfy cluster aria-label and aria-hidden on count span

Build, type-check, and lint all pass ✅

I'm also going to create a few issues for accessibility improvements that are outside the scope of this PR (modal focus trap, search combobox ARIA pattern, etc.) — in case you or anyone else wants to pick them up!

Thanks again for the contribution, really appreciate it 🙏

@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Mar 25, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
dea-map Ready Ready Preview Mar 25, 2026 7:02pm

Request Review

@vgpastor vgpastor merged commit a1352d7 into GlobalEmergency:main Mar 25, 2026
6 checks passed
@rdcrampton
Copy link
Copy Markdown
Contributor Author

Thanks so much for the detailed review and the care you put into optimizing the changes, @vgpastor. The performance feedback is really valuable. I hadn't considered the memory impact of per-name icon caching at the scale of 35k+ markers, and using Leaflet's native alt/title props is a much cleaner approach. The duplicate tab stop issue with role="button" + tabindex="0" on divIcon HTML is a good catch too. Good to know Leaflet handles marker interactivity natively.

The additions you made (prefers-reduced-motion, the live region for AED counts, aria-hidden on decorative icons) round things out nicely. Really glad to see the four new accessibility issues as well. I'd be happy to pick up one of those if you'd like a hand.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add descriptive alt text to map markers for screen readers

2 participants