diff --git a/packages/react/src/accordion/panel/AccordionPanel.tsx b/packages/react/src/accordion/panel/AccordionPanel.tsx index d506d511e9..7f485b8aa6 100644 --- a/packages/react/src/accordion/panel/AccordionPanel.tsx +++ b/packages/react/src/accordion/panel/AccordionPanel.tsx @@ -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. @@ -81,6 +82,8 @@ const AccordionPanel = React.forwardRef(function AccordionPanel( setKeepMounted(keepMounted); }, [setKeepMounted, keepMounted]); + usePanelResize(panelRef, setDimensions, open); + const { getRootProps } = useCollapsiblePanel({ abortControllerRef, animationTypeRef, @@ -111,6 +114,7 @@ const AccordionPanel = React.forwardRef(function AccordionPanel( render: render ?? 'div', state, className, + ref: [forwardedRef, panelRef], extraProps: { ...otherProps, 'aria-labelledby': triggerId, @@ -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 { diff --git a/packages/react/src/collapsible/panel/CollapsiblePanel.tsx b/packages/react/src/collapsible/panel/CollapsiblePanel.tsx index 56e8a70955..5e863fda2e 100644 --- a/packages/react/src/collapsible/panel/CollapsiblePanel.tsx +++ b/packages/react/src/collapsible/panel/CollapsiblePanel.tsx @@ -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. @@ -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: { @@ -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 }; diff --git a/packages/react/src/utils/usePanelResize.ts b/packages/react/src/utils/usePanelResize.ts new file mode 100644 index 0000000000..86022da4e5 --- /dev/null +++ b/packages/react/src/utils/usePanelResize.ts @@ -0,0 +1,68 @@ +'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, + setDimensions: React.Dispatch>, + open: boolean, +) { + React.useEffect(() => { + const panel = panelRef.current; + if (!panel || !open || typeof ResizeObserver === 'undefined') { + return undefined; + } + + function recalculateSize() { + if (!panel) { + 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; + }); + } + + const observer = new ResizeObserver(() => { + if (panel.getAnimations().length > 0) { + return; + } + recalculateSize(); + }); + + function handleWindowResize() { + recalculateSize(); + } + + const win = ownerWindow(panel); + win.addEventListener('resize', handleWindowResize); + observer.observe(panel); + return () => { + observer.disconnect(); + win.removeEventListener('resize', handleWindowResize); + }; + }, [panelRef, setDimensions, open]); +}