## 39 · 吸顶容器 Sticky ### 39.1 用途 让一段内容在滚动时**贴住滚动容器的边缘**保持可见(列表标题、分组、购物车合计条)。移动端与桌面端的关键差别是**滚动惯性**:桌面端滚动是离散的滚轮步进,用「监听 scroll + `position: fixed`」的 JS 方案看不出问题;触屏上滚动是带惯性的连续位移,`fixed` 方案会在惯性阶段出现一帧的抖动与跳位,且元素脱离文档流后原位置塌陷、必须手动补一个占位元素 —— 所以这里用 CSS 原生的 `position: sticky`,贴合由浏览器在合成层完成,不监听滚动。 ### 39.2 结构(anatomy) - `sticky`:根元素,**就是滚动内容流里的那一行**(sticky 不脱离文档流,因此不需要占位元素) - `title`:标题文字,单行省略,占满剩余宽度 - `extra`:右侧附加说明(数量、合计等),可选 - `action`:右侧动作按钮(原生 `button`,热区 44px),可选 ### 39.3 变体维度 - `position`:`top`(贴顶,最常用)/ `bottom`(贴底,合计条一类) - `safeArea`:`false`(只用偏移量)/ `true`(偏移量再叠加刘海或底部横条安全区) - `shadow`:`false`(恒定无投影)/ `true`(**仅吸顶后**才有投影,用于区分「贴住了」与「还在流里」) ### 39.4 状态 - default:未吸顶,正常参与文档流 - stuck:`is-stuck` —— 已贴合边缘(加下边框 / 投影)。**判定由宿主负责**:贴合时机只有浏览器知道,组件不监听滚动 ### 39.5 交互与触控 - 贴合位置由组件级变量 `--kole-m-sticky-offset` 决定,默认等于导航栏高度;业务侧覆盖它即可适配自有导航(贴底用法通常覆盖为 `0px`) - `safeArea=true` 时偏移叠加 `--kole-m-safe-top` / `--kole-m-safe-bottom`:支持 `env()` 的浏览器拿到真实刘海高度,不支持的拿到 `0px`(不会因非法值整条声明被丢弃) - **组件本身不监听滚动**:`is-stuck` 由宿主按滚动位置切换。原因有两条 —— ① 贴合判定与滚动容器强绑定,组件无法知道自己在哪个容器里;② 逐帧读写滚动位置在移动端会挤占合成线程,原生 sticky 已经把这件事交给浏览器 - 触屏热区:整条高度 ≥ 44px;右侧动作按钮自身撑满 44px,负外边距吸收不撑高栏体 - `position=bottom` 时吸顶后的描边换到**上边**(视觉上仍是「靠内容那一侧」的分隔) - 不响应长按、双击与横滑:吸顶条不是可拖拽元素(拖拽排序不在本组件范围内) ### 39.6 无障碍 - 根是普通容器,标题文字正常参与读屏朗读;`is-stuck` 是纯视觉增强,不添加任何 `aria-*`(吸顶不是状态变化,播报它只会打断用户) - 右侧动作是原生 `button`,名称由可见文字承担;装饰性图形必须 `aria-hidden="true"` - 吸顶不改变文档顺序:读屏与键盘的遍历顺序与未吸顶时完全一致(这也是不用 `fixed` 的另一个理由 —— `fixed` 会它把挪出正常流,键盘聚焦时的滚动定位会跳) - `prefers-reduced-motion` 下不引入任何过渡(组件本身不加动效,此处显式声明避免宿主样式穿透) ### 39.7 doNotInvent - 吸顶触发的位移/缩放动画(如标题从大字缩成小字)—— 规格只定义了贴合,没有定义形态演变 - 多个吸顶条的层叠顺序与相互推挤(层叠上下文规则由宿主决定) - 进入/离开视口时的埋点事件与曝光统计 - 拖拽排序与吸附 ### 39.8 unknowns - 吸顶判定是否应由组件内部提供一个可选的滚动监听辅助(当前完全交给宿主) - 是否需要在吸顶时自动隐藏相邻内容(当前不做,靠宿主布局) - 贴底形态在内容不足一屏时是否应始终贴底(当前 sticky 语义下会贴着容器底,未做额外分支) ---