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) <noreply@anthropic.com>
This commit is contained in:
2026-09-11 23:23:40 +02:00
parent a7a6d205e1
commit 3c899d971b
2 changed files with 14 additions and 16 deletions

View File

@@ -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');

View File

@@ -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 headers 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();
});