closed
- MOUSE_ENTER
- → opening · start_open_timer(openDelay)
- FOCUS_IN
- → open · show_immediately
组件契约
WAI-ARIA Tooltip 模式(从零构建)——将使用者的第一个 Element 子节点包装为触发器,hover 时打开(带 openDelay),focus 时立即打开;按 Escape 关闭,焦点仍留在触发器上;在 blur 或真正的 mouseleave 时关闭。设为 shadow:false,以便触发器上的 aria-describedby 能引用 tooltip 的 id。
| 名称 | 类型 | 取值 | 默认值 |
|---|---|---|---|
| label | string | — | — |
| openDelay | number | — | 250 |
| closeDelay | number | — | 100 |
| placement | enum | top, bottom | bottom |
初始状态: 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": [
"primitive.spacing.xs",
"primitive.spacing.sm"
],
"radius": [
"primitive.radius.sm"
],
"font": [
"primitive.font.family",
"primitive.font.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"
]
}
}