Fix Models section collapse dead pause and missing animation

The collapse handler waited a fixed itemCount*25+230ms for the
section-domino-out keyframes, but the CSS rule only targeted .list-item.
#models-section uses .models-row, so the rule matched nothing: no
animation played and itemCount was 0, leaving a flat ~230ms pause before
the section snapped shut.

- CSS: the collapse/expand animation rules now match
  :is(.list-item, .models-row) so the Models rows actually animate.
- JS: drive the collapse off the real animations via getAnimations()
  instead of a hard-coded timeout. Wait only on the section-domino-out
  keyframes (ignoring unrelated/infinite animations); collapse
  immediately when nothing animates so there is never a dead pause. A
  generation token neutralizes stale callbacks from rapid toggles, with
  a 600ms safety net so a section can't get stuck open.
This commit is contained in:
Zeus-Deus
2026-06-01 16:53:46 +02:00
parent 70a71f603c
commit 21b40195b7
2 changed files with 60 additions and 38 deletions
+34 -12
View File
@@ -33,31 +33,53 @@ export function initSectionCollapse(Storage) {
Storage.setJSON('section-collapsed', state);
// Always clear any in-flight animation classes from a previous toggle
// so back-to-back clicks restart cleanly.
// so back-to-back clicks restart cleanly. Bump a generation token so
// any callback still pending from a superseded toggle becomes a no-op.
section.classList.remove('section-just-expanded', 'section-just-collapsing');
const gen = (section._collapseGen = (section._collapseGen || 0) + 1);
if (willCollapse) {
// Domino-out: play the fade/slide-down on .list-item children
// BEFORE actually adding .collapsed (which hides them via
// display:none). After the cascade finishes, lock in collapse.
// Force reflow so the keyframes restart.
// Domino-out: play the fade/slide-down on the row children BEFORE
// actually adding .collapsed (which hides them via display:none),
// then lock in collapse once the cascade finishes.
//
// We wait on the REAL animations (getAnimations) rather than a fixed
// timeout. Different sections animate different rows — .list-item in
// most, .models-row in #models-section — so any hard-coded duration
// either stalls with a dead pause (when the selector matches nothing,
// as it did for #models-section) or guesses the wrong length. Force a
// reflow first so the keyframes restart from the top.
// eslint-disable-next-line no-unused-expressions
section.offsetHeight;
section.classList.add('section-just-collapsing');
const itemCount = Math.min(12, section.querySelectorAll('.list-item').length);
const total = itemCount * 25 + 230; // matches CSS keyframes + stagger
setTimeout(() => {
const lockCollapsed = () => {
if (section._collapseGen !== gen) return; // superseded by a newer toggle
section.classList.remove('section-just-collapsing');
section.classList.add('collapsed');
}, total);
};
// Only the domino-out keyframes gate the collapse — ignore unrelated
// (and possibly infinite, e.g. spinners) animations in the subtree.
const dominoOut = section.getAnimations({ subtree: true })
.filter(a => a.animationName === 'section-domino-out');
if (dominoOut.length === 0) {
lockCollapsed(); // nothing to animate — collapse now, no dead pause
} else {
Promise.allSettled(dominoOut.map(a => a.finished)).then(lockCollapsed);
// Safety net: if an animation never settles (e.g. element removed),
// still lock in the collapse so the section can't get stuck open.
setTimeout(lockCollapsed, 600);
}
} else {
// Expand path — already had this: remove .collapsed and replay
// the inbound domino.
// Expand path — remove .collapsed and replay the inbound domino.
section.classList.remove('collapsed');
// eslint-disable-next-line no-unused-expressions
section.offsetHeight;
section.classList.add('section-just-expanded');
setTimeout(() => section.classList.remove('section-just-expanded'), 700);
setTimeout(() => {
if (section._collapseGen !== gen) return; // superseded by a newer toggle
section.classList.remove('section-just-expanded');
}, 700);
}
}