Skip to content

feat: let a Tooltip trigger be a control - #29

Merged
inureyes merged 1 commit into
mainfrom
feat/tooltip-toggleable-trigger
Sep 8, 2026
Merged

inureyes merged 1 commit into
mainfrom
feat/tooltip-toggleable-trigger

Conversation

@inureyes

@inureyes inureyes commented Sep 8, 2026

Copy link
Copy Markdown
Member

Tooltip takes toggleable. With it the trigger carries role="button" and an aria-expanded that tracks the content, and Enter and Space toggle it. Off by default, so nothing that exists today changes.

Why

A trigger whose content only supplements what is already on screen is not a control and should not announce itself as one, which is why this is opt-in and not the reverse. A glossary term is the opposite case: the definition is something a reader summons deliberately, and where there is no hover at all, activation is the only route to it.

This is not hypothetical. Both continuum-hub and backend.ai-go have a Term component for glossary phrases, both written before this package existed, and both built the same trigger contract around it: tabIndex={0}, role="button", aria-describedby, aria-expanded bound to the open state, Enter and Space toggling, Escape closing. Two products, the same shape, arrived at independently.

continuum-hub adopted this component in lablup/continuum-hub#1057 and lost that affordance, because the wrapper has no seam for it. I recorded the loss as the price of composing rather than reimplementing. That was the wrong call: the package is ours to change, and a shared component that costs accessibility to adopt will be worked around rather than adopted.

What it does

role="button" and aria-expanded on the wrapper, and a keydown handler for Enter and Space that flips visibility. Both keys are prevented from their defaults, since Space scrolls the page and Enter submits an enclosing form. Everything else is untouched: hover, focus, Escape and the placement all behave as they did, and aria-expanded reports the state however it was reached, including a plain hover.

No pinning. Toggling open then moving the pointer away closes it, which is what both existing implementations do.

The prop docs say not to combine it with tabIndex={-1}. That is for wrapping an element that is already focusable, which then owns its own role.

Tests

Six, each verified against the perturbation that should break it: dropping the role, dropping aria-expanded, dropping the preventDefault, wiring the handler unconditionally rather than behind the flag, and handling Enter without Space. One of them asserts the default case claims nothing: no role, no aria-expanded, and Space left to the page.

pnpm run verify passes.

Refs lablup/continuum-hub#996

`toggleable` gives the trigger role="button" with an aria-expanded that tracks the content, and makes Enter and Space toggle it. Both keys are prevented from their defaults, since Space scrolls the page and Enter submits an enclosing form. Off by default, so no existing trigger changes.

A trigger whose content only supplements what is already on screen is not a control and should not say it is, which is why this is opt-in rather than the other way round. A glossary term is the opposite case: the definition is summoned deliberately, and without hover, activation is the only way to it.

Both continuum-hub and backend.ai-go had built this same contract around a glossary term (tabIndex, role="button", aria-describedby, aria-expanded, Enter and Space) before this component existed. Adopting it cost the first of them that affordance, which is not a reasonable price for sharing code, and would have cost the second the same.

Six tests, each checked against the perturbation that should break it: dropping the role, dropping aria-expanded, dropping the preventDefault, wiring the handler unconditionally, and handling Enter without Space.
@inureyes
inureyes merged commit aea2d97 into main Sep 8, 2026
3 checks passed
@inureyes
inureyes deleted the feat/tooltip-toggleable-trigger branch September 8, 2026 06:45
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.

1 participant