Skip to content

[Collapsible][Accordion] Recalculate panel dimensions on layout resize #1704

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 16, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions packages/react/src/accordion/panel/AccordionPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type { AccordionItem } from '../item/AccordionItem';
import { useAccordionItemContext } from '../item/AccordionItemContext';
import { accordionStyleHookMapping } from '../item/styleHooks';
import { AccordionPanelCssVars } from './AccordionPanelCssVars';
import { usePanelResize } from '../../utils/usePanelResize';

/**
* A collapsible panel with the accordion item contents.
Expand Down Expand Up @@ -81,6 +82,8 @@ const AccordionPanel = React.forwardRef(function AccordionPanel(
setKeepMounted(keepMounted);
}, [setKeepMounted, keepMounted]);

usePanelResize(panelRef, setDimensions, open);

const { getRootProps } = useCollapsiblePanel({
abortControllerRef,
animationTypeRef,
Expand Down Expand Up @@ -111,6 +114,7 @@ const AccordionPanel = React.forwardRef(function AccordionPanel(
render: render ?? 'div',
state,
className,
ref: [forwardedRef, panelRef],
extraProps: {
...otherProps,
'aria-labelledby': triggerId,
Expand All @@ -124,11 +128,12 @@ const AccordionPanel = React.forwardRef(function AccordionPanel(
customStyleHookMapping: accordionStyleHookMapping,
});

if (keepMounted || hiddenUntilFound || (!keepMounted && mounted)) {
return renderElement();
const shouldRender = keepMounted || hiddenUntilFound || (!keepMounted && mounted);
if (!shouldRender) {
return null;
}

return null;
return renderElement();
});

namespace AccordionPanel {
Expand Down
11 changes: 8 additions & 3 deletions packages/react/src/collapsible/panel/CollapsiblePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { CollapsibleRoot } from '../root/CollapsibleRoot';
import { collapsibleStyleHookMapping } from '../root/styleHooks';
import { useCollapsiblePanel } from './useCollapsiblePanel';
import { CollapsiblePanelCssVars } from './CollapsiblePanelCssVars';
import { usePanelResize } from '../../utils/usePanelResize';

/**
* A panel with the collapsible contents.
Expand Down Expand Up @@ -98,11 +99,14 @@ const CollapsiblePanel = React.forwardRef(function CollapsiblePanel(
width,
});

usePanelResize(panelRef, setDimensions, open);

const { renderElement } = useComponentRenderer({
propGetter: getRootProps,
render: render ?? 'div',
state,
className,
ref: [forwardedRef, panelRef],
extraProps: {
...otherProps,
style: {
Expand All @@ -114,11 +118,12 @@ const CollapsiblePanel = React.forwardRef(function CollapsiblePanel(
customStyleHookMapping: collapsibleStyleHookMapping,
});

if (keepMounted || hiddenUntilFound || (!keepMounted && mounted)) {
return renderElement();
const shouldRender = keepMounted || hiddenUntilFound || (!keepMounted && mounted);
if (!shouldRender) {
return null;
}

return null;
return renderElement();
});

export { CollapsiblePanel };
Expand Down
70 changes: 70 additions & 0 deletions packages/react/src/utils/usePanelResize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
'use client';
import * as React from 'react';
import { ownerWindow } from './owner';
import { Dimensions } from '../collapsible/root/useCollapsibleRoot';

function setAutoSize(panel: HTMLElement) {
const originalHeight = panel.style.height;
const originalWidth = panel.style.width;
panel.style.height = 'auto';
panel.style.width = 'auto';
return () => {
panel.style.height = originalHeight;
panel.style.width = originalWidth;
};
}

/**
* Ensures the panel will expand to the correct height when the window is resized.
* This prevents content from being cut off or the panel not fitting to the content.
*/
export function usePanelResize(
panelRef: React.RefObject<HTMLElement | null>,
setDimensions: React.Dispatch<React.SetStateAction<Dimensions>>,
open: boolean,
) {
React.useEffect(() => {
const panel = panelRef.current;
if (!panel || !open || typeof ResizeObserver === 'undefined') {
return undefined;
}

const observer = new ResizeObserver(() => {
if (panel.getAnimations().length > 0) {
return;
}

const cleanup = setAutoSize(panel);
const scrollHeight = panel.scrollHeight;
const scrollWidth = panel.scrollWidth;
cleanup();

setDimensions((prev) => {
if (prev.height !== scrollHeight || prev.width !== scrollWidth) {
return { height: scrollHeight, width: scrollWidth };
}
return prev;
});
});

let frame = -1;

function handleWindowResize() {
// Avoid size transitions when the window is resized.
if (panel) {
cancelAnimationFrame(frame);
const cleanup = setAutoSize(panel);
frame = requestAnimationFrame(cleanup);
}
}

const win = ownerWindow(panel);
win.addEventListener('resize', handleWindowResize);
Copy link
Member

Choose a reason for hiding this comment

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

Testing this a bit and it works well, just wondering is it not enough to only use ResizeObserver?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In this case the ResizeObserver fires when transitioning the height, which is why I added if (panel.getAnimations().length > 0) { to prevent its logic from running, but when resizing the window, I don't want any animations to occur so the resizing is 1:1 with layout.

For this purpose we can't reuse the ResizeObserver since we want regular transitions to work when opening/closing

observer.observe(panel);
return () => {
observer.disconnect();
win.removeEventListener('resize', handleWindowResize);
cancelAnimationFrame(frame);
};
}, [panelRef, setDimensions, open]);
}