Commit graph

1 commit

Author SHA1 Message Date
Léo
663d6879b7
fix(ui): stop the whirlpool spinner animating when it is never attached (#5990)
_drawWhirlpool re-armed requestAnimationFrame forever whenever its element
had never been connected to the document. The grace period is there so a
spinner can keep drawing between start() and the caller appending the
element, but it had no deadline: while the element has never been connected
_wpWasConnected stays false, so the guard stays true and the else branch is
unreachable. Any caller that starts a spinner and then takes an early return,
such as an aborted request or a panel that resolved from cache, leaves a loop
redrawing an 84-segment spiral into a detached canvas at one frame per
displayed frame until the tab closes.

Put a 2 second deadline on the grace period. Callers append in the same task
as start(), so that is far more slack than any of them need. A spinner that
is actually in the document is unaffected.

Two supporting changes in the same file:

- Both self-terminate paths now call stop() instead of setting isRunning
  directly, so termination always runs one cancelAnimationFrame and never
  depends solely on inferring DOM connectivity. Both draw functions bail at
  the top when they are no longer running, and _requestFrame() clears rafId
  as the callback enters so it is a truthful "a frame is pending" flag.
- start() arms a visibilitychange listener and stop() removes it. A hidden
  tab cancels the pending frame, a re-shown tab re-arms it. Chrome throttles
  background rAF but does not reliably stop the canvas work, and owning the
  listener from start/stop means a dead spinner never leaves one behind.

Adds tests/test_spinner_stops_when_never_attached_js.py, which drives the
real module under node with a fake clock and a manual frame pump. It covers
all four exits and, importantly, the converse: a spinner that is attached
keeps running well past the grace window.
2026-08-12 00:25:03 +01:00