Move animation glitches in keyed lists

Components with a move hook (componentWillMove, onComponentWillMove, used by inferno-animation) prepare keyed lists before their layout changes. Each section checks a previous glitch. Animated cards are orange, plain cards are grey. The status line compares what happened with what should happen.

1. Animated and plain items move into the correct order

Only card 3 has a move hook. Deferring its DOM move used to place card 4 incorrectly. All DOM moves now follow the same reconciliation order. The list should read 0 1 4 3 2.

2. Animations measure the list before it changes

All cards are animated. Moving D to the top and adding X must measure the original A B C D layout: A, B and C should slide two rows without jumping. Insertion-only and removal-only updates should also animate the retained cards, even though their relative order is unchanged.

3. Move hooks work through nested components

The callback must use the type and props of the component that owns the hook. Reversing either list should animate to C B A without throwing a TypeError.

Class component rendering a function component with onComponentWillMove

Function component rendering a class component with componentWillMove

4. Composed components animate like direct elements

Both lists use AnimatedMoveComponent. On the left it renders an element, on the right it renders a function component that renders the same element. Both lists should animate and receive the same number of preparation callbacks.

Renders <li>

Renders <Card>

Edge cases to verify manually

Sections 5–15 use AnimatedMoveComponent and AnimatedAllComponent with 1.5 s moves, enters and leaves. Each section says what should happen and what used to go wrong. The status line measures the first frame of the animation or its end result, so wait for the previous animation to finish before pressing a button again. A red status means that the glitch reproduced.

5. Moves in a transformed coordinate space

A move measures boxes in viewport pixels and applies the offset as a CSS translate in the item's own coordinate space, so it converts the offset into that space. Cards should start exactly where they were and slide to their new places. Before the conversion the cards started at the wrong distance and jumped at the start of the move: 40 % short inside a transform: scale(0.6) ancestor, 40 % short with scale: 0.6 on the cards themselves (the individual transform properties apply before transform), and twice too far in an SVG whose viewBox scales its content by 2. The cards of the last list are children of a shadow root, whose host is scaled: a list in a shadow root used to throw when it moved, which stopped every animation of that update. CSS zoom and 3D transforms are not taken into account.

List inside transform: scale(0.6)

Cards with scale: 0.6

SVG <g> items, viewBox at half size

Shadow root in a host with scale(0.6)

6. An item that becomes visible in the same update as a reorder

Card C starts with display: none. The button moves E to the top and shows C in the same update. C should simply appear in its place (between B and D) while the others slide. A hidden element has an empty box at the viewport origin: C used to be treated as moved from the top-left corner of the window and flew in from there.

7. An item running a CSS keyframe animation on transform

Card B pulses with a CSS @keyframes animation that sets transform, and card D has popped in with one that has finished and keeps its last keyframe (animation-fill-mode: forwards). Moving E to the top should slide B and D down one row like A and C: they move with the translate property. A keyframe animation overrides an inline transform, so B and D used to jump to their new places while their neighbours slid.

8. Removing an item while it moves

The buttons move E to the top and remove cards 0.6 s later, halfway through the moves. The cards that leave should stay where they are on screen and leave from there. The leave used to finish the move first, so E jumped to the top of the list and collapsed there. Removing the whole list or replacing every card did the same to all cards. With the first button E slides out to the right while it fades and collapses: its leave animates transform, which keeping E in place must not stop, so the position is kept in translate. With the other buttons the cards fade and collapse in place.

9. Removing an item while it enters

The button adds X and removes it 0.6 s later, before its enter animation has finished. X should shrink and fade out from its current size and opacity over the 1.5 s leave. The enter transitions that the leave interrupts fire transitioncancel, which the leave used to count as its own transitions ending, so X was removed after a few frames. When X is removed in the next frame, before its enter has started, nothing of it has been visible and it should be removed at once: it used to show at full opacity and fade out.

10. Nested lists reordered in one update

The groups and their cards have move hooks. The buttons reverse the groups and the cards inside them in one update. Everything should start where it was: the groups slide past each other and the cards slide within them. Each card's offset contains its group's movement, which the group's own transform adds again: the cards used to start far outside their groups and fly in. When the first group is removed in the same update, the cards are measured after the groups have moved up into its place: they used to start that much too high. On the right the groups have no box of their own. G1 moves two rows down and its card c two rows up, so c should not move at all: it used to start two rows higher and slide back.

11. A displaced item that starts a CSS transition in the same update

The cards have transition: background-color 1s. The button moves E to the top and highlights B in the same update. B is displaced by one row without being moved in the DOM. The readme documents that elements running an author transition are excluded from moves, so B jumps down while A, C and D slide. Check whether that is acceptable: selecting an item and re-sorting the list at once is a common pattern. (A card that is moved in the DOM does not start the transition, so it slides normally.)

12. Inline width and height of an entering element

Each added box is rendered with style="width: 160px; height: 56px". It should appear and keep that size. The enter animation writes its own inline width and height, and used to remove them when it ended, together with the ones the application rendered. The box snapped to the stylesheet size (full width, 36 px) at the end of the enter, and Inferno does not restore the style until the prop changes. A box that is removed should keep its size while it fades out: the leave used to remove the inline size as well, so the box snapped to the full width when its leave started. The inline height takes precedence over the animation classes, so these boxes do not grow or collapse.

13. A global animation source used much later

The logo has globalAnimationKey. Move in one update removes it on the left and shows it on the right in the same task: the logo should fly from left to right (the intended global animation). Remove and, more than a second later, Show on the right should be a plain enter, because the source expires after one second. Within that second the logo should still fly, which allows for a page that loads before it mounts. The expiry used to never start, so the logo still flew from where it was removed, however long ago, and unused sources stayed in memory.

14. The inline transform of an element that enters from a global source

The tile moves from the left pane to the right one with globalAnimationKey. On the right it is rendered with style="transform: rotate(-12deg)". It should fly over and end up tilted. The global enter writes its own transform and transform-origin, and used to clear both when it activated, together with the ones the application rendered, so the tile flew in untilted and stayed that way.

15. Random updates

Inserts, removes and moves cards at the chosen interval, sometimes twice in one task. Watch for cards that jump, flicker, overlap or leave gaps. Stop and check waits for the animations to finish and compares the DOM with the expected order, and reports cards left with inline styles or animation classes.