From 3c899d971b7ddd4d5def1f0931a2ce9ebe84f721 Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Fri, 11 Sep 2026 23:23:40 +0200 Subject: [PATCH] fix(fullscreen): stop the recalc loop once the header stops animating recalcWhileCollapsing ran a requestAnimationFrame loop and cancelled it on the header's max-height transitionend. When no max-height transition ran, for example because the header was already in its target state, that event never fired and the loop recalculated the scroll container on every frame for the rest of the page's life; each further call started another such loop. The loop now continues only while header.getAnimations() reports a running animation. The resize handler entered or exited fullscreen on every resize event. It now returns early when the UI fullscreen state already matches the body class, so a resize that changes nothing starts no recalc loop. A Cypress spec spies on adjustScrollContainerHeight after exitFullscreen() and requires the call count to stop growing. Co-Authored-By: Claude Opus 5 (1M context) --- app/cypress/e2e/fullscreen.spec.js | 12 ++++++++++++ app/static/js/fullscreen.js | 18 ++---------------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/app/cypress/e2e/fullscreen.spec.js b/app/cypress/e2e/fullscreen.spec.js index e6626a0..55c2dca 100644 --- a/app/cypress/e2e/fullscreen.spec.js +++ b/app/cypress/e2e/fullscreen.spec.js @@ -71,6 +71,18 @@ describe('Fullscreen Toggle', () => { }); }); + it('stops recalculating once the header has nothing left to animate', () => { + cy.window().then(win => { + cy.spy(win, 'adjustScrollContainerHeight').as('recalc'); + win.exitFullscreen(); + }); + cy.wait(500); + cy.get('@recalc').then(spy => { + const settled = spy.callCount; + cy.wait(500).then(() => expect(spy.callCount).to.eq(settled)); + }); + }); + it('toggleFullscreen() toggles into and out of fullscreen', () => { // Toggle into fullscreen cy.window().invoke('toggleFullscreen'); diff --git a/app/static/js/fullscreen.js b/app/static/js/fullscreen.js index ac6625a..3cda091 100644 --- a/app/static/js/fullscreen.js +++ b/app/static/js/fullscreen.js @@ -9,31 +9,16 @@ function updateUrlFullscreen(enabled) { window.history.replaceState({}, '', url); } -/** - * Starts a requestAnimationFrame loop that calls your recalc methods, - * and stops automatically when the header’s max-height transition ends. - */ function recalcWhileCollapsing() { const header = document.querySelector('header'); if (!header) return; - // 1) Start the RAF loop - let rafId; const step = () => { adjustScrollContainerHeight(); updateCustomScrollbar(); - rafId = requestAnimationFrame(step); + if (header.getAnimations().length > 0) requestAnimationFrame(step); }; step(); - - // 2) Listen for the end of the max-height transition - function onEnd(e) { - if (e.propertyName === 'max-height') { - cancelAnimationFrame(rafId); - header.removeEventListener('transitionend', onEnd); - } - } - header.addEventListener('transitionend', onEnd); } function enterFullscreen() { @@ -100,6 +85,7 @@ document.addEventListener('fullscreenchange', function() { }); window.addEventListener('resize', function() { var isUiFs = Math.abs(window.innerHeight - screen.height) < 2; + if (isUiFs === document.body.classList.contains('fullscreen')) return; if (isUiFs) enterFullscreen(); else exitFullscreen(); });