Fix height animation

Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
Charles de Dreuille
2026-02-03 17:53:18 +00:00
committed by Patrik Oldsberg
parent a8615fc145
commit 183cd70978
3 changed files with 21 additions and 20 deletions
+18 -20
View File
@@ -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<HTMLButtonElement>(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 (
<motion.div
@@ -230,17 +229,16 @@ export const Toast = forwardRef(
style={
{
'--toast-index': index,
height: shouldConstrainHeight ? collapsedHeight : undefined,
overflow: shouldConstrainHeight ? 'hidden' : undefined,
} as React.CSSProperties
}
layout
initial={{ opacity: 0, y: 100, scale: 1, height: 'auto' }}
initial={{ opacity: 0, y: 100, scale: 1 }}
animate={{
opacity: 1,
y: animateY,
scale: animateScale,
zIndex: stackZIndex,
height: animatedHeight,
}}
exit={exitAnimation}
onAnimationComplete={definition => {
@@ -46,6 +46,7 @@ export const ToastDefinition = defineComponent<ToastOwnProps>()({
style: {},
expandedY: {},
collapsedHeight: {},
naturalHeight: {},
onHeightChange: {},
},
});
@@ -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;
};