diff --git a/packages/ui/src/components/Toast/Toast.tsx b/packages/ui/src/components/Toast/Toast.tsx index 9e738cc0fc..9736ccd71c 100644 --- a/packages/ui/src/components/Toast/Toast.tsx +++ b/packages/ui/src/components/Toast/Toast.tsx @@ -21,6 +21,7 @@ import { ReactElement, useRef, useEffect, + useState, } from 'react'; import { useToast } from '@react-aria/toast'; import { useButton } from 'react-aria'; @@ -114,29 +115,26 @@ export const Toast = forwardRef( 'aria-setsize': toastProps['aria-setsize'], }; - // Measure and report this toast's natural height + // Track whether we've measured this toast's natural height + const [hasMeasured, setHasMeasured] = useState(false); + + // Measure this toast's natural height on mount (before constraints are applied) useEffect(() => { if (!onHeightChange) return; + if (hasMeasured) return; // Only measure once const element = toastRef.current; if (!element) return; - // Report initial height - onHeightChange(toast.key, element.offsetHeight); - - // Watch for size changes (content could change) - const resizeObserver = new ResizeObserver(entries => { - for (const entry of entries) { - const height = - entry.borderBoxSize?.[0]?.blockSize ?? - entry.target.getBoundingClientRect().height; + // Measure on next frame (after initial render but before constraint is visible) + requestAnimationFrame(() => { + const height = element.offsetHeight; + if (height > 0) { onHeightChange(toast.key, height); + setHasMeasured(true); } }); - - resizeObserver.observe(element); - return () => resizeObserver.disconnect(); - }, [toast.key, onHeightChange]); + }, [toast.key, onHeightChange, hasMeasured]); // Close button ref and props const closeButtonRef = useRef(null); @@ -218,9 +216,10 @@ export const Toast = forwardRef( }; // Back toasts (index > 0) use front toast's height when collapsed - // When expanded, all toasts use their natural height (auto) - const shouldConstrainHeight = !isExpanded && index > 0 && collapsedHeight; - const animatedHeight = shouldConstrainHeight ? collapsedHeight : 'auto'; + // Height is set via style (not animated) to avoid measurement issues + // Only apply constraint after we've measured the natural height + const shouldConstrainHeight = + hasMeasured && !isExpanded && index > 0 && collapsedHeight; return ( { diff --git a/packages/ui/src/components/Toast/definition.ts b/packages/ui/src/components/Toast/definition.ts index eb43357ca0..42175a4359 100644 --- a/packages/ui/src/components/Toast/definition.ts +++ b/packages/ui/src/components/Toast/definition.ts @@ -46,6 +46,7 @@ export const ToastDefinition = defineComponent()({ style: {}, expandedY: {}, collapsedHeight: {}, + naturalHeight: {}, onHeightChange: {}, }, }); diff --git a/packages/ui/src/components/Toast/types.ts b/packages/ui/src/components/Toast/types.ts index dec8d15af0..25b920a012 100644 --- a/packages/ui/src/components/Toast/types.ts +++ b/packages/ui/src/components/Toast/types.ts @@ -56,6 +56,8 @@ export type ToastOwnProps = ContainerSurfaceProps & { expandedY?: number; /** Height to use when collapsed (front toast's height, for uniform stacking) */ collapsedHeight?: number; + /** This toast's natural height (for smooth animation) */ + naturalHeight?: number; /** Callback to report this toast's natural height */ onHeightChange?: (key: string, height: number) => void; };