Tooltip puts aria-describedby on its own wrapper, so the description never reaches the control it describes.
0.2.396 fixed the hoverable, Escape and whitespace-nowrap parts of #2045, and the tooltip panel now has an id with aria-describedby pointing at it. Thank you for that. The reference is on the wrong element though:
<div class="relative inline-block">
<div
aria-describedby="" <!-- a plain, non-focusable div -->
@mouseenter="showTooltip()" …
>
<slot /> <!-- the control lives here -->
</div>
<div id="" role="tooltip" …>{{ content }}</div>
</div>aria-describedby is not inherited. The description is computed for the element that carries the attribute, and that element is a non-focusable <div> which assistive technology does not land on or announce. The button, link or input inside the slot gets nothing.
What this costs a caller replacing a native title
A title on a control is announced: it supplies the description, or the name when there is nothing else. So swapping title for <Tooltip> moves the text from something an AT reads to something it does not.
Measured in a real accessibility tree, with a symbol-only button, which is the common shape for an icon action:
| markup | accessible name |
|---|---|
<button title="Resolve, alerts again only if it comes back">✓</button> | "Resolve, alerts again only if it comes back" |
the component's shape, aria-describedby on the wrapper | "✓" |
So across 131 title attributes in six apps, the 12 on interactive elements cannot be converted: the control would lose its description, and the icon-only ones would lose their name outright.
Suggested fix
Put the reference on the slotted element rather than beside it. Either:
- Set
aria-describedbyon the slot's root during render. The component already rewrites slot content, so the id is available at the point the trigger is emitted. - Expose the id to the caller, e.g. a
describedByIdprop or a slot prop, so<Tooltip content="…" describedById="tip-1"><button aria-describedby="tip-1">✓</button></Tooltip>is expressible without guessing a generated id.
(1) keeps the call site clean and is what a caller would expect from a component that renders role="tooltip".
Worth noting the keyboard path has the same shape: @focusin/@focusout are on the wrapper, which works only because focus events bubble from the slotted control. The description is the half that cannot bubble.
Found on 0.2.396. Related: #2045.