closed
- MOUSE_ENTER
- → opening · start_open_timer(openDelay)
- FOCUS_IN
- → open · show_immediately
Contrato del componente
Patrón WAI-ARIA Tooltip (scratch build) — envuelve el primer Element child del consumidor como trigger, abre en hover (con openDelay) y en focus (inmediato), se cierra con Escape sin sacar el foco del trigger, y también en blur o mouseleave real. shadow:false para que aria-describedby del trigger pueda apuntar al id del tooltip.
| Nombre | Tipo | Valores | Predeterminado |
|---|---|---|---|
| label | string | — | — |
| openDelay | number | — | 250 |
| closeDelay | number | — | 100 |
| placement | enum | top, bottom | bottom |
Estado inicial: closed
{
"name": "tooltip",
"version": "1.0.0",
"props": {
"label": {
"type": "string"
},
"openDelay": {
"type": "number",
"default": 250
},
"closeDelay": {
"type": "number",
"default": 100
},
"placement": {
"type": "enum",
"values": [
"top",
"bottom"
],
"default": "bottom"
}
},
"slot": {
"name": "default",
"purpose": "the trigger — the first Element child inside ds-tooltip is treated as the trigger and MUST be focusable (native button/link/form control, or an element with tabindex=0). The tooltip is rendered as a sibling <span role=\"tooltip\"> at the end of the host's children."
},
"state_machine": {
"initial": "closed",
"states": {
"closed": {
"on": {
"MOUSE_ENTER": {
"target": "opening",
"actions": [
"start_open_timer(openDelay)"
]
},
"FOCUS_IN": {
"target": "open",
"actions": [
"show_immediately"
]
}
}
},
"opening": {
"on": {
"TIMER_ELAPSED": {
"target": "open",
"actions": [
"show"
]
},
"MOUSE_LEAVE": {
"target": "closed",
"actions": [
"cancel_open_timer"
]
}
}
},
"open": {
"on": {
"MOUSE_LEAVE": {
"target": "closing",
"actions": [
"start_close_timer(closeDelay)"
]
},
"FOCUS_OUT": {
"target": "closed",
"actions": [
"hide_immediately"
]
},
"ESCAPE": {
"target": "closed",
"actions": [
"hide_immediately",
"keep_focus_on_trigger"
]
}
}
},
"closing": {
"on": {
"TIMER_ELAPSED": {
"target": "closed",
"actions": [
"hide"
]
},
"MOUSE_ENTER": {
"target": "open",
"actions": [
"cancel_close_timer"
]
}
}
}
}
},
"tokens_used": {
"spacing": [
"xs",
"sm"
],
"radius": [
"sm"
],
"font": [
"family",
"size-md"
]
},
"a11y": {
"native_element": "span[role=tooltip] alongside the consumer trigger (scratch-built)",
"apg_pattern": "https://www.w3.org/WAI/ARIA/apg/patterns/tooltip/",
"keyboard": [
"Focus (Tab into trigger) → tooltip opens immediately (no openDelay for keyboard)",
"Escape (while open) → dismisses without moving focus; trigger remains focused (WCAG 1.4.13 dismissable)",
"Blur (Tab out of trigger) → tooltip closes immediately",
"Enter / Space / arrow / printable → forwarded to trigger untouched"
],
"pointer": [
"mouseenter host → open after openDelay (ergonomic against twitchy tooltips)",
"mouseleave host → close after closeDelay",
"re-entering during closeDelay cancels the hide (hoverable — WCAG 1.4.13)"
],
"aria": {
"tooltip_element": "role=tooltip, unique id per instance (auto-generated to avoid collisions on pages with multiple tooltips)",
"trigger": "aria-describedby=<tooltipId> set imperatively on the first Element child in componentDidLoad"
},
"wcag_1_4_13_content_on_hover_or_focus": {
"dismissable": "Escape hides the tooltip without moving focus",
"hoverable": "closeDelay + cancel-on-re-enter allow the pointer to move onto the tooltip surface without dismissing it",
"persistent": "tooltip stays visible while trigger is focused or hovered and while the pointer is over the tooltip surface"
},
"consumer_contract": [
"The first Element child inside <ds-tooltip> MUST be focusable — otherwise keyboard users cannot summon the tooltip and the pattern fails WCAG 1.4.13. The component does not force focusability onto arbitrary children.",
"`label` is REQUIRED — a tooltip with no accessible text is inert.",
"The tooltip is describedby, NOT labelledby — it supplements the trigger's own accessible name (which comes from the trigger's text or aria-label)."
],
"notes": "Scratch-built per the a11y spec §D ceremony because there is no native tooltip element. shadow:false is intentional: aria-describedby needs to reference the tooltip's id from the light-DOM trigger, and shadow-scoped ids are unreachable.",
"pending_manual_verification": [
"VoiceOver on macOS: confirm the tooltip text is announced as a description of the trigger when the trigger gains focus",
"VoiceOver: confirm Escape dismisses the tooltip and leaves the trigger focused; re-focusing brings it back",
"Chromium reduced-motion: confirm no residual animation is added by the component"
]
}
}