From 06e09e8285e582896da9777dea79501694fa2fa3 Mon Sep 17 00:00:00 2001 From: Keith Solomon Date: Sat, 25 Jul 2026 15:50:59 -0500 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9E=20fix:=20Intercept=20summary=20cli?= =?UTF-8?q?ck=20to=20animate=20both=20directions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- views/blocks/team-grid/team-grid.js | 92 +++++++++++++++++++++-------- 1 file changed, 67 insertions(+), 25 deletions(-) diff --git a/views/blocks/team-grid/team-grid.js b/views/blocks/team-grid/team-grid.js index 3fac1a2..306ac71 100644 --- a/views/blocks/team-grid/team-grid.js +++ b/views/blocks/team-grid/team-grid.js @@ -1,27 +1,33 @@ /** * Team Grid block — bio expand/collapse animation. * - * Native
/ handles the toggle, but CSS transitions on - * max-height need a concrete target value to animate to. This module - * measures each bio's natural height on open and animates max-height - * from current → measured (open) or current → 0 (close) so the - * transition fires in both directions. + * 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 toggle handler to each. + * .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. + // 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; @@ -36,7 +42,7 @@ const measureBioHeight = (bioWrap) => { return height; }; -const animateOpen = (bioWrap) => { +const animateOpen = (bioWrap, onComplete) => { const target = measureBioHeight(bioWrap); setMaxHeight(bioWrap, '0px'); @@ -50,17 +56,21 @@ const animateOpen = (bioWrap) => { setMaxHeight(bioWrap, `${target}px`); }); - // After the transition completes, leave max-height at the measured - // pixel value. Resetting to 'none' breaks the close animation: the - // browser treats 'none' as a discrete keyword value and won't - // transition from it to a length. The pixel value gives the close - // animation a clean from-state to interpolate from. + 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) => { - // Capture the current rendered height so the browser has a concrete - // starting value for the transition. Without this, going from - // 'none' to '0px' snaps instead of animating. +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`); @@ -72,6 +82,40 @@ const animateClose = (bioWrap) => { 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) => { @@ -81,17 +125,15 @@ const attach = (details) => { details.dataset.teamGridBound = 'true'; const bioWrap = details.querySelector(BIO_WRAP_SELECTOR); - if (!bioWrap) { return; } - details.addEventListener('toggle', () => { - if (details.open) { - animateOpen(bioWrap); - } else { - animateClose(bioWrap); - } + // 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); }); };