From ff2a46885188a41493060c49b4af9be743853378 Mon Sep 17 00:00:00 2001 From: Kaleb Sitton Date: Tue, 30 Jun 2026 18:40:58 -0500 Subject: [PATCH] feat(ui): animate collapse panels in the new UI Collapse panels in the new UI (finding-detail sections and other data-toggle="collapse" panels) toggled open/closed instantly. Add a smooth height slide, consistent with the filter accordion. - index.js: the shared collapse shim now animates the target height and vertical padding between 0 and its natural size over 250ms, settling to auto (open) or display:none via .in (closed). Respects prefers-reduced-motion (instant toggle), preserves aria-expanded, guards against re-clicks mid-animation, and handles both border-box and content-box. One change covers every collapse panel in the new UI. - base.html: cache-bust index.js with ?v={% static_v %} (matching how the CSS is loaded) so the JS change reaches users; first-party JS was not versioned before. No Bootstrap is reintroduced: this enhances the new UI vanilla-JS shim that already replaced Bootstrap collapse. --- dojo/static/dojo/js/index.js | 66 +++++++++++++++++++++++++++++++++--- dojo/templates/base.html | 2 +- 2 files changed, 63 insertions(+), 5 deletions(-) diff --git a/dojo/static/dojo/js/index.js b/dojo/static/dojo/js/index.js index d9986d7b975..4629b5af2d9 100644 --- a/dojo/static/dojo/js/index.js +++ b/dojo/static/dojo/js/index.js @@ -69,6 +69,67 @@ Handles [data-toggle="collapse"] by toggling .in on the target element. CSS in tailwind.css: .collapse { display:none } .collapse.in { display:block } */ +/* Animate a collapse target open/closed by sliding its height between 0 and + its natural size, then settling to auto (open) or display:none via .in + (closed). Falls back to an instant toggle when reduced motion is requested. */ +function animateCollapse(target, toggle) { + if (target._ddCollapsing) return; // ignore clicks while mid-animation + var willOpen = !target.classList.contains('in'); + if (toggle) toggle.setAttribute('aria-expanded', willOpen ? 'true' : 'false'); + + if (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) { + target.classList.toggle('in', willOpen); + return; + } + + var DURATION = 250; + var finished = false; + target._ddCollapsing = true; + + if (willOpen) target.classList.add('in'); // make it measurable / visible + + // Animate height AND vertical padding together, so the panel collapses all + // the way to 0 instead of stalling at the panel-body's padding "floor". + var cs = getComputedStyle(target); + var padTop = cs.paddingTop, padBottom = cs.paddingBottom; + // In border-box (Tailwind's default) the height property already includes + // padding, so animate to the full scrollHeight; only subtract in content-box. + var fullH = cs.boxSizing === 'border-box' + ? target.scrollHeight + : target.scrollHeight - parseFloat(padTop) - parseFloat(padBottom); + var contentH = fullH + 'px'; + var collapsed = { height: '0px', paddingTop: '0px', paddingBottom: '0px' }; + var expanded = { height: contentH, paddingTop: padTop, paddingBottom: padBottom }; + var from = willOpen ? collapsed : expanded; + var to = willOpen ? expanded : collapsed; + + target.style.overflow = 'hidden'; + target.style.height = from.height; + target.style.paddingTop = from.paddingTop; + target.style.paddingBottom = from.paddingBottom; + void target.offsetHeight; // force reflow so the transition runs + target.style.transition = 'height ' + DURATION + 'ms ease, padding ' + DURATION + 'ms ease'; + target.style.height = to.height; + target.style.paddingTop = to.paddingTop; + target.style.paddingBottom = to.paddingBottom; + + function done() { + if (finished) return; + finished = true; + if (!willOpen) target.classList.remove('in'); // hide before clearing styles + target.style.transition = ''; + target.style.height = ''; + target.style.paddingTop = ''; + target.style.paddingBottom = ''; + target.style.overflow = ''; + target._ddCollapsing = false; + target.removeEventListener('transitionend', onEnd); + } + function onEnd(ev) { if (ev.target === target && ev.propertyName === 'height') done(); } + target.addEventListener('transitionend', onEnd); + setTimeout(done, DURATION + 80); // fallback if transitionend doesn't fire +} + document.addEventListener('click', function (e) { var toggle = e.target.closest('[data-toggle="collapse"]'); if (!toggle) return; @@ -86,10 +147,7 @@ document.addEventListener('click', function (e) { if (!sel) return; var target = document.querySelector(sel); if (target) { - target.classList.toggle('in'); - // Toggle aria-expanded on the trigger - var isOpen = target.classList.contains('in'); - toggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false'); + animateCollapse(target, toggle); } }); }); diff --git a/dojo/templates/base.html b/dojo/templates/base.html index 1929f8370f8..2c455e8416e 100644 --- a/dojo/templates/base.html +++ b/dojo/templates/base.html @@ -949,7 +949,7 @@ - +