问题引出
考虑如下场景:一个菜单,限高可滚动;其中每一个元素又可以在侧边展开为小菜单,小菜单也同样限高可滚动
主流的思路应该是利用大部分组件库都有的 Popup 浮层(我猜的
但是如果你的子菜单弹出层是父菜单的元素的子元素,子菜单是靠父菜单的元素 hover
唤出的。比如这样👇🏻
<div class="tw-w-[100vw] tw-h-[100vh] tw-flex tw-items-center tw-justify-center">
<div class="content">
<div v-for="i in 10" class="item">
{{ i }}
<div class="sub-content content">
<div v-for="j in 10" class="item">
{{ j }}
</div>
</div>
</div>
</div>
</div>
我的第一直觉告诉我,这不就是一个 overflow
的问题吗?我直接使其在 y 轴上自由滚动,x 轴上也无遮无挡。
overflow-y: auto;
overflow-x: visible;
实际情况是,刷新完页面,清完缓存,再刷再清后,不论我怎么hover,子菜单都弹不出来。最后检查了一下控制台的 computed
属性,发现我的 overflow-x: visible
已经被浏览器偷偷换成了 overflow: auto
为什么明明我写的是 visible
,浏览器却解释成 auto
了呢?再尝试将y轴设为 hidden
, x 轴保持 visible
。发现浏览器依然将 x 轴解释成了 auto
。
浏览器:“你弹子菜单出来,可以,但是不能滚动;你要滚动,可以,但是不能弹子菜单出来。”
问题出现
简单翻阅资料后在 sof 发现了想要的回答,有个老哥引用了一段 W3C 规范,原文如下
The computed values of ‘overflow-x’ and ‘overflow-y’ are the same as their specified values, except that some combinations with ‘visible’ are not possible: if one is specified as ‘visible’ and the other is ‘scroll’ or ‘auto’, then ‘visible’ is set to ‘auto’. The computed value of ‘overflow’ is equal to the computed value of ‘overflow-x’ if ‘overflow-y’ is the same; otherwise it is the pair of computed values of ‘overflow-x’ and ‘overflow-y’.
看到这里顿时茅塞顿开,原来这是 W3C规定的,并不是其他问题导致的无法实现。但是为什么要这样子规定呢?这一层回答下面也有很多感到不解的回复
仔细一看,回答是在2011年的,下面几个相关评论也直到17/18年左右,有没有可能已经 W3C 更新了呢?(明显没有,要是更新了不就没有那么多问题了)
点开老哥引用的 W3C 链接,发现确实更新了,overflow
属性已经不在那篇文档里了,又稍微找了找,发现了更新的两篇关于 overflow
的草稿
https://www.w3.org/TR/2022/WD-css-overflow-3-20221231/
https://www.w3.org/TR/2022/WD-css-overflow-4-20221231/
在CSS3的草稿中摸了半天,看到了这么一句话
The visible/clip values of overflow compute to auto/hidden (respectively) if one of overflow-x or overflow-y is neither visible nor clip.
虽然没看到为什么要这样调整,但是W3C这么做一定有他的道理(确信。既然如此,我们只能另辟蹊径了。
尝试解决
说是另辟蹊径,但是想了半天也没得思路。
这个时候我的导师给我支了一招:“你可以试试用 transform 模拟滚动”。这可真是醍醐灌顶的当头一棒。浏览器不给咱们滚,咱们可以用自己滚!
监听目标的 WheelEvent 来获得鼠标滚动的 deltaY
的值,通过对应修改容器的 transform
,妙啊!说时迟那时快,一顿操作后简单实现了一下这个想法,同时也发现了一些问题。
- 滚动方向问题:Mac 和 windows 的滚动方向不一致,甚至有些情况下触控板和鼠标的滚动都是不一致的。
- 需要计算滚动的边界问题(避免滚出去滚不回来)
- 子菜单的滚轮滚动也会被监听,导致整个菜单一起滚动
- 层级问题:向上滚动时会遮挡上方同层级的元素
- ...
就当我准备把这些难关一一攻克的时候,想起导师说的一句话:“咱们遇到的问题肯定有人都踩过坑,可以去找找有没有现成的库”。
关键词:transform + scroll + js
搜索结果:ustbhuangyi/better-scroll
虽然他是通过鼠标拖拽来实现的滚动,但是配合他的 mouse-wheel
插件也可以实现鼠标滚动。简单上手尝试了一下之后发现确实好用,很多细小问题都解决了。这是初步尝试的效果👇🏻
从上面的效果图中可以看出来,虽然我们解决了基础的滚动问题,但是还有两个比较麻烦的问题没有解决
- 子菜单的滚轮滚动也会被监听,导致整个菜单一起滚动
- 层级问题:向上滚动时会遮挡上方同层级的元素
子菜单滚动问题
研究了一段时间 Better-scroll
文档后,我有了这两个想法
- 利用
Better-scroll
的nested-scroll
插件 - 利用
Better-scroll
的preventDefaultException
配置项
nested-scroll
插件就是为了嵌套滚动而设计的,应该可以完美解决。而 preventDefaultException
的说明如下
我们可以让其在子菜单滚动时让 BS
不拦截滚动,转为原生滚动,岂不美哉。
但是现实总是残酷的, nested-scroll
插件是为了拖拽而并非鼠标滚轮的嵌套滚动设计的。preventDefaultException
配置项也不知道为什么不生效...
就在要放弃的时候,转念一想,我需要配置 preventDefaultException
的目的就是为了在子菜单的滚动不触发 Better-scroll
,转为原生滚动。那么我可以稍微改造一下 mouse-wheel
插件。增加一个排除DOM的配置项,如果是被排除的DOM发出的产生的 wheelEvent
就直接放行给浏览器进行原生滚动。
在 mouse-wheel
插件源码里我找到了这个函数
private wheelHandler(e: CompatibleWheelEvent) {
// ↓ 在这里增加一个DOM判断!!
if (!this.scroll.enabled) {
return
}
this.beforeHandler(e)
// start
if (!this.wheelStart) {
this.wheelStartHandler(e)
this.wheelStart = true
}
// move
const delta = this.getWheelDelta(e)
this.wheelMoveHandler(delta)
// end
this.wheelEndDetector(delta)
}
如果当前时间的 target
是需要排除的DOM或者其子元素的话就直接 return
掉,不进行拦截。又是一番小改动之后,发现可以完美达到我们需要的效果。
层级问题
如果是想完美实现 overflow-x: visible
+ overflow-y: auto
的效果的话,是不应该会出现向上面的 GIF 那样遮挡或者溢出的情况的。这里我们应该怎么解决呢?
翻着翻着 MDN 文档,我又找到了个神奇的属性值:overflow: clip
。clip
属性与 hidden
很相似,都是将溢出的部分隐藏且禁止滚动。但不同的是 hidden
只是禁止用户滚动(鼠标滚轮、拖拽),并没有限制编程滚动(element.scrollTop
),而 clip
直接把编程滚动也给禁止了。
但是你猜怎么着,我用的是 transform
来滚动,没想到吧。我给父菜单的容器使用 overflow-y: clip
,然后让父菜单在容器里自由 transform
滚动,我们就得到了下面的效果
结语
客套话版本:
在本篇博客中,我们通过一个简单的小场景引出了 overflow-x: visible
+ overflow-y: auto
从 W3C 标准来看是不可能的问题。通过对 overflow
与 transform
属性的不断探索,我们也找到了相应的 hack 的方法,在这期间对两个属性以及 Javascript 的事件都有了更加深入的理解。
碎碎念版本:
其实这是在实习过程中遇到的问题,解决这个问题花了大概两三天时间。在解决这次这个bug的过程中,除了上面客套话版本中说到的对于几个属性的理解更加深入了以外,其实还有一点比较关键——在复杂的业务中定位Bug的能力。在修这个问题的前期,我甚至都找不到是什么原因导致的。这次遇上这个问题也可以说很幸运,学到了很多东西的同时发现了自己的一些薄弱点。路还很长,夯实基础继续前进。
感谢分享,谢谢
网站恢复访问了,求恢复友链
加回来啦
可以直接在组件上用 stopPropagation() 来阻止鼠标滚动事件传播罢,让子组件捕获不到事件就行了
有无codepen例子
大佬晚上好
牛,查阅w3c规范,我一般百度的,不是专业做法了哈
哈哈,我也是顺着stackoverflow上面的老哥的思路才想起去看看的