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

标签: CSS3, Chrome, video

已有 4 条评论

  1. 2025年10月新盘 做第一批吃螃蟹的人coinsrore.com
    新车新盘 嘎嘎稳 嘎嘎靠谱coinsrore.com
    新车首发,新的一年,只带想赚米的人coinsrore.com
    新盘 上车集合 留下 我要发发 立马进裙coinsrore.com
    做了几十年的项目 我总结了最好的一个盘(纯干货)coinsrore.com
    新车上路,只带前10个人coinsrore.com
    新盘首开 新盘首开 征召客户!!!coinsrore.com
    新项目准备上线,寻找志同道合的合作伙伴coinsrore.com
    新车即将上线 真正的项目,期待你的参与coinsrore.com
    新盘新项目,不再等待,现在就是最佳上车机会!coinsrore.com
    新盘新盘 这个月刚上新盘 新车第一个吃螃蟹!coinsrore.com

  2. 新车新盘 嘎嘎稳 嘎嘎靠谱

  3. Holy Crab

    一般人遇不到这么奇葩的事情……

添加新评论