[whatwg/dom] Strange behavior with `insertBefore` vs `appendChild` and transitions (#880)

When using `insertBefore` with transitions and the FLIP technique, I get very strange results across browsers.

- [Smooth transitions using `appendChild`](https://flems.io/#0=N4Igxg9gdgzhA2BTEAucD4EMAONEBMQAaETAVwBcIAlReCTQlAM03jxOYEskZUBtUFEwBbZGgB0ACwoj4xdFAqIlqEAB4y8AARd8AXgA6pKFxGZlxgHzr4XKwAZ1Aejs23ARhdvb9gEze9r5WAMyBNs5aVgp4SGAUXNB8aA4oAGx+IAC+REKi4iASAFZ8JJBKKhRqzs7aAGKY8RAATgTaEJTacLoUAOQw2mBImK3N2gDuUhbadIhiSgMjiNqM+G2YUPjarSIQAG4EhlBH5TAUK6bmytQQEOf62vgQYGTzFBIA5ogUAKJIbwAhACeAEl8AAKYwbMwWRDGACUJyS51mIgc2ge0KuiBudwkYCkPHwADkIGsYPwHABdJGwFH-DwYi4w663d4Eomk8n8Dw0qCnelzPxMrGw3HswnwElkxAUvx8gUzf4hEWXMVs-GS6XckJ8o41drNNatfAobSUojaDyWvyW3X62oiLgADwIZu05tt2gcdstvKOR2YZH5CWg2idzvB8O0wG0BpaxraAForOGXYcoB68BQACpmRAdCjghOIZqWgCsDgciMz2iOHoNdS4zTO9cGyO03BbFHRD1RDk+3wBHU2XCgHwAwnZKrR4lGJFRsG3FV2zoy+wzBxRh8H8GPJ9OlLOi-CFxAl7WV82zsKN0KtzvR+Op1wZ4g56fF8uO6uKCq7yIIQPiOe7PoeFDHvOX7HLWBoAKrYPgsLmj6Vo2naVLaCmnqWqhIR+nyHqiqyeI4NgKj4BOWrgqifg1kRaoke8ZEUVRRI0f81ZtsROIaixmxsVKHFzCE9HMti4oSPxlHUaiHg1m2BoADKYGc2gANTaCCUAHM0FCKbUpLKGaiAjNOYwAAYUM0GwwFwoZQAAImQNkORZ4ZkGpABGyzZtoPnMC0yxYMozQGdoVk2bAgXNCIFmWi0MwHJmuytBMiDNvgUCygMzwvC22jgkUnnnNZQI9BcWx4HCsG1JM0xTNg5GwKebb9hIZxAkgC5RXZDnOa5iSZkYtZyR1FBdYgPW2fZQ0DRYQ0Ym1-x+ONk3TbAs3QPNDlLaNyprd11kzf1LkLWGDzGA4MDGMtcwDp1R29TFIhMpFtkhYgACa4IACTAL+6JJkq93Abu+4vm+H5ntgWTYM68IWXdIgeIdU3HdFLSvQ872wJ9P3-b+jLA2NXzbiBEPgZBn7nnDCNI-t96Pejz1Y29GMwPjf0A9eFDCiTK1g0+B6vke74njDdOI8jQHMxtMAvezvVc4TvMqgLIlC6BItQxLi5Swz4V1C0YDLK0SZYEChZtk8LxvBIXlkkCEgQMwzDZgAEhlHwyAGtXaAACpbyMDkMqkwEpXBnFJ+AQsYHNbccIBiWNYcwBHUfMbHkIgAnDkIsjq1pxn0erDnedDQXjOAfiWDp5HpfZ-HvWJ1XHrtXLHOKwBD0TU9J1zWdu0je3m6d6zsVMmN48D9tQ+LSPIMiKtM+Y5PAEr33LOz0588XTLaPy93S+y1v8uJztC91iAt1QFkftBiGi0ls0UYxnGjrpls2EvxmWbfHmMQhZwQRgrFWMSRtebfjpJ2XmvYl4DjJo+bWkMxbQ2gh6K83Z1xL1RkgimYFRYQXFlBc80C1K-lvEvVa+DwaEN1qQi8mCfxqyngdWhwtUHEPQWQmCDZagISQsoHC3pfRWkwthC0aFtBentLWHiklpKCQhP2MSCi+JNVYrJBkajGK8V)
- [Choppy transitions when interrupted using `insertBefore`](https://flems.io/#0=N4Igxg9gdgzhA2BTEAucD4EMAONEBMQAaEAMwEskZUBtUKTAW2TQDoALAF0fmPSk6IBqEAB4ArvAAE5fAF4AOiExRyjTIKUA+UfHJaADKID0enWYCMJs7v0Ama-ttaAzI53HJWvniRhO5NDUaAYoAGx2IAC+RPRMLCCsAFbUJJACQpwixsZSAGKY-hAATgRSEOKcUnAynADkMFJgSJilxVIA7uwaUohIzAKNrYhSmPj4ZSr4UqWMEABuBApQy+kwVSpqGogAShAQVXJS+BBg4gOcrADmiJwAov2ZAEIAngCS+AAUSpvqmiAASlWQSqfUQjAMUiOv22ewOrDA7Eo+AAchAJjAaAYALrA2Cg-oWKGjVR-Xb7S6I5FojE0Cy4qBrAnguzEmGCOGUpHwVHoxCYuwMpm9fouNmk2EUhHc3m0lwM5Y5crFCalfAoKRYohSCzauza+WK3KMcgADwIGqkmv1UgMBu19OWy1I4kZAWgUhNps+AKkwCkSpKqrKAFotJ6zUsoFa8JwACpqRAVTifIOIYragCsBgMQOjUmWVqVeXIxXWhaaIKkFDLnEhRzBEOutyeFSg+HIUCuAGE9Jkdoh-D7WJwINgK8Ka+siQ3Cc3OK3XR2u73yP3BymASOxxOq1POKzZyz54v252e32BAOh1vR+P85PS+sxUfGC4T23lxe11eN8O706+ZKgAqtg+DbJqdo6nqBrYlIYbWtqUEuA6DJWuy5Lwp2eDFAuiCkCUiCfI2NqNrmFYYZyrA4NgQj4N2MrEYSeYVkqAAymDrFIADUUhvFAiy4axuRooIGqIK0fbtAABpwxQqDA5DulAAAi4jycp0meuIXEAEYjLGUj6QRpRSFggjFMJUiyfJsAmYw0naiUvSLNGcymR0iClvgUD8o0pxnGWUifEkOlVHJLy1CS0x4IgVldD03TYLRsBbhWZGsOsLxICOtmKcpakaYE0aKPmjYWJlnDZYguUKUpxWFRoxVQul-R2JV1W1bA9XQI1yktWVoodTlcl1QV6lNR6RxKAYMBKK14IGMNNWjXZJSMMSNkKeZiAAJqfAAJMA+6QiGIqLR+S7nqu643tu2BRNgpoAtJC2MBVWUjXl9mbatMA7ftR37kSZ3lZdZ4rpenDXpu92Pc9r2Dcen0rd962-XlAOHcdT4HvB52MO1NwLp+11QzD-5jvDL1ve+KNdTAP1HFtsBY0DuNiqDQ3E6eX43b+d13tTiNWXkJRgCMpQhlgLzJhWJxnBcrC6eiLysBApCkLGAASXlXFwgFFrkAAKMtvUtzScTAbHkOs1HjN8IB-T1KyAm9FWWzA1u25cYxfEozvKUoeZWiRCJYF7Nt237juB8Vwe0+HVtR77DsB3lLsJ0jTb039TME0tucZ+NRVTe7y0M-nYNF2NDUTf1pWh21Fd5+jr7tTX3Ul5NJWJ53jNtwTdNVV9te9fXzXTSA81QFEgEum6zVpsUPp+gGxqRtMCHL1GMa3AmzDJp8XpZjmIcFkBuQlrWu74tWuP1gX4N8+Tf63juD57rjM4ExVPOk5DH80M373Vvlxfch4CZExbAA78t1YYAU-nffcL4h7PzJkAim797xWVAuBQQiFbT2h1HBBCWpoJSBtIafMlEpTYXTHhEyRFyrakbC4c+tCsKwAYU8fChEmIslYaKFil8pAcS4rxfiglOBWVEogcSkk1wyTjuPUuUAtKMDCkZAytxtFMLMtsSyoiWYD2KA5Jy7RECuU9IRToXkV)

The only difference is the way the DOM nodes are rearranged, which is what confuses me about this.

<details>
<summary>Code for each</summary>

`insertBefore`:

```html
<!doctype html>
<ul id="animate"><li>0</li><li>1</li><li>2</li><li>3</li></ul>
<script>
// Factored out so it's clearer what elements are added and removed

const animateRoot = document.getElementById("animate")
const elem0 = animateRoot.childNodes[0]
const elem1 = animateRoot.childNodes[1]
const elem2 = animateRoot.childNodes[2]
const elem3 = animateRoot.childNodes[3]

// ordered: [0, 1, 2, 3]
// mixed:   [2, 0, 3, 1]

function mix() { // ordered -> mixed
  setTimeout(order, 500)
  
  // First
  const first0 = elem0.getBoundingClientRect().top
  const first1 = elem1.getBoundingClientRect().top
  const first2 = elem2.getBoundingClientRect().top
  const first3 = elem3.getBoundingClientRect().top

  // Update [0, 1, 2, 3] -> [2, 0, 3, 1]
  animateRoot.insertBefore(elem2, elem0)
  animateRoot.appendChild(elem1)

  // Last + Invert
  // Note: earlier `transitionDuration` must be set before later
  // `transform`, or even more weirdness occurs (just try it and see
  // what happens).
  elem0.style.transitionDuration =
  elem1.style.transitionDuration =
  elem2.style.transitionDuration =
  elem3.style.transitionDuration = "0s"
  elem0.style.transform = `translateY(${first0 - elem0.getBoundingClientRect().top}px)`
  elem1.style.transform = `translateY(${first1 - elem1.getBoundingClientRect().top}px)`
  elem2.style.transform = `translateY(${first2 - elem2.getBoundingClientRect().top}px)`
  elem3.style.transform = `translateY(${first3 - elem3.getBoundingClientRect().top}px)`

  // Force re-layout
  document.body.offsetHeight

  // Play
  elem0.classList.add("transition")
  elem1.classList.add("transition")
  elem2.classList.add("transition")
  elem3.classList.add("transition")
  elem0.style.transform = elem0.style.transitionDuration =
  elem1.style.transform = elem1.style.transitionDuration =
  elem2.style.transform = elem2.style.transitionDuration =
  elem3.style.transform = elem3.style.transitionDuration = ""
}

function order() { // mixed -> ordered
  setTimeout(mix, 500)
  
  // First
  const first0 = elem0.getBoundingClientRect().top
  const first1 = elem1.getBoundingClientRect().top
  const first2 = elem2.getBoundingClientRect().top
  const first3 = elem3.getBoundingClientRect().top

  // Update [2, 0, 3, 1] -> [0, 1, 2, 3]
  animateRoot.insertBefore(elem1, elem3)
  animateRoot.insertBefore(elem2, elem3)

  // Last + Invert
  // Note: earlier `transitionDuration` must be set before later
  // `transform`, or even more weirdness occurs (just try it and see
  // what happens).
  elem0.style.transitionDuration =
  elem1.style.transitionDuration =
  elem2.style.transitionDuration =
  elem3.style.transitionDuration = "0s"
  elem0.style.transform = `translateY(${first0 - elem0.getBoundingClientRect().top}px)`
  elem1.style.transform = `translateY(${first1 - elem1.getBoundingClientRect().top}px)`
  elem2.style.transform = `translateY(${first2 - elem2.getBoundingClientRect().top}px)`
  elem3.style.transform = `translateY(${first3 - elem3.getBoundingClientRect().top}px)`

  // Force re-layout
  document.body.offsetHeight

  // Play
  elem0.classList.add("transition")
  elem1.classList.add("transition")
  elem2.classList.add("transition")
  elem3.classList.add("transition")
  elem0.style.transform = elem0.style.transitionDuration =
  elem1.style.transform = elem1.style.transitionDuration =
  elem2.style.transform = elem2.style.transitionDuration =
  elem3.style.transform = elem3.style.transitionDuration = ""
}

setTimeout(mix, 500)
</script>
```

`appendChild`:

```html
<!doctype html>
<ul id="animate"><li>0</li><li>1</li><li>2</li><li>3</li></ul>
<script>
// Factored out so it's clearer what elements are added and removed

const animateRoot = document.getElementById("animate")
const elem0 = animateRoot.childNodes[0]
const elem1 = animateRoot.childNodes[1]
const elem2 = animateRoot.childNodes[2]
const elem3 = animateRoot.childNodes[3]

// ordered: [0, 1, 2, 3]
// mixed:   [2, 0, 3, 1]

function mix() { // ordered -> mixed
  setTimeout(order, 500)
  
  // First
  const first0 = elem0.getBoundingClientRect().top
  const first1 = elem1.getBoundingClientRect().top
  const first2 = elem2.getBoundingClientRect().top
  const first3 = elem3.getBoundingClientRect().top

  // Update [0, 1, 2, 3] -> [2, 0, 3, 1]
  animateRoot.appendChild(elem2)
  animateRoot.appendChild(elem0)
  animateRoot.appendChild(elem3)
  animateRoot.appendChild(elem1)

  // Last + Invert
  // Note: earlier `transitionDuration` must be set before later
  // `transform`, or even more weirdness occurs (just try it and see
  // what happens).
  elem0.style.transitionDuration =
  elem1.style.transitionDuration =
  elem2.style.transitionDuration =
  elem3.style.transitionDuration = "0s"
  elem0.style.transform = `translateY(${first0 - elem0.getBoundingClientRect().top}px)`
  elem1.style.transform = `translateY(${first1 - elem1.getBoundingClientRect().top}px)`
  elem2.style.transform = `translateY(${first2 - elem2.getBoundingClientRect().top}px)`
  elem3.style.transform = `translateY(${first3 - elem3.getBoundingClientRect().top}px)`

  // Force re-layout
  document.body.offsetHeight

  // Play
  elem0.classList.add("transition")
  elem1.classList.add("transition")
  elem2.classList.add("transition")
  elem3.classList.add("transition")
  elem0.style.transform = elem0.style.transitionDuration =
  elem1.style.transform = elem1.style.transitionDuration =
  elem2.style.transform = elem2.style.transitionDuration =
  elem3.style.transform = elem3.style.transitionDuration = ""
}

function order() { // mixed -> ordered
  setTimeout(mix, 500)
  
  // First
  const first0 = elem0.getBoundingClientRect().top
  const first1 = elem1.getBoundingClientRect().top
  const first2 = elem2.getBoundingClientRect().top
  const first3 = elem3.getBoundingClientRect().top

  // Update [2, 0, 3, 1] -> [0, 1, 2, 3]
  animateRoot.appendChild(elem0)
  animateRoot.appendChild(elem1)
  animateRoot.appendChild(elem2)
  animateRoot.appendChild(elem3)

  // Last + Invert
  // Note: earlier `transitionDuration` must be set before later
  // `transform`, or even more weirdness occurs (just try it and see
  // what happens).
  elem0.style.transitionDuration =
  elem1.style.transitionDuration =
  elem2.style.transitionDuration =
  elem3.style.transitionDuration = "0s"
  elem0.style.transform = `translateY(${first0 - elem0.getBoundingClientRect().top}px)`
  elem1.style.transform = `translateY(${first1 - elem1.getBoundingClientRect().top}px)`
  elem2.style.transform = `translateY(${first2 - elem2.getBoundingClientRect().top}px)`
  elem3.style.transform = `translateY(${first3 - elem3.getBoundingClientRect().top}px)`

  // Force re-layout
  document.body.offsetHeight

  // Play
  elem0.classList.add("transition")
  elem1.classList.add("transition")
  elem2.classList.add("transition")
  elem3.classList.add("transition")
  elem0.style.transform = elem0.style.transitionDuration =
  elem1.style.transform = elem1.style.transitionDuration =
  elem2.style.transform = elem2.style.transitionDuration =
  elem3.style.transform = elem3.style.transitionDuration = ""
}

setTimeout(mix, 500)
</script>
```
</details>

I've reproduced this behavior explained above on each of the following platforms:

- Chrome 83.0.4103.116 on Windows 10 64-bit
- Safari 13.1 on iOS 13
- Firefox 78.0.2 on Windows 10 64-bit

Relevant framework bugs I've filed, before I narrowed it down further to this:

- https://github.com/MithrilJS/mithril.js/issues/2612
- https://github.com/facebook/react/issues/19406
- https://github.com/preactjs/preact/issues/2637
- https://github.com/infernojs/inferno/issues/1519

-----

I suspect it's a bug in all three of those listed browsers, but I'm filing an issue here in case it's actually a spec issue or if a spec note is necessary for this.

-- 
You are receiving this because you are subscribed to this thread.
Reply to this email directly or view it on GitHub:
https://github.com/whatwg/dom/issues/880

Received on Monday, 20 July 2020 07:46:29 UTC