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 @@