Skip to content

fix(accordion): grid-row open animation — no clipping on growth, popovers escape the box (ARTESCA-17819) - #1170

Merged
bert-e merged 4 commits into
development/1.0from
bugfix/accordion-clip-on-grow
Jul 28, 2026
Merged

bert-e merged 4 commits into
development/1.0from
bugfix/accordion-clip-on-grow

Conversation

@JeanMarcMilletScality

@JeanMarcMilletScality JeanMarcMilletScality commented Jul 24, 2026 •

Copy link
Copy Markdown
Contributor

TL;DR — Rewrites the Accordion open/collapse animation to a pure-CSS grid-row transition (0fr → 1fr) instead of a JS-measured pixel height. Content that grows or shrinks after opening is never clipped, and once open the box releases overflow so a Select menu, 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.scrollHeight in a ref callback). Two bugs fell out of that:

Bug Cause
Content that grows/shrinks after open is clipped the measured height goes stale on any reflow that fires no React render — a container resize, a @container responsive flip
A Select menu / tooltip opened near the bottom is cut off at the border overflow: hidden (needed to clip the height animation) also clips anything a child legitimately renders outside the box — ARTESCA-17819

This 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-rows from 0fr to 1fr; a 1fr track always resolves to the content's current height, so growth/shrink is followed instantly with no measurement.

Before:

<AccordionContent
  ref={(el) => {                                                    // ←
    el?.style.setProperty('height', isOpen ? el.scrollHeight + 'px' : '0px');
  }}
  $isOpen={isOpen}
>
  <Wrapper>{children}</Wrapper>
</AccordionContent>
// AccordionContent styled: overflow: hidden; transition: height 0.3s;   // ←

After:

<AccordionContent $isOpen={isOpen} onTransitionEnd={handleTransitionEnd}>
  <ContentClip $isExpanded={isOpen && isExpanded}>                  // ←
    <Wrapper>{children}</Wrapper>
  </ContentClip>
</AccordionContent>
// AccordionContent styled: display: grid; grid-template-rows: 0fr ↔ 1fr;  // ←
// ContentClip styled:      overflow: hidden → visible; min-height: 0;

No ResizeObserver, no scrollHeight, 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 ContentClip keeps overflow: hidden while the row animates, then flips to visible once the open transition finishes — gated by an isExpanded state set on the element's own grid-template-rows transitionEnd (e.target === e.currentTarget so a descendant's bubbled transition can't flip it early), initialized to open, and reset on close so the collapse animation clips again.

How to test

  • Storybook → Accordion → WithSelectAtBottom: open the Select near the bottom; its menu overflows past the accordion border and stays fully visible/scrollable.
  • Open an accordion whose content changes height after opening (e.g. a responsive Form that flips to a taller stacked layout as the frame narrows) — the box follows the content; nothing is clipped.
  • Jest — npx jest src/lib/components/accordion

References

@bert-e

bert-e commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Hello jeanmarcmilletscality,

My role is to assist you with the merge of this
pull request. Please type @bert-e help to get information
on this process, or consult the user documentation.

Available options
name description privileged authored
/after_pull_request Wait for the given pull request id to be merged before continuing with the current one.
/bypass_author_approval Bypass the pull request author's approval ⭐
/bypass_build_status Bypass the build and test status ⭐
/bypass_commit_size Bypass the check on the size of the changeset TBA ⭐
/bypass_incompatible_branch Bypass the check on the source branch prefix ⭐
/bypass_jira_check Bypass the Jira issue check ⭐
/bypass_peer_approval Bypass the pull request peers' approval ⭐
/bypass_leader_approval Bypass the pull request leaders' approval ⭐
/approve Instruct Bert-E that the author has approved the pull request. ✍️
/create_pull_requests Allow the creation of integration pull requests.
/create_integration_branches Allow the creation of integration branches.
/no_octopus Prevent Wall-E from doing any octopus merge and use multiple consecutive merge instead
/unanimity Change review acceptance criteria from one reviewer at least to all reviewers
/wait Instruct Bert-E not to run until further notice.
Available commands
name description privileged
/help Print Bert-E's manual in the pull request.
/status Print Bert-E's current status in the pull request TBA
/clear Remove all comments from Bert-E from the history TBA
/retry Re-start a fresh build TBA
/build Re-start a fresh build TBA
/force_reset Delete integration branches & pull requests, and restart merge process from the beginning.
/reset Try to remove integration branches unless there are commits on them which do not appear on the source branch.

Status report is not available.

@bert-e

bert-e commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Waiting for approval

The following approvals are needed before I can proceed with the merge:

  • the author

  • one peer

Peer approvals must include at least 1 approval from the following list:

) {
return;
}
const observer = new ResizeObserver(syncHeight);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Suggested change
const observer = new ResizeObserver(syncHeight);
const observer = new ResizeObserver(() => {
content.style.transition = 'none';
syncHeight();
content.offsetHeight;
content.style.transition = '';
});

@bert-e

bert-e commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Waiting for approval

The following approvals are needed before I can proceed with the merge:

  • the author

  • one peer

Peer approvals must include at least 1 approval from the following list:

JeanMarcMilletScality and others added 2 commits July 28, 2026 17:05
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>
@JeanMarcMilletScality
JeanMarcMilletScality force-pushed the bugfix/accordion-clip-on-grow branch from 57d8103 to c64d353 Compare July 28, 2026 15:16
… (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>
@JeanMarcMilletScality JeanMarcMilletScality changed the title fix(accordion): follow content height so growth is not clipped fix(accordion): grid-row open animation — no clipping on growth, popovers escape the box (ARTESCA-17819) Jul 28, 2026
Comment on lines +112 to +126
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);
}
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor Author

/approve

@bert-e

bert-e commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

In the queue

The changeset has received all authorizations and has been added to the
relevant queue(s). The queue(s) will be merged in the target development
branch(es) as soon as builds have passed.

The changeset will be merged in:

  • ✔️ development/1.0

There is no action required on your side. You will be notified here once
the changeset has been merged. In the unlikely event that the changeset
fails permanently on the queue, a member of the admin team will
contact you to help resolve the matter.

IMPORTANT

Please do not attempt to modify this pull request.

  • Any commit you add on the source branch will trigger a new cycle after the
    current queue is merged.
  • Any commit you add on one of the integration branches will be lost.

If you need this pull request to be removed from the queue, please contact a
member of the admin team now.

The following options are set: approve

@bert-e

bert-e commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

I have successfully merged the changeset of this pull request
into targetted development branches:

  • ✔️ development/1.0

Please check the status of the associated issue None.

Goodbye jeanmarcmilletscality.

@bert-e
bert-e merged commit ce9a1e1 into development/1.0 Jul 28, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants