[css]对于“<video>若存在多个fixed定位的父元素,其三个点菜单不显示”的问题的探索
1、问题重现
有下面的 HTML 结构(没错,我说的就是 Element Plus 的 Dialog
组件):
/* css */
.div, .inner-div {
position: fixed;
inset: 0;
}
.video {
height: 500px;
}
<!-- html -->
<div class="div">
<div class="inner-div">
<video class="video" autoplay controls src="http://xxx.com/xxxx3.mp4"></video>
</div>
</div>
video 的两个父元素 .div
、.inner-div
都是 fixed
定位。一切看起来都没什么问题,直到你想点击 video 控件最右侧的三个点按钮,你会发现毫无反应。
2、video
的弹出菜单哪去了?
在 Chrome 开发者工具的设置中,勾选“显示用户代理 Shadow DOM”,再返回“元素”面板,就能看到 video
元素的内部样式。这时你会发现,点击三个点其实是有反应的,对应的元素在下图的红圈中:
为了节省点流量,图片经过压缩,有点模糊不清,请看官谅解。下同。
查看其样式,可以发现它的 position
计算结果非常奇怪:
经过尝试,position
的 top
、right
、bottom
的值不受外面的样式影响,它是浏览器内部计算出来的。也就是说,不管你在 video
的父容器上怎么折腾,出现异常的 top
、right
、bottom
的值永远是 539.33、1507.330、-144,至少在我的浏览器里是这样的(Chrome 125.0.6422.142)。
3、能否使用伪类选择器(-webkit-xxxx
)矫正错误的定位?
答案是不能。经过尝试,在 Shadow DOM 中,如果一个元素有 pseudo="-webkit-xxxx"
这样的属性,那么它就可以使用属性值作为伪类选择器,从而改变它的样式:
video::-webkit-xxxx { }
不幸的是,咱们的目标元素的 pseudo
属性的值为 -internal-media-controls-overflow-menu-list
,单词 internal 就说明了它是内部的,外部无法影响它。
4、Fixed
定位能从相对于窗口(视口,viewport
)改为相对于其他元素?
答案是可以。没想到吧.jpg!MDN 文档对此有详细的描述:
元素会被移出正常文档流,并不为元素预留空间,而是通过指定元素相对于屏幕视口(
viewport
)的位置来指定元素位置。元素的位置在屏幕滚动时不会改变。打印时,元素会出现在的每页的固定位置。fixed
属性会创建新的层叠上下文。当元素祖先的transform
、perspective
、filter
或backdrop-filter
属性非none
时,容器由视口改为该祖先。
真的没想到.jpg!video
元素的三个点弹出菜单就是 fixed
定位。
不过,值得指出的是,这个弹出菜单设置了相对于三个点图标的定位——锚点定位:
/* css 弹出菜单 */
position-anchor: --internal-media-control-button-anchor;
/* css 三个点图标 */
anchor-name: --internal-media-control-button-anchor;
这指定了弹出菜单相对三个点的图标来确定位置。不过这不影响本文的结论。
5、如何利用上面的特点?
Chrome 是如何计算出上面第 2 点那些 top
、right
、bottom
值的不得而知,因为这是浏览器内部实现的。既然这个弹出菜单的位置能够被外部影响,那么我们可以尝试利用上面第 4 点提到的 fixed
定位的特性,看看能不能让这个弹出菜单相对一个咱们自己的元素来定位?
根据上面提到的 MDN 文档,让任意一个 video
的父元素的 transform
、perspective
、filter
或 backdrop-filter
属性值不为 none
,就会让 video
中的弹出菜单变成相对其进行定位。尝试给 .inner-div
设置 transform
:
.inner-div {
transform: translate(0, 0);
}
刷新页面,点击三个点图标,菜单仍旧不能显示出来。难道这个方法不起作用吗?改为给 .div
设置 transform
:
.div {
transform: translate(0, 0);
}
刷新页面,点击 video
的三个点图标,这次弹出菜单出来了!
经过多次尝试,发现了下面的规律:
- 如果
video
有多个fixed
定位的父元素,比如层次结构为div1 - div2 - div3 - video
,那么给最外层的div1
设置transform
、perspective
、filter
或backdrop-filter
才会起作用 - 除了
div1
, 给body
(包含) 到div1
之间的任意video
的父元素设置以上 4 个 css 属性都会起作用。
例如下面的代码:
body { perspective: 0; } /* 起作用 */
a { filter: blur(0); } /* 起作用 */
div { position: fixed; }
.div1 { transform: translate(0, 0); } /* 起作用 */
.div1 { filter: blur(0); } /* 起作用 */
.div1 { perspective: 0; } /* 起作用 */
.div1 { backdrop-filter: blur(0); } /* 起作用 */
.div2 { transform: translate(0, 0); } /* 不起作用 */
.div3 { transform: translate(0, 0); } /* 不起作用 */
<body>
<a>
<div class="div1">
<div class="div2"
<div class="div3"
<video src="xxxx.mp4" controls autoplay></video>
</div>
</div>
</div>
</a>
</body>
下图展示了 filter: blur(1px)
的作用。可以看到视频内容有些模糊,这是 blur
滤镜起的作用,而弹出菜单显示出来了:
视频内容为直播带货_0-O
6、需要注意的地方
最好使用除了 transform
之外的那 3 个 css 属性,因为 transform 是常用的 css 属性,可能会被其他样式覆盖,比如,如果其他地方为上面代码的 .div1
设置了 transform: translate(-50%, -50%)
,受此影响,video
的弹出菜单不会显示在正确的位置。
UPDATE: 2024-10-23
CSS 已经支持某些 transform 属性简写了,例如transition: -50%, -50%;
、scale: .5;
具体可查看 MDN 上的文档
7、另一种解决办法
这种办法原理特别简单:隐藏原生控件,自己实现一套控件。就是有点麻烦。
8、结束
如果哪位大佬阅读了 Chrome 相关源代码,知道其中的原理,请不吝赐教。期待 Chrome 修复此 Bug。
END
一般人遇不到这么奇葩的事情……
☹☹