toFrankie / blog

种一棵树,最好的时间是十年前。其次,是现在。
20 stars 1 forks source link

Safari/WebKit 无法正确渲染 <foreignObject> 中的 HTML 元素 #312

Open toFrankie opened 1 year ago

toFrankie commented 1 year ago

配图源自 Freepik

实锤了,Safari 就是新时代的 IE 浏览器。原因是有些东西在 Safari 渲染表现与预期(标准)不一致,而且 Safari for Mac 跟 Safari for iOS 的表现还不一定是相同的。

背景

今天遇到了这样一个问题。举个例子,假设外层一个 max-width: 430px 的 section 元素,里面是一个 svg 元素,里面包含动画还有嵌套了一些元素。预期表现是:点击红色区域,绿色背景透明度匀速从 0 切换至 1。

<section style="max-width: 430px; margin: auto; overflow: hidden; font-size: 0">
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 350 350" preserveAspectRatio="xMidYMin meet" style="pointer-events: none; width: 100%; background-color: red">
    <foreignObject x="0" y="0" width="100%" height="100%">
      <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 350 350" preserveAspectRatio="xMidYMin meet" style="opacity: 0; background-color: green">
        <animate attributeName="opacity" begin="click" from="0" to="1" calcMode="linear" dur="1s" fill="freeze" restart="never" />
        <rect x="0" y="0" width="100%" height="100%" fill="transparent" style="pointer-events: visible">
          <set attributeName="visibility" begin="click" to="hidden" fill="freeze" restart="never" />
        </rect>
      </svg>
    </foreignObject>
  </svg>
</section>
body {
  margin: 20px;
}

根据所设置的 viewBox="0 0 350 350"preserveAspectRatio="xMidYMin meet" 以及 width: 100%,按道理的话,红色的 <svg> 及其内嵌套 <foreignObject><svg>,应该都是同等大小的正方形,而且取决于父元素 <section> 的宽度。

是的,这个在 Chrome 表现没问题,但在 Safari for Mac 上就出现问题了,离奇的是 Safari for iOS 也是正常的

案例一

如下图,此时 <body> 的宽度是大于 430px,因此 <section> 的宽度为 430px,自然 <svg> 的宽度就是 430px

但是,当我们点击蓝色框之外,红色区域(截图由于选中元素,该区域表现为橘色)以内的位置,你知道 Safari 定位到的元素是什么吗?

嗯......它定位到 <section> 元素了。意思就是说,内部的 元素区域并未覆盖到点击区,但我宽高明明设置的都是 100%,就很离谱。

但是,我在右侧 Elements 选项卡选中 <rect> 元素时,它表现的区域明明就是占满的啊,也就是 430 * 430

Safari 你在玩我?

经多次测试,它可点击区域只有 350 * 350,也就是 viewBox 那个空间。

解决办法

由于是 Safari 的 bug,目前只能用一些治标不治本的方法,用魔法打败魔法。

<rect> 设置 transform: scale(2); transform-origin: left top;,其父级的 <svg> 设置 overflow: visible。由于 <foreignObject> 元素默认为 overflow: hidden,因此不用担心点击 430 * 430 之外的位置会触发事件。

案例二

利用 <svg> 做了一个循环切换的交互,同样地,它在 Chrome 一切安好,而在 Safari 下则惊喜满满。

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 350 350" preserveAspectRatio="xMidYMin meet" style="width: 100%">
  <foreignObject x="0" y="0" width="100%" height="100%">
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 350 350" preserveAspectRatio="xMidYMin meet" style="width: 100%">
      <foreignObject x="0" y="0" width="100%" height="100%">
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 350 350" preserveAspectRatio="xMidYMin meet" style="opacity: 1; width: 100%; background-size: cover; background-image: url(https://cdn.jsdelivr.net/gh/toFrankie/blog/images/1682475354583.png); background-color: red">
          <animate attributeName="opacity" begin="0s" keyTimes="0; 0.22222222; 0.33333333; 1" values="1; 1; 0; 0" calcMode="linear" dur="9s" repeatCount="indefinite" />
        </svg>
      </foreignObject>
      <foreignObject x="0" y="0" width="100%" height="100%">
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 350 350" preserveAspectRatio="xMidYMin meet" style="opacity: 0; width: 100%; background-repeat: no-repeat; background-size: cover; background-position: top center; background-image: url(https://cdn.jsdelivr.net/gh/toFrankie/blog/images/1682475369330.png); background-color: green">
          <animate attributeName="opacity" begin="0s" keyTimes="0; 0.22222222; 0.33333333; 0.55555556; 0.66666667; 0.666666670001; 1" values="0; 0; 1; 1; 0; 0; 0" calcMode="linear" dur="9s" repeatCount="indefinite" />
        </svg>
      </foreignObject>
      <foreignObject x="0" y="0" width="100%" height="100%">
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 350 350" preserveAspectRatio="xMidYMin meet" style="opacity: 0; width: 100%; background-repeat: no-repeat; background-size: cover; background-position: top center; background-image: url(https://cdn.jsdelivr.net/gh/toFrankie/blog/images/1682475408407.png); background-color: blue">
          <animate attributeName="opacity" begin="0s" keyTimes="0; 0.55555556; 0.66666667; 0.88888889; 0.99999999; 1" values="0; 0; 1; 1; 0; 0" calcMode="linear" dur="9s" repeatCount="indefinite" />
        </svg>
      </foreignObject>
      <foreignObject x="0" y="0" width="100%" height="100%">
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 350 350" preserveAspectRatio="xMidYMin meet" style="opacity: 0; width: 100%; background-repeat: no-repeat; background-size: cover; background-position: top center; background-image: url(https://cdn.jsdelivr.net/gh/toFrankie/blog/images/1682475354583.png); background-color: red">
          <animate attributeName="opacity" begin="0s" keyTimes="0; 0.88888889; 0.99999999; 1" values="0; 0; 1; 0" calcMode="linear" dur="9s" repeatCount="indefinite" />
        </svg>
      </foreignObject>
    </svg>
  </foreignObject>
</svg>

Safari 表现出「忽大忽小」的问题。如下图,灰色背景大小为 430 430,而红色背景处则是 350 350。

由于录制 GIF 太麻烦了,你可以使用 Safari 打开链接体验一下:https://codepen.io/tofrankie/full/abRWpaE

解决方法

由于 <foreignObject> 的坑,那就不要嵌套多层,所以可以这样处理,结构上也更清晰。

<section>
  <section style="height: 0">
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 350 350" preserveAspectRatio="xMidYMin meet" style="opacity: 1; width: 100%; background-size: cover; background-image: url(https://cdn.jsdelivr.net/gh/toFrankie/blog/images/1682475354583.png); background-color: red">
      <animate attributeName="opacity" begin="0s" keyTimes="0; 0.22222222; 0.33333333; 1" values="1; 1; 0; 0" calcMode="linear" dur="9s" repeatCount="indefinite" />
    </svg>
  </section>
  <section style="height: 0">
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 350 350" preserveAspectRatio="xMidYMin meet" style="opacity: 0; width: 100%; background-repeat: no-repeat; background-size: cover; background-position: top center; background-image: url(https://cdn.jsdelivr.net/gh/toFrankie/blog/images/1682475369330.png); background-color: green">
      <animate attributeName="opacity" begin="0s" keyTimes="0; 0.22222222; 0.33333333; 0.55555556; 0.66666667; 0.666666670001; 1" values="0; 0; 1; 1; 0; 0; 0" calcMode="linear" dur="9s" repeatCount="indefinite" />
    </svg>
  </section>
  <section style="height: 0">
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 350 350" preserveAspectRatio="xMidYMin meet" style="opacity: 0; width: 100%; background-repeat: no-repeat; background-size: cover; background-position: top center; background-image: url(https://cdn.jsdelivr.net/gh/toFrankie/blog/images/1682475408407.png); background-color: blue">
      <animate attributeName="opacity" begin="0s" keyTimes="0; 0.55555556; 0.66666667; 0.88888889; 0.99999999; 1" values="0; 0; 1; 1; 0; 0" calcMode="linear" dur="9s" repeatCount="indefinite" />
    </svg>
  </section>
  <section style="height: 0">
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 350 350" preserveAspectRatio="xMidYMin meet" style="opacity: 0; width: 100%; background-repeat: no-repeat; background-size: cover; background-position: top center; background-image: url(https://cdn.jsdelivr.net/gh/toFrankie/blog/images/1682475354583.png); background-color: red">
      <animate attributeName="opacity" begin="0s" keyTimes="0; 0.88888889; 0.99999999; 1" values="0; 0; 1; 0" calcMode="linear" dur="9s" repeatCount="indefinite" />
    </svg>
  </section>
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 350 350" preserveAspectRatio="xMidYMin meet" style="width: 100%"></svg>
</section>

原因

这是 Webkit 的 Bug,相关链接:

该问题早在 2009 年就提出了,至今仍然没有任何进展,隔壁 Chromium 的 Blink 已在 2020 年 9 月修复。其中一个可复现的示例:https://codesandbox.io/s/chrome-foreignobject-defect-wf91j。在 Safari 打开使用 + + + - 去缩放页面就能看到。

我用 Chrome 62 亲测了一下,确实也有问题,而且区域更小了。

Chrome 62

简言之,根本原因就是 Safari/WebKit 无法正确渲染 <foreignObject> 中的 HTML 元素

实锤了,Safari 就是新时代的 IE 浏览器。

References

Damon99999 commented 1 year ago

赞一个,但我还是没解决safari 兼容问题 渲染位置偏移,放大缩小后偏位更夸张,拖动元素偶尔出现残影