组件契约
dialog
原生 <dialog> 元素,封装为设计系统的自定义元素——showModal() 借助浏览器平台提供模态状态机(焦点陷阱、按 Escape 关闭、背景 inert、焦点归还),而非从零构建的 ARIA。Props:open、label|labelledBy、describedBy。事件:dsOpen、dsClose。
- 版本
- v1.0.0
Props
| 名称 | 类型 | 取值 | 默认值 |
|---|---|---|---|
| open | boolean | — | false |
| label | string | — | — |
| labelledBy | string | — | — |
| describedBy | string | — | — |
事件
| 名称 | 载荷 | 条件 |
|---|---|---|
| dsOpen | void | — |
| dsClose | void | fires on every native close (Escape, form method=dialog, programmatic close(), or open prop transition to false) |
插槽
- 名称
- default
- 用途
- dialog content — headings, body copy, form fields, and action buttons. Focus lands on the first `autofocus` element if present, or the first focusable descendant otherwise.
Token
- spacing
- primitive.spacing.xl
- radius
- primitive.radius.md
无障碍
- native_element
- dialog (opened via showModal())
- apg_pattern
- https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/
- role
- implicit `dialog` (native); `aria-modal="true"` implicit when opened via showModal()
- labeling
- Choose ONE: `label` (string → aria-label) for short titles, OR `labelledBy` (id of a light-DOM heading inside the slot → aria-labelledby via flattened-tree resolution) for dialogs with a visible title. Omitting both fails axe `dialog-name`.
- keyboard
- ["Tab / Shift+Tab: focus trap (native, cycles within slotted descendants)","Escape: closes and returns focus (native, fires `close` event which we re-emit as dsClose)","Enter / Space / arrow keys / printable chars: forwarded to focused descendant","Home / End: no dialog-level behavior; forwarded to descendants"]
- focus_lifecycle
- {"opening":"browser focuses the first `autofocus` element inside the slot, or the first focusable descendant if none","while_open":"Tab / Shift+Tab cycle within the flattened tree of the <dialog>; focus cannot escape into the inert background","closing":"focus returns to the element that had it immediately before showModal() was called; native, not implemented by us"}
- motion
- no component-owned animation; consumers can style `dialog[open]` and `::backdrop` with their own animation, gated on `@media (prefers-reduced-motion: no-preference)`
- notes
- This is the deliberate answer to the a11y spec §D 'do not scratch-build state-machine components' rule — wrapping the native <dialog> delegates the state machine to the browser platform, which is verified to trap focus and restore correctly. Escape is native and always closes; consumers who need to guard destructive flows must call `event.preventDefault()` on the dialog's keydown before it reaches the browser. `spec.state_machine` is intentionally omitted from this contract: the state machine is owned by the browser (the native <dialog> element via showModal()/close()), not synthesized by this component — so declaring it in the spec would misattribute ownership and trip the L1-with-state-machine doctor warning.
- pending_manual_verification
- ["VoiceOver on macOS: announces accessible name + 'dialog' on open, initial focus lands on autofocus target, Escape closes and focus returns to trigger","Chromium high-contrast / forced-colors: surface reads as bordered container, ::backdrop dim is visible"]
经过校验的原始 JSON
{
"name": "dialog",
"version": "1.0.0",
"props": {
"open": {
"type": "boolean",
"default": false
},
"label": {
"type": "string"
},
"labelledBy": {
"type": "string"
},
"describedBy": {
"type": "string"
}
},
"events": {
"dsOpen": {
"payload": "void"
},
"dsClose": {
"payload": "void",
"condition": "fires on every native close (Escape, form method=dialog, programmatic close(), or open prop transition to false)"
}
},
"slot": {
"name": "default",
"purpose": "dialog content — headings, body copy, form fields, and action buttons. Focus lands on the first `autofocus` element if present, or the first focusable descendant otherwise."
},
"tokens_used": {
"spacing": [
"primitive.spacing.xl"
],
"radius": [
"primitive.radius.md"
]
},
"a11y": {
"native_element": "dialog (opened via showModal())",
"apg_pattern": "https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/",
"role": "implicit `dialog` (native); `aria-modal=\"true\"` implicit when opened via showModal()",
"labeling": "Choose ONE: `label` (string → aria-label) for short titles, OR `labelledBy` (id of a light-DOM heading inside the slot → aria-labelledby via flattened-tree resolution) for dialogs with a visible title. Omitting both fails axe `dialog-name`.",
"keyboard": [
"Tab / Shift+Tab: focus trap (native, cycles within slotted descendants)",
"Escape: closes and returns focus (native, fires `close` event which we re-emit as dsClose)",
"Enter / Space / arrow keys / printable chars: forwarded to focused descendant",
"Home / End: no dialog-level behavior; forwarded to descendants"
],
"focus_lifecycle": {
"opening": "browser focuses the first `autofocus` element inside the slot, or the first focusable descendant if none",
"while_open": "Tab / Shift+Tab cycle within the flattened tree of the <dialog>; focus cannot escape into the inert background",
"closing": "focus returns to the element that had it immediately before showModal() was called; native, not implemented by us"
},
"motion": "no component-owned animation; consumers can style `dialog[open]` and `::backdrop` with their own animation, gated on `@media (prefers-reduced-motion: no-preference)`",
"notes": "This is the deliberate answer to the a11y spec §D 'do not scratch-build state-machine components' rule — wrapping the native <dialog> delegates the state machine to the browser platform, which is verified to trap focus and restore correctly. Escape is native and always closes; consumers who need to guard destructive flows must call `event.preventDefault()` on the dialog's keydown before it reaches the browser. `spec.state_machine` is intentionally omitted from this contract: the state machine is owned by the browser (the native <dialog> element via showModal()/close()), not synthesized by this component — so declaring it in the spec would misattribute ownership and trip the L1-with-state-machine doctor warning.",
"pending_manual_verification": [
"VoiceOver on macOS: announces accessible name + 'dialog' on open, initial focus lands on autofocus target, Escape closes and focus returns to trigger",
"Chromium high-contrast / forced-colors: surface reads as bordered container, ::backdrop dim is visible"
]
}
}