## 40 · 遮罩层 Overlay ### 40.1 用途 浮层的**基座**:在内容之上盖一层半透明遮罩,让下层内容退到背后(对话框、抽屉、图片预览、卡片加载态)。它与弹出层的分工是:弹出层自带方向位移与开合动画、是「能独立使用的完整浮层」;遮罩层只提供「变暗 + 拦手势 + 承载任意内容」三件事,属于被别的浮层复用的底座。移动端与桌面端的关键差别是**手势拦截面**:触屏上遮罩必须真的吃掉触摸事件(否则惯性滚动会从遮罩底下穿过去,把下层页面滚走),而桌面端只需要处理鼠标点击。 ### 40.2 结构(anatomy) - `overlay`:根元素,定位容器与开合开关;`role="presentation"`,自身不承担语义 - `scrim`:遮罩面(原生 `button`),`aria-hidden="true"` 且 `tabindex="-1"` —— 可点但不进键盘序列 - `content`:内容容器(默认插槽落点),语义由宿主决定(对话框给 `role="dialog"`、面板给 `role="region"`) ### 40.3 变体维度 - `tone`:`default`(普通遮罩)/ `strong`(浓遮罩,用于需要专注的确认)/ `blur`(叠加背景模糊,用于图片预览) - `contained`:`false`(固定全屏)/ `true`(绝对定位填充最近的定位祖先,做卡内局部遮罩) ### 40.4 状态 - closed:收起(`opacity: 0` + `pointer-events: none`,视觉与手势同时让开) - open:展开(`is-open`;`pointer-events: auto` 拦下所有手势) ### 40.5 交互与触控 - 一次轻点遮罩即关闭;`closeOnMask=false` 时不关闭(填到一半的表单不该因为一次误触丢数据) - 遮罩关闭的**键盘路径不落在遮罩上**:遮罩显式 `tabindex="-1"`,键盘用户靠内容里的关闭按钮(与宿主实现的 Esc)关闭 —— 让 Tab 停在遮罩上会让读屏读到一个没有名称的元素 - `lockScroll=true` 时宿主应锁住下层滚动(组件在根上写 `data-lock-scroll` 作为标记,实际的 `overflow: hidden` 由宿主执行:组件不该直接改 `document.body.style`,那会污染宿主状态并在多层浮层叠加时互相踩踏) - 开合动效 240ms(`--kole-m-duration-slide`),`prefers-reduced-motion: reduce` 下瞬时切换 - `contained=true` 时要求宿主祖先链上存在定位元素(`position: relative` 一类),否则会向上找到视口 ### 40.6 无障碍 - 遮罩面 `aria-hidden="true"`(纯装饰)且 `tabindex="-1"`(不进键盘序列) - 根 `role="presentation"`:容器本身不产生语义,避免读屏把它当成一个空的分组 - 内容语义完全由宿主提供:焦点陷阱、`aria-modal`、初始焦点与关闭后焦点归还都属于宿主职责(本组件是基座,不替业务决定这些策略) - 遮罩关闭不依赖颜色:开合只改透明度且有 240ms 过渡,减少动态偏好下瞬时切换 ### 40.7 doNotInvent - 焦点陷阱(focus trap)与初始焦点策略 - 多层遮罩的层叠顺序管理 - 手势下滑关闭与拖拽阻尼 - 滚动锁定的实现细节(组件只写标记,改 DOM 由宿主做) ### 40.8 unknowns - `blur` 档在低端机上的性能开销是否可接受(未做降级探测) - `contained=true` 时是否应自动为宿主补 `position: relative`(当前要求宿主自己保证) - 是否要支持「点遮罩不关但双击关」这类折中策略 ---