浮层
Sheet - 抽屉
停靠在视口某一边的面板。
用法
什么时候用它
import { Sheet } from '@misoto22/folio'说明
Radix Dialog 的根节点、触发器和关闭按钮——sheet 就是一个靠边停放的 dialog。
结构
| 部件 | 说明 |
|---|---|
| 遮罩必填 | 和 Dialog 用的是同一层停在 --z-overlay 的 --scrim——事实上就是同一个组件,所以后面的页面是惰性的、被锁住滚动的,跟在对话框底下一模一样。 |
| 面板必填 | 靠边停住的那个盒子。start 和 end 是一列 min(24rem, 92vw) 宽的满高栏;top 和 bottom 是一条满宽、高度上限 85vh 的带。它是一个自己滚动的 flex 竖列。 |
| 标题必填 | title,类型上是必填而不是可选——这里没有一条通向「没名字的抽屉」的路,只有经 hideTitle 把标题藏起来那一条。 |
| 描述 | description,在标题下面,和它共用那个被 hideTitle 藏起来的外层容器。 |
| 关闭必填 | 顶部末端角上那个 X。和 Dialog 不同,这里没有 showClose 能把它关掉,所以无论停靠在哪一边,每个抽屉都有一个。 |
实践建议
推荐
- 内容是一份列表、或者一张长到要滚动的表单时,选 Sheet 而不是 Dialog:它拿的是整个视口的高度,而不是 Dialog 那个 32rem × 85vh 的盒子,读者也还留着页面边缘作参照。
- 内容又宽又矮时用 top 或 bottom——一条筛选栏、一个日期区间。start 和 end 是一列 24rem 的窄栏,硬塞一张表格进去,它会折成一条带子。
- 边名写 start 和 end,别去伸手拿 left 和 right:每条边都有自己那串字面类名,各自带着自己的 rtl: 变体,所以 end 在英文里从右边进来、在阿拉伯语里从左边进来,不需要第二条代码路径。
- 取消的那个控件要用 SheetClose 包住,好让关闭走 Radix——用自己那个 setState 关掉的抽屉,会把焦点留在一个已经不在页面上的面板里。
避免
- 别在 className 里把这段位移再声明一遍。面板带着 data-folio-animated,在减少动效下它的 transform 是被整个去掉的;而你自己加的第二个 transform 只吃得到那条通用兜底,所以它照样会到——晚百分之一毫秒,从你写的那个位置。
- 元素本身没有定位的 OverlayContainer,会把抽屉停靠到错的那个盒子上:一旦指定 container,面板就从 fixed 切成 absolute,于是一个没有定位的 container 把它送到的是最近那个有定位的祖先,而不是那个框。
- 它是一个模态对话框,所以后面的页面被锁住滚动、指针也是惰性的:需要读者一边看页面一边调的筛选面板,不该住在这里。那是 Popover 的活,或者干脆在布局里开一栏。
示例
sides
四条边,按阅读顺序命名,而不是按左右:end 在英文里是右边,在阿拉伯文里是左边,靠的是一条自带 rtl 变体的类名字符串,所以没有第二条需要同步维护的代码路径。把这块画布切到 RTL,看着 start 和 end 对调。start 和 end 是一根 min(24rem, 92vw) 的满高列;top 和 bottom 是一条满宽、最高 85vh 的带子。无论从哪条边出来,每个 Sheet 都有一个关闭按钮——不像 Dialog,这里没有 showClose 可以把它关掉。
room to scroll
Dialog 接不住的那个场景:一份长到要滚动的筛选列表。停靠在 end 上,这块面板拿到的是整个视口的高度,而不是 Dialog 那个 32rem × 85vh 的盒子,读者在列表滚动时仍然守着页面边缘这个锚。注意那些文字:Checkbox 自己一个字都不渲染,所以那些词、以及它们给出的点击区域,是调用处的活儿。页脚两个控件都是 SheetClose:通过 Radix 关闭,才会把焦点放回触发器,而不是把它留在一块已经不在页面上的面板里。
a wide band
又宽又矮的内容属于底边,不属于一根柱子。start 和 end 是一根 24rem 的列,一行四个字段硬塞进去,会折成一条读者要滚的带子;bottom 是满宽、最高 85vh,所以那一行还是一行。它仍然是一个模态对话框——背后的页面被锁住滚动、也不接受指针——所以它适合读者设完就关掉的一个范围,不适合他们要一边看一边干活的面板。那种是 Popover,或者布局里的一列。
组成部分
在调用处组合,而不是通过 props 配置——所以这个组件没有预料到的布局,依然表达得出来。
SheetContent
停靠在视口某一边的面板。
它是一个模态对话框——Radix 的那个,所以焦点陷阱、Escape 键、滚动锁定和 aria-modal 的接线,和 Dialog 拿到的是同一套。差别只在它停在哪、从哪个方向进来,这也正是它复用那份实现而不是再写一遍的原因:第二个焦点陷阱,就是第二个会出错的焦点陷阱。
标题是必需的,不管可不可见。一个没有可访问名称的模态,会把读屏用户丢进一个没有名字、也没有退路的区域。
它会 portal 到外层 OverlayContainer 指名的那个元素里;有这么一个容器时,它停靠的是那个元素的边,而不是视口的边。
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| title必填 | ReactNode | — | |
| closeLabel | string | 'Close' | 关闭操作的无障碍名称,由应用提供对应语言的文字。 |
| description | ReactNode | — | |
| hideTitle | boolean | false | 在视觉上隐藏标题,但为辅助技术保留它。 |
| side | SheetSide | 'end' | 停靠在哪条边上。默认 `end`。 |
同时接受 Omit<ComponentProps<typeof DialogPrimitive.Content>, 'title'> 里的全部属性,它们会直接透传给底层元素,不再逐条列出。
再导出
Sheet = DialogPrimitive.RootRadix Dialog 的根节点、触发器和关闭按钮——sheet 就是一个靠边停放的 dialog。
SheetTrigger = DialogPrimitive.TriggerSheetClose = DialogPrimitive.Close类型
export type SheetSide = 'start' | 'end' | 'top' | 'bottom'键盘操作
| 按键 | 作用 |
|---|---|
| Escape | 关闭它,焦点回到触发器。 |
| Tab | 在面板内部循环。 |
无障碍
- 复用 Dialog 的焦点陷阱、Escape 与滚动锁定,而不是再实现一遍——第二个焦点陷阱就是第二个会出错的焦点陷阱。
- 标题必填,无论可不可见。