标签 Chrome 下的文章

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 元素的内部样式。这时你会发现,点击三个点其实是有反应的,对应的元素在下图的红圈中:

1.jpg

为了节省点流量,图片经过压缩,有点模糊不清,请看官谅解。下同。

查看其样式,可以发现它的 position 计算结果非常奇怪:

2.jpg

经过尝试,positiontoprightbottom 的值不受外面的样式影响,它是浏览器内部计算出来的。也就是说,不管你在 video 的父容器上怎么折腾,出现异常的 toprightbottom 的值永远是 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 属性会创建新的层叠上下文。当元素祖先的 transformperspectivefilterbackdrop-filter 属性非 none,容器由视口改为该祖先。

真的没想到.jpg!video 元素的三个点弹出菜单就是 fixed 定位。

不过,值得指出的是,这个弹出菜单设置了相对于三个点图标的定位——锚点定位:

/* css 弹出菜单 */
position-anchor: --internal-media-control-button-anchor;
/* css 三个点图标 */
anchor-name: --internal-media-control-button-anchor;

这指定了弹出菜单相对三个点的图标来确定位置。不过这不影响本文的结论。

5、如何利用上面的特点?

Chrome 是如何计算出上面第 2 点那些 toprightbottom 值的不得而知,因为这是浏览器内部实现的。既然这个弹出菜单的位置能够被外部影响,那么我们可以尝试利用上面第 4 点提到的 fixed 定位的特性,看看能不能让这个弹出菜单相对一个咱们自己的元素来定位?

根据上面提到的 MDN 文档,让任意一个 video 的父元素的 transformperspectivefilterbackdrop-filter 属性值不为 none,就会让 video 中的弹出菜单变成相对其进行定位。尝试给 .inner-div 设置 transform

.inner-div {
    transform: translate(0, 0);
}

刷新页面,点击三个点图标,菜单仍旧不能显示出来。难道这个方法不起作用吗?改为给 .div 设置 transform

.div {
    transform: translate(0, 0);
}

刷新页面,点击 video 的三个点图标,这次弹出菜单出来了!

经过多次尝试,发现了下面的规律:

  1. 如果 video 有多个 fixed 定位的父元素,比如层次结构为 div1 - div2 - div3 - video,那么给最外层div1 设置 transformperspectivefilterbackdrop-filter 才会起作用
  2. 除了 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 滤镜起的作用,而弹出菜单显示出来了:

3.jpg

视频内容为直播带货_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