modal Dialog 内 portal 浮层列表无法滚动(WEB-1286)
版本: 0.9.0 · 类型: 🐛 Bug Fix
问题
在 modal Dialog 内使用 DatePicker(复现路径:web4 分享弹窗 → Link expires → Custom date → 打开日历),点开日历顶部的月份 / 年份下拉,列表选项超出可视高度(年份默认为当前年 ±50 共 101 项),滚轮 / 触摸板滚不动,只能看到列表顶部一小段,无法跳到目标年份。
同一下拉在组件库独立场景(非 modal 的 Popover 内)可正常滚动,问题仅在浮层被 modal 层(Dialog / modal Popover)包裹时复现。
根本原因
两层原因叠加:
-
react-remove-scroll的滚动隔离:RadixDialog用react-remove-scroll锁定背景滚动,其shards白名单只含 Dialog 内容节点。它在document上注册冒泡阶段、非 passive 的wheel监听器,对目标不在白名单内的 wheel 事件走隔离分支(noIsolation默认false)直接preventDefault。而DropdownMenu的内容 portal 到document.body,不在白名单内,被判定为「外部滚动」而锁死。 -
useStopWheelPropagation从未真正绑定监听(真正的 bug):该 hook 本意是在浮层内容上stopPropagation,阻止 wheel 冒泡到 document、从而绕过上面的隔离逻辑。但旧实现用useEffect(fn, [ref])读ref.current:依赖稳定只跑一次,而 RadixContent的 DOM 经 Portal / Presence 延迟挂载,effect 首次运行时ref.current仍为null,if (!el) return直接退出,且依赖不变永不重跑。等菜单真正打开、DOM 出现时,监听器早已放弃绑定。经 Playwright CT 复现验证:modal Dialog 内下拉几何正常(
overflowY: auto、scrollHeight 4052 > clientHeight 280),但 hook 的 effect 三次运行ref.current均为null(绑定 0 次),wheel 事件照常冒泡到 document 并被react-remove-scrollpreventDefault(defaultPrevented: true)→ 滚不动。手动在内容节点上补一个stopPropagation监听即可恢复滚动(验证了修复方向)。
改动文件
packages/design/src/utils/use-stop-wheel-propagation.tspackages/design/src/components/ui/dropdown-menu.tsxpackages/design/src/components/ui/context-menu.tsxpackages/design/src/components/ui/menubar.tsxpackages/design/src/components/ui/popover.tsxpackages/design/src/components/ui/select.tsxpackages/design/src/utils/__tests__/use-stop-wheel-propagation.test.ts
改动内容
- 将
useStopWheelPropagation从「接收RefObject+ 在useEffect里读ref.current」重写为返回一个稳定的 ref 回调(React.RefCallback<T>,useCallback包裹):在 React 把 DOM 节点赋给 ref 的精确时刻(含 Portal / Presence 延迟挂载)绑定wheel的stopPropagation监听,节点卸载(ref 收到null)时解绑,用useRef保存 cleanup。彻底规避「effect 首次运行时节点尚未挂载」的时序陷阱。 - 所有调用方从
const localRef = useRef(...); useStopWheelPropagation(localRef); ref={mergeRefs(ref, localRef)}改为const stopWheelRef = useStopWheelPropagation<...>(); ref={mergeRefs(ref, stopWheelRef)},覆盖DropdownMenu、ContextMenu、Menubar(Content + SubContent 共 2 处)、Popover、Select全部 8 处。 - 新增 hook 单测:断言绑定后 wheel 不再冒泡到祖先、
ref(null)后恢复冒泡,且 ref 回调跨重渲染稳定。
影响面
所有经 Portal 渲染的浮层(DropdownMenu / ContextMenu / Menubar / Popover / Select)在 modal Dialog、modal Popover 等 react-remove-scroll 包裹场景下,内部滚动列表现在均可正常 wheel 滚动。非 modal 场景行为不变(stopPropagation 对普通下拉无副作用:内容不可滚时既不滚动也不冒泡,符合浮层预期)。此前该 hook 实际从未生效,本次修复不改变任何视觉,仅让「阻止 wheel 冒泡」的既定意图真正落地。