preact icon indicating copy to clipboard operation
preact copied to clipboard

<picture> element rendered with preact is broken in safari

Open gpoitch opened this issue 3 years ago • 3 comments

In safari (latest v14), a picture element rendered with preact does not properly change its source as the window size changes. It works correctly on Chrome and works correctly in all browsers when not rendered with preact.

Additionally, it also unneccesarily downloads the fallback img src in the preact version.

I'm guessing it's an issue with the order the vdom is built/appended, probably adding the img to the dom before the sources?

Reproduction

https://codesandbox.io/s/purple-monad-dt5f2

Steps to reproduce

Resize the window smaller + larger and the picture source should change.

Expected Behavior

The preact rendered version should change it's source as the window size changes, like the non-preact version.

Actual Behavior

The preact version only renders whichever source matches when the window initially loads and never switches.

gpoitch avatar Mar 05 '21 03:03 gpoitch

That's interesting. It seems like Safari is incapable of rendering dynamically created <picture> elements properly. The following snippet reproduces the issue for me without Preact in play:

// HTML: <div id="app"></div>
//
const p = document.createElement("picture");
const s1 = createSource("650px", "https://googlechrome.github.io/samples/picture-element/images/kitten-large.png")
const s2 = createSource("465px", "https://googlechrome.github.io/samples/picture-element/images/kitten-medium.png")
const img = createImg("https://googlechrome.github.io/samples/picture-element/images/kitten-small.png");

p.appendChild(s1)
p.appendChild(s2);
p.appendChild(img);

document.getElementById("app").appendChild(p)

EDIT: This error seems to occur in every framework. Tried React, Vue and Svelte. Makes me think that this is a real issue in Safari itself

marvinhagemeister avatar Mar 05 '21 12:03 marvinhagemeister

Ok, so the only thing that seems to trigger Safari to re-evaluate the <source>-elements is to update one of it's attribute. This can be done automatically in Preact via the following options hook (add this once to the project).

import { options } from "preact";

const oldDiffed = options.diffed
options.diffed = (vnode) => {
  if (vnode.type === 'picture') {
    const dom = vnode._dom || vnode.__e

    dom.querySelectorAll("source").forEach(s => {
      // Set the `media` attribute to the same value to force
      // Safari to re-evaluate image sizes of the <picture>-element
      s.setAttribute("media", s.getAttribute("media"))
    })
  }
  if (oldDiffed) oldDiffed(vnode)
}

Filed an issue over at the webkit bug tracker. Hopefully they'll fix it in Safari https://bugs.webkit.org/show_bug.cgi?id=222801

marvinhagemeister avatar Mar 05 '21 13:03 marvinhagemeister

@marvinhagemeister thank you for looking into this and for the great workaround. Indeed a safari bug. Hoping it gets some attention since it's a general js issue.

gpoitch avatar Mar 05 '21 18:03 gpoitch

Closing, since this is issue has been fixed in Safari for a while. See: https://github.com/WebKit/WebKit/commit/b70d55af2a228821e2b430443e6196894a980e24

marvinhagemeister avatar Oct 15 '22 12:10 marvinhagemeister