preact
preact copied to clipboard
<picture> element rendered with preact is broken in safari
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.
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
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 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.
Closing, since this is issue has been fixed in Safari for a while. See: https://github.com/WebKit/WebKit/commit/b70d55af2a228821e2b430443e6196894a980e24