浮层
Dialog - 对话框
一个模态表面:portal、遮罩、居中面板。
用法
import { Dialog } from '@misoto22/folio'说明
Radix Dialog 的根节点、触发器和关闭按钮,以带类型的原样透传再导出。
结构
| 部件 | 说明 |
|---|---|
| 遮罩必填 | 铺满视口的那层 --scrim,停在 --z-overlay(200)。点击外部落在的就是它,而页面钉在这一档以下的所有东西都会被它盖住——包括停在 100 的 FloatingIconButton。锚定面板不在此列:那些停在 220,正是为了让在这个对话框里打开的 Select 依然够得到。 |
| 面板必填 | 居中的那个盒子,停在 --z-modal(210),宽度上限 min(92vw, 32rem),高度上限 85vh,超过就自己滚正文。它靠一次 translate 让自己居中,而这件事对里面任何 fixed 定位的东西都有后果。 |
| 标题必填 | title,渲染成 Radix 的 Title。它一直在:不传 title 时会渲染一个视觉上隐藏的标题,念的就是「Dialog」这个词本身,同时开发环境会警告 DIALOG_TITLE_MISSING——这个兜底存在的意义,是让没名字的模态发不出去,不是让它发得出去。 |
| 描述 | description,标题底下轻声的一行。它和标题共用一个外层容器,所以 hideTitle 是把两个一起藏起来。 |
| 关闭 | 顶部末端角上那个 36px 的 X,在 showClose 为 true(默认值)时渲染,并且自带一个 aria-label「Close」。 |
实践建议
推荐
- 设了 hideTitle 也照样要传 title:完全不传时,兜底的无障碍名称就是「Dialog」这个字面字符串,于是应用里每一个没名字的模态,播报出来都是同一句话——而且它一路还能通过自动化可访问性检查,所以这件事由开发环境发警告,而不是留给评审。
- 取消的那个控件要用 DialogClose 包住,别自己翻一个 state——这样关闭走的是 Radix,焦点会被送回触发器,而不是被丢在文档最开头。
- 装得下多少就放多少。面板到 32rem × 85vh 就停住,再多就自己滚正文——所以一张长到要滚动的表单,其实已经是一个 Sheet(它拿的是整个视口的高度),或者干脆是一个页面。
- 除非面板自己给了出口,否则别关掉 showClose:另外两条出路只有 Escape 和遮罩,两个都看不见——所以在一个装满内容的对话框上写 showClose={false},是一间门上没有标记的房间。
避免
- 元素本身没有定位的 OverlayContainer,会把错的那个盒子交给对话框:一旦指定 container,面板就从 fixed 切成 absolute,而一个没有定位的 container 把它送到的是最近那个有定位的祖先——通常就是页面,看起来就像 container 被无视了。
- 同时开着的两个对话框,先后由 DOM 决定,不是由层级决定:两个都停在 --z-modal,所以后挂载的那个画在先来的那个上面——在对话框之上召出来的命令面板压在上面,只是因为它是第二个打开的;把这个顺序倒过来,画面也跟着倒过来。
- 面板靠一次 transform 让自己居中,于是它成了每一个 position: fixed 后代的包含块——扔进对话框里的 FloatingIconButton 会钉在面板的角上,而不是屏幕的角上。
示例
a destructive confirm
一个问题、两个答案,面板里再没有别的。两个控件都包在 DialogClose 里,而不是自己手动翻状态:这样关闭走的是 Radix,它会把焦点交还给触发器,而不是把焦点丢在文档顶上。title 是给这个模态命名的东西——不写它,兜底的无障碍名称就是字面的「Dialog」,于是一个应用里每一个没命名的模态,播报出来都是同一个东西。答案是「是」或「否」时拿这个形状;任何更长的东西,都已经变成一个 Sheet 了。
a hidden title
一块自己画页头的面板——标题上方一个引题,旁边一枚徽标——这是那行朴素的标题表达不了的。hideTitle 阻止那一行渲染两遍,而 title 仍然为读屏软件命名这个对话框,所以即便在这里也要传它:完全不给 title,无障碍名称就退回字面的「Dialog」。hideTitle 会把 description 一并带走,因为两者共用一层容器,所以任何读者必须读到的东西都放进 children。showClose 保持开着,就像它的默认值一样:除此之外只有 Escape 和遮罩两条出路,而它们都是看不见的。
or a sheet
同一个模态的两种长度。Dialog 到 32rem × 85vh 就打住,超出的部分在自己的正文里滚动,这对一个问题是对的,对一份表单是错的:读者失去了页面边缘这个锚,还得在一个盒子里滚另一个盒子。Sheet 是同一个组件停靠到边上——同样的遮罩、同样的焦点陷阱、同样的滚动锁定——但有整个视口的高度可花。所以这个选择只关乎内容需要多大地方,别的什么都不关乎。两者都通过 DialogClose 或 SheetClose 关闭,好让焦点回到触发器上。
组成部分
在调用处组合,而不是通过 props 配置——所以这个组件没有预料到的布局,依然表达得出来。
DialogContent
一块模态表面:portal → 遮罩 → 居中面板。
焦点陷阱、Escape 键、滚动锁定和 aria-modal 的接线都归 Radix 管——这些每一项手搓的对话框都会微妙地做错,通常是把焦点留在了底下的页面里。
不管显不显示,Radix 都要求有一个 Dialog.Title,所以一个没有可见标题的对话框仍然会渲染一个隐藏的,而不是交付一个没有名字的模态——同时它会在开发环境发出警告,因为它渲染出来的那个兜底就是“Dialog”这个词,而一个能通过无障碍检查的占位,正是这个问题得以熬过代码评审的方式。
它会 portal 到外层 OverlayContainer 指名的那个元素里,并且在有这么一个容器时,从按视口定位切换成按容器定位。一个 fixed 的面板不管被 portal 到哪里都会盖满整页,所以只认容器、不做这次切换,等于只挪了标记而画面纹丝不动。
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| className | string | — | |
| description | ReactNode | 标题下面的副标题。 | |
| hideTitle | boolean | false | 保留给辅助技术的标题,但在视觉上隐藏它。 用于那些看得见的人一眼就明白用途的表面——命令面板、图片灯箱——在那里印一个标题只是多余的家具。标题本身从来不是可选的:Radix 要求有一个,而一个没有可访问名称的模态,会把读屏用户丢进一个无名的区域里。 |
| showClose | boolean | true | 显示右上角的关闭控件(默认 true)。 |
| title | ReactNode | 标题文字。即使设了 `hideTitle` 也要传。 Radix 要求有标题,所以不传就会渲染一个隐藏的兜底,内容是“Dialog”这个词——它能满足自动化的无障碍检查,同时把应用里每一个没名字的模态都播报成同一样东西。开发环境会把这件事说出来。 |
同时接受 Omit<ComponentProps<typeof DialogPrimitive.Content>, 'title' | 'className'> 里的全部属性,它们会直接透传给底层元素,不再逐条列出。
再导出
Dialog = DialogPrimitive.RootRadix Dialog 的根节点、触发器和关闭按钮,以带类型的原样透传再导出。
DialogTrigger = DialogPrimitive.TriggerDialogClose = DialogPrimitive.Close键盘操作
| 按键 | 作用 |
|---|---|
| Escape | 关闭对话框,焦点回到它来的那个触发器。 |
| Tab | 在对话框内部循环;打开期间焦点出不去。 |
无障碍
- 焦点陷阱、Escape、滚动锁定和 aria-modal 都由 Radix 负责。
- 没有可见标题的对话框仍然会渲染一个隐藏标题,而不是交付一个没有名字的模态。