/** * Team Grid block — bio expand/collapse animation. * * Native
/ handles accessibility (keyboard, screen * reader), but its default toggle behavior hides the children of the * details element synchronously when [open] is removed. That makes * CSS transitions impossible on the close direction, because by the * time the toggle event fires, the element is already hidden. * * This module intercepts the click on the summary, prevents the default * toggle, animates max-height manually, and toggles [open] after the * animation completes. The bio is always at a known state during the * animation. * * The module is enqueued per-block by the team-grid PHP template. It * auto-initializes on import: queries the DOM for any existing * .team-grid__details elements and attaches a click handler to each. */ const DETAILS_SELECTOR = '.team-grid__details'; const BIO_WRAP_SELECTOR = '.team-grid__bio-wrap'; const DURATION_MS = 300; const setMaxHeight = (el, value) => { el.style.maxHeight = typeof value === 'number' ? `${value}px` : value; }; const measureBioHeight = (bioWrap) => { // Temporarily allow the inner content to render at its natural size // so scrollHeight reports the real height rather than the clipped height. const previousMax = bioWrap.style.maxHeight; const previousOverflow = bioWrap.style.overflow; setMaxHeight(bioWrap, 'none'); bioWrap.style.overflow = 'visible'; const height = bioWrap.scrollHeight; bioWrap.style.overflow = previousOverflow; setMaxHeight(bioWrap, previousMax || ''); return height; }; const animateOpen = (bioWrap, onComplete) => { const target = measureBioHeight(bioWrap); setMaxHeight(bioWrap, '0px'); // Force a reflow so the browser registers the starting value before // we change it to the target — otherwise the transition is skipped. // eslint-disable-next-line no-unused-expressions bioWrap.offsetHeight; requestAnimationFrame(() => { setMaxHeight(bioWrap, `${target}px`); }); const onEnd = (event) => { if (event.propertyName !== 'max-height' || event.target !== bioWrap) { return; } bioWrap.removeEventListener('transitionend', onEnd); if (onComplete) { onComplete(); } }; bioWrap.addEventListener('transitionend', onEnd); }; const animateClose = (bioWrap, onComplete) => { // The bio is currently open and rendered at its natural height. // Capture it before we change anything. const current = bioWrap.getBoundingClientRect().height; setMaxHeight(bioWrap, `${current}px`); // Force a reflow so the browser registers the starting value. // eslint-disable-next-line no-unused-expressions bioWrap.offsetHeight; requestAnimationFrame(() => { setMaxHeight(bioWrap, '0px'); }); const onEnd = (event) => { if (event.propertyName !== 'max-height' || event.target !== bioWrap) { return; } bioWrap.removeEventListener('transitionend', onEnd); if (onComplete) { onComplete(); } }; bioWrap.addEventListener('transitionend', onEnd); }; const handleClick = (details, bioWrap, event) => { // Only intercept clicks on the summary (or its descendants). const summary = details.querySelector('summary'); if (!summary || !summary.contains(event.target)) { return; } // Prevent the browser's default toggle so we can animate first. event.preventDefault(); if (details.open) { // Closing: animate to 0, then remove [open] so the children hide. animateClose(bioWrap, () => { details.open = false; }); } else { // Opening: set [open] first (so the children are visible), then // measure and animate to the natural height. details.open = true; animateOpen(bioWrap); } }; const attach = (details) => { if (details.dataset.teamGridBound === 'true') { return; } details.dataset.teamGridBound = 'true'; const bioWrap = details.querySelector(BIO_WRAP_SELECTOR); if (!bioWrap) { return; } // Use click (not toggle) so we can preventDefault. Keyboard activation // (Enter/Space on the summary) also dispatches a click event, so this // covers both mouse and keyboard. details.addEventListener('click', (event) => { handleClick(details, bioWrap, event); }); }; const init = () => { document.querySelectorAll(DETAILS_SELECTOR).forEach(attach); }; init();