The brief says of the no-selection chooser: "Keep the dropdowns as the
keyboard/screen-reader path." Pin clicking is mouse-only by
construction, so that path is load-bearing — and nothing had ever
pressed Tab. There was zero keyboard coverage across 42 probes.
The path itself is sound, and is now asserted: 30 distinct focus stops;
the chooser's region keys are real focusable buttons and Enter on one
selects it ("All Regions" to "Africa") with no mouse involved; the
SELECT VIEW strip carries role=tab on both buttons with exactly one
aria-selected.
The defect that fell out: of those 30 stops, 29 showed a 2px ring and
one showed outline:none — button.mcb, the shared MapControlButton. Its
line 38 sets outline:none deliberately, to kill the webkit focus box on
click/tap alongside -webkit-tap-highlight-color, but it had no
:focus-visible replacement. So a sighted keyboard user tabbing onto ANY
map control in ANY bundle saw nothing at all. The accessible name was
already correct (aria-label mirrors title, fixed 2026-09-21), so screen
readers were never affected — only the people who navigate by eye and
keyboard, which is the very group outline:none was written for.
The fix is a :focus-visible rule plus a .dark variant. :focus-visible
is the correct half of the pair: it never fires for a mouse click, so
the original reason for outline:none still holds. Consumers re-checked
per RULE 2 — ShareButton, ZoomIn, ZoomOut, Fullscreen, Help,
ThemeToggle, Location, map-controls/index.js, my-upg-100-list and
research-dashboard's DashboardDataTable — all render the button
icon-only and none styled focus themselves, so the rule only adds a
ring where there was none. All five affected bundles rebuilt.
Proven both ways: Tab gives solid 2px rgb(26,115,232) with
:focus-visible true and the "Share" label intact; a real mouse click on
a fresh page gives outline:none with :focus-visible false.
Three of my own errors on the way, each caught before it shipped. I
first read a region key's name as "Africa315" and nearly fixed a
non-bug — the button already carries aria-label="Open the Africa
dashboard", which wins over textContent, and the label and count have a
real 7.5px flex gap. I put backticks in a CSS comment inside a template
literal, which ended the string and broke four of five builds;
check:sfc-parse caught it at once. And my mouse assertion failed twice
on correct behaviour: synthetic PointerEvents with a manual focus() do
not update the browser's pointer-modality heuristic, and clicking the
button that had just held keyboard focus leaves the page in keyboard
modality. Only a fresh page with no key ever pressed is a clean test.
Regression: 22 suites green at the time of commit, including new
keyboard and focus-ring suites.
|
||
|---|---|---|
| .. | ||
| FullscreenButton.vue | ||
| GeocoderComponent.vue | ||
| HamburgerButton.vue | ||
| HelpButton.vue | ||
| index.js | ||
| LocationButton.vue | ||
| MapControlButton.vue | ||
| MapControlMenu.vue | ||
| MapToolbar.vue | ||
| README.md | ||
| ShareButton.vue | ||
| ThemeToggleButton.vue | ||
| ZoomInButton.vue | ||
| ZoomOutButton.vue | ||
library/components/map-controls/ — toolbar buttons
Each file is a single, focused button or input that mounts inside the map's control area. Replaces
the older monolithic MapControlsComponent.vue.
MapToolbar.vue (container)
├─ ZoomInButton · ZoomOutButton · LocationButton
├─ FullscreenButton · HamburgerButton · ThemeToggleButton
├─ HelpButton · ShareButton
└─ GeocoderComponent (all built on MapControlButton.vue)
- Every button takes a
sizeprop (sm | md | lg) and follows theMapControlButtonshape. - Click handlers go through
inject('mapStore')orgetMap()— never a captured Mapbox instance. - To add one: copy
ZoomInButton.vueand register it inMapToolbar.vue.
Full conventions + catalog: ../../../documentation/architecture/components.md.