跳到主要内容

modal Dialog 内 portal 浮层列表无法滚动(WEB-1286)

版本: 0.9.0 · 类型: 🐛 Bug Fix

WEB-1286

问题

在 modal Dialog 内使用 DatePicker(复现路径:web4 分享弹窗 → Link expires → Custom date → 打开日历),点开日历顶部的月份 / 年份下拉,列表选项超出可视高度(年份默认为当前年 ±50 共 101 项),滚轮 / 触摸板滚不动,只能看到列表顶部一小段,无法跳到目标年份。

同一下拉在组件库独立场景(非 modal 的 Popover 内)可正常滚动,问题仅在浮层被 modal 层(Dialog / modal Popover)包裹时复现。

根本原因

两层原因叠加:

  1. react-remove-scroll 的滚动隔离:Radix Dialogreact-remove-scroll 锁定背景滚动,其 shards 白名单只含 Dialog 内容节点。它在 document 上注册冒泡阶段、非 passive 的 wheel 监听器,对目标不在白名单内的 wheel 事件走隔离分支(noIsolation 默认 false)直接 preventDefault。而 DropdownMenu 的内容 portal 到 document.body,不在白名单内,被判定为「外部滚动」而锁死。

  2. useStopWheelPropagation 从未真正绑定监听(真正的 bug):该 hook 本意是在浮层内容上 stopPropagation,阻止 wheel 冒泡到 document、从而绕过上面的隔离逻辑。但旧实现用 useEffect(fn, [ref])ref.current:依赖稳定只跑一次,而 Radix Content 的 DOM 经 Portal / Presence 延迟挂载,effect 首次运行时 ref.current 仍为 nullif (!el) return 直接退出,且依赖不变永不重跑。等菜单真正打开、DOM 出现时,监听器早已放弃绑定。

    经 Playwright CT 复现验证:modal Dialog 内下拉几何正常(overflowY: autoscrollHeight 4052 > clientHeight 280),但 hook 的 effect 三次运行 ref.current 均为 null(绑定 0 次),wheel 事件照常冒泡到 document 并被 react-remove-scroll preventDefaultdefaultPrevented: true)→ 滚不动。手动在内容节点上补一个 stopPropagation 监听即可恢复滚动(验证了修复方向)。

改动文件

  • packages/design/src/utils/use-stop-wheel-propagation.ts
  • packages/design/src/components/ui/dropdown-menu.tsx
  • packages/design/src/components/ui/context-menu.tsx
  • packages/design/src/components/ui/menubar.tsx
  • packages/design/src/components/ui/popover.tsx
  • packages/design/src/components/ui/select.tsx
  • packages/design/src/utils/__tests__/use-stop-wheel-propagation.test.ts

改动内容

  1. useStopWheelPropagation 从「接收 RefObject + 在 useEffect 里读 ref.current」重写为返回一个稳定的 ref 回调React.RefCallback<T>useCallback 包裹):在 React 把 DOM 节点赋给 ref 的精确时刻(含 Portal / Presence 延迟挂载)绑定 wheelstopPropagation 监听,节点卸载(ref 收到 null)时解绑,用 useRef 保存 cleanup。彻底规避「effect 首次运行时节点尚未挂载」的时序陷阱。
  2. 所有调用方从 const localRef = useRef(...); useStopWheelPropagation(localRef); ref={mergeRefs(ref, localRef)} 改为 const stopWheelRef = useStopWheelPropagation<...>(); ref={mergeRefs(ref, stopWheelRef)},覆盖 DropdownMenuContextMenuMenubar(Content + SubContent 共 2 处)、PopoverSelect 全部 8 处。
  3. 新增 hook 单测:断言绑定后 wheel 不再冒泡到祖先、ref(null) 后恢复冒泡,且 ref 回调跨重渲染稳定。

影响面

所有经 Portal 渲染的浮层(DropdownMenu / ContextMenu / Menubar / Popover / Select)在 modal Dialog、modal Popoverreact-remove-scroll 包裹场景下,内部滚动列表现在均可正常 wheel 滚动。非 modal 场景行为不变(stopPropagation 对普通下拉无副作用:内容不可滚时既不滚动也不冒泡,符合浮层预期)。此前该 hook 实际从未生效,本次修复不改变任何视觉,仅让「阻止 wheel 冒泡」的既定意图真正落地。