The zoom buttons render an icon through a slot and carried only `title`, so their accessible name was empty: a screen reader announced "button" with no indication of what it does. title is a tooltip, not an accessible name — unreliably announced, and never surfaced to touch users. Found while proving the research-dashboard zoom controls actually work. They do: a click triggered six tile requests at zoom level 2, so the open-work item is function-verified, not just present. The probe that matched aria-label found nothing while the probe that fell back to title found both buttons, and that discrepancy was the defect. One binding in the shared MapControlButton reaches all eight control buttons — zoom in and out, fullscreen, location, help, share, hamburger, theme — across seven map bundles, because every one composes this base. No caller changes: the component does not set inheritAttrs:false, so a caller passing its own aria-label still wins through normal fallthrough, and undefined omits the attribute when there is no title to mirror. Verified across all seven bundles: 34 control buttons, 0 without an accessible name.
90 lines
3.7 KiB
Vue
90 lines
3.7 KiB
Vue
<!--
|
|
MapControlButton.vue — Shared base button for all map toolbar controls.
|
|
|
|
Renders a single round button with consistent light/dark mode styling.
|
|
Each specific button (ZoomIn, Location, etc.) wraps this component and
|
|
supplies its own icon slot + service logic.
|
|
|
|
Props:
|
|
isDark — mirrors the app theme; switches bg/border/shadow
|
|
title — tooltip text
|
|
active — optional pressed/active visual state (e.g. fullscreen on)
|
|
disabled — disables interaction
|
|
|
|
Emits:
|
|
click — forwarded from the native button element
|
|
-->
|
|
<script setup>
|
|
import { useShadowStyles } from '../../composables/useShadowStyles.js'
|
|
|
|
useShadowStyles(`
|
|
.mcb{
|
|
width:var(--map-btn-size,40px);height:var(--map-btn-size,40px);
|
|
background:#fff;
|
|
border:none;
|
|
border-radius:50%;
|
|
cursor:pointer;
|
|
/* Don't let a drag that starts near a button select its glyph (the old
|
|
"drag selects the emoji to copy" bug while grabbing the scroll edge). */
|
|
user-select:none;-webkit-user-select:none;
|
|
display:flex;align-items:center;justify-content:center;
|
|
/* Tight shadow only — the old 0 2px 8px spread bled to the map's clipped right
|
|
edge (overflow:hidden) and read as a "shadow wall" + made buttons look
|
|
over-padded. (coder 2026-06-24) */
|
|
box-shadow:0 1px 3px rgba(0,0,0,0.18);
|
|
transition:background-color 0.2s,box-shadow 0.2s,transform 0.1s;
|
|
color:#333;
|
|
flex-shrink:0;
|
|
outline:none;
|
|
/* No blue/grey selection box on click/tap (webkit tap highlight). */
|
|
-webkit-tap-highlight-color:transparent;
|
|
}
|
|
.mcb:hover:not(:disabled){background:#f0f0f0;box-shadow:0 2px 8px rgba(0,0,0,0.18),0 4px 16px rgba(0,0,0,0.12);}
|
|
.mcb:active:not(:disabled){transform:scale(0.93);}
|
|
.mcb:disabled{opacity:0.4;cursor:not-allowed;}
|
|
.mcb.active{background:#e8f0fe;color:#1a73e8;}
|
|
/* dark */
|
|
.mcb.dark{background:#3b463d;color:#F3F3F1;border:1px solid rgba(255,255,255,0.14);box-shadow:0 1px 4px rgba(0,0,0,0.4);}
|
|
.mcb.dark:hover:not(:disabled){background:#4e594f;box-shadow:0 2px 8px rgba(0,0,0,0.5);}
|
|
.mcb.dark.active{background:#4e594f;color:#92b195;}
|
|
@media(max-width:767px){.mcb{width:36px;height:36px;}}
|
|
`, 'map-control-button')
|
|
|
|
defineProps({
|
|
isDark: { type: Boolean, default: false },
|
|
title: { type: String, default: '' },
|
|
active: { type: Boolean, default: false },
|
|
disabled: { type: Boolean, default: false }
|
|
})
|
|
|
|
defineEmits(['click'])
|
|
</script>
|
|
|
|
<template>
|
|
<!--
|
|
★ aria-label MIRRORS title (2026-09-21). These buttons render an ICON via <slot/> and carried
|
|
ONLY `title`, so their accessible name was EMPTY: a screen reader announced "button" with no
|
|
indication of what it does. `title` is a tooltip, not an accessible name — it is unreliably
|
|
announced, and never surfaced to touch users at all.
|
|
|
|
Found while proving the research-dashboard zoom controls actually WORK (they do: a click
|
|
triggered 6 tile requests at zoom level 2). The probe that matched on aria-label found nothing
|
|
while the probe that fell back to title found both buttons — the discrepancy WAS the defect.
|
|
|
|
One binding here reaches all EIGHT control buttons (zoom in/out, fullscreen, location, help,
|
|
share, hamburger, theme) across SEVEN map bundles, because every one composes this base.
|
|
No caller has to change. This component does not set inheritAttrs:false, so a caller that
|
|
passes its own aria-label still wins via normal attribute fallthrough — no $attrs juggling
|
|
needed here, and `undefined` omits the attribute entirely when there is no title to mirror.
|
|
-->
|
|
<button
|
|
class="mcb"
|
|
:class="{ dark: isDark, active }"
|
|
:title="title"
|
|
:aria-label="title || undefined"
|
|
:disabled="disabled"
|
|
@click="$emit('click')"
|
|
>
|
|
<slot />
|
|
</button>
|
|
</template>
|