fix(accordion): grid-row open animation — no clipping on growth, popovers escape the box (ARTESCA-17819) - #1170
Conversation
Hello jeanmarcmilletscality,My role is to assist you with the merge of this Available options
Available commands
Status report is not available. |
Waiting for approvalThe following approvals are needed before I can proceed with the merge:
Peer approvals must include at least 1 approval from the following list: |
| ) { | ||
| return; | ||
| } | ||
| const observer = new ResizeObserver(syncHeight); |
There was a problem hiding this comment.
The CSS transition: height 0.3s ease-in on AccordionContent still applies when the ResizeObserver updates the height. Combined with overflow: hidden, growing content is clipped for ~300ms — the same symptom the PR fixes, just transient instead of permanent. Disable the transition inside the observer callback so resize-while-open is instantaneous while the open/close animation is preserved:
| const observer = new ResizeObserver(syncHeight); | |
| const observer = new ResizeObserver(() => { | |
| content.style.transition = 'none'; | |
| syncHeight(); | |
| content.offsetHeight; | |
| content.style.transition = ''; | |
| }); |
Waiting for approvalThe following approvals are needed before I can proceed with the merge:
Peer approvals must include at least 1 approval from the following list: |
The AccordionContent froze its height once on open, so content that grew afterwards (e.g. a responsive Form flipping its fields to a stacked column) was clipped by overflow:hidden and became invisible. Track the wrapper with a ResizeObserver while open and sync the height on every change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…red height (CUI) Replace the render-time/ResizeObserver height measurement with a pure-CSS grid-template-rows 0fr->1fr transition. A 1fr track always resolves to the content's current height, so content that grows or shrinks while open follows instantly and is never clipped -- the bug this branch targets -- without any JS measurement, refs, layout effect, or observer. Also drop two pre-existing smells while here: - remove the dead onKeyDown handler (referenced handleToggleContent but never called it; the native <button> already toggles on Enter/Space); - replace the useMemo-as-side-effect prop->state sync with the documented adjust-state-during-render pattern. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
57d8103 to
c64d353
Compare
… (ARTESCA-17819) Folds in the fix from #1171. While the grid row animates (open or collapse) the content stays clipped; once the open transition settles, overflow flips to visible so a Select menu, dropdown or tooltip near the bottom is no longer cut off at the accordion border. Adds the WithSelectAtBottom story. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| const handleToggleContent = () => { | ||
| setIsOpen((prev) => !prev); | ||
| }; | ||
|
|
||
| const handleTransitionEnd = (e: React.TransitionEvent<HTMLDivElement>) => { | ||
| // Only react to this element's own row transition finishing, not a | ||
| // transition bubbling up from a descendant. | ||
| if ( | ||
| e.target === e.currentTarget && | ||
| e.propertyName === 'grid-template-rows' && | ||
| isOpen | ||
| ) { | ||
| setIsExpanded(true); | ||
| } | ||
| }; |
There was a problem hiding this comment.
Here your Claude assumes react 19 memoization compiler. For better performance memoize theses.
I personaly created a skill for my Claude to not forget that but you may enforce it however you want:
---
name: react-memoization
description: >-
Default-memoize convention for React/TSX code. When writing or editing React components —
adding event handlers, passing props, building dependency arrays, or deriving values —
wrap callbacks in useCallback and non-primitive/derived values in useMemo by default
rather than leaving them inline. Use whenever authoring or modifying React component code.
---
# React: memoize by default
Assume the project does **NOT** use the React Compiler unless you've verified it does
(check `package.json` / babel / build config for `babel-plugin-react-compiler`). When it's
absent, memoization is manual and expected — do not skip it on the assumption a
compiler/runtime handles it or that the cost is negligible. If the React Compiler *is*
enabled, this skill does not apply: prefer plain inline code and let the compiler memoize.
## Always memoize
- Every callback / event handler passed as a prop, used in JSX, or listed in another hook's
dependency array → wrap in `useCallback`.
- Every non-primitive value (object literal, array literal) passed as a prop or used in a
dependency array → wrap in `useMemo`.
- Derived values from non-trivial computation (sort / map / filter / reduce, parsing,
formatting) → wrap in `useMemo`.
## Don't memoize
- Primitive literals (string / number / boolean) passed as props — pass them raw.
- Values read only once that never cross a prop or dependency-array boundary.
- Already-stable references (`navigate`, `useState` setters, react-query `mutate`) don't
need wrapping, but still belong in the dependency array.
## Keep deps honest
Dependency arrays must be exhaustive and correct. Match the project's linter
(e.g. Biome `useExhaustiveDependencies`, or ESLint `react-hooks/exhaustive-deps`).
## Example
// ✗ new function + new object every render
<Chip onRemove={() => remove(item.id)} style={{ marginLeft: 4 }} />
// ✓
const handleRemove = useCallback(() => remove(item.id), [remove, item.id]);
const chipStyle = useMemo(() => ({ marginLeft: 4 }), []);
<Chip onRemove={handleRemove} style={chipStyle} />
The project does not use the React Compiler, so callbacks passed to JSX are memoized manually (per reviewer feedback on #1170). Wrap handleToggleContent (stable, functional setState) and handleTransitionEnd (depends on isOpen) in useCallback. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
/approve |
In the queueThe changeset has received all authorizations and has been added to the The changeset will be merged in:
There is no action required on your side. You will be notified here once IMPORTANT Please do not attempt to modify this pull request.
If you need this pull request to be removed from the queue, please contact a The following options are set: approve |
|
I have successfully merged the changeset of this pull request
Please check the status of the associated issue None. Goodbye jeanmarcmilletscality. |
TL;DR — Rewrites the
Accordionopen/collapse animation to a pure-CSS grid-row transition (0fr → 1fr) instead of a JS-measured pixelheight. Content that grows or shrinks after opening is never clipped, and once open the box releasesoverflowso aSelectmenu, dropdown or tooltip can escape the border.Context
The Accordion animated open by transitioning an explicit measured pixel
height, measured once at render time (element.scrollHeightin a ref callback). Two bugs fell out of that:@containerresponsive flipSelectmenu / tooltip opened near the bottom is cut off at the borderoverflow: hidden(needed to clip the height animation) also clips anything a child legitimately renders outside the box — ARTESCA-17819This PR absorbs and closes #1171, which fixed only the second bug.
Approach
Animate a grid row, not a pixel height. A single-row grid transitioning
grid-template-rowsfrom0frto1fr; a1frtrack always resolves to the content's current height, so growth/shrink is followed instantly with no measurement.Before:
After:
No
ResizeObserver, noscrollHeight, no refs — the whole measurement path is deleted. (Grid-row transitions need Chrome 107 / Firefox 66 / Safari 16, all 2022+.)Release overflow once settled (ARTESCA-17819). The inner
ContentClipkeepsoverflow: hiddenwhile the row animates, then flips tovisibleonce the open transition finishes — gated by anisExpandedstate set on the element's owngrid-template-rowstransitionEnd(e.target === e.currentTargetso a descendant's bubbled transition can't flip it early), initialized toopen, and reset on close so the collapse animation clips again.How to test
WithSelectAtBottom: open theSelectnear the bottom; its menu overflows past the accordion border and stays fully visible/scrollable.responsiveForm that flips to a taller stacked layout as the frame narrows) — the box follows the content; nothing is clipped.npx jest src/lib/components/accordionReferences
Selectmenu opened inside an Accordion is clipped by the box border.