From 1bb638d1ff72e9c343db7f00877615a146d1f32c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20Pradel?= Date: Thu, 24 Sep 2026 19:30:50 +0200 Subject: [PATCH] fix: pass the animation event to onAnimationEnd The handler dropped the animation event and also reacted to animationend events bubbling up from elements inside the modal. A child animation could trigger onAnimationEnd and, while closing, unmount the portal before the modal exit animation finished. Pass the React.AnimationEvent to onAnimationEnd and ignore events whose target is not the modal element itself. Closes #522 --- .../__tests__/index.test.tsx | 43 +++++++++++++++++++ react-responsive-modal/src/index.tsx | 13 ++++-- website/src/docs/index.mdx | 2 +- 3 files changed, 54 insertions(+), 4 deletions(-) diff --git a/react-responsive-modal/__tests__/index.test.tsx b/react-responsive-modal/__tests__/index.test.tsx index a8695f30..a6a77881 100644 --- a/react-responsive-modal/__tests__/index.test.tsx +++ b/react-responsive-modal/__tests__/index.test.tsx @@ -746,6 +746,49 @@ describe('modal', () => { fireEvent.animationEnd(getByTestId('modal')); expect(onAnimationEnd).toHaveBeenCalledTimes(1); }); + + it('should pass the animation event to onAnimationEnd', async () => { + const onAnimationEnd = vitest.fn(); + const { getByTestId } = render( + null} onAnimationEnd={onAnimationEnd}> +
modal content
+
, + ); + + const modal = getByTestId('modal'); + fireEvent.animationEnd(modal); + expect(onAnimationEnd).toHaveBeenCalledTimes(1); + expect(onAnimationEnd.mock.calls[0][0].target).toBe(modal); + }); + + it('should not be called when a child animation ends', async () => { + const onAnimationEnd = vitest.fn(); + const { getByTestId } = render( + null} onAnimationEnd={onAnimationEnd}> +
modal content
+
, + ); + + fireEvent.animationEnd(getByTestId('child')); + expect(onAnimationEnd).not.toHaveBeenCalled(); + }); + + it('should not hide the modal when a child animation ends while closing', async () => { + const { getByTestId, queryByTestId, rerender } = render( + null} animationDuration={0.01}> +
modal content
+
, + ); + + rerender( + null} animationDuration={0.01}> +
modal content
+
, + ); + + fireEvent.animationEnd(getByTestId('child')); + expect(queryByTestId('modal')).toBeTruthy(); + }); }); describe('prop: containerId', () => { diff --git a/react-responsive-modal/src/index.tsx b/react-responsive-modal/src/index.tsx index 78465784..15d761eb 100644 --- a/react-responsive-modal/src/index.tsx +++ b/react-responsive-modal/src/index.tsx @@ -164,7 +164,7 @@ export interface ModalProps { /** * Callback fired when the Modal has exited and the animation is finished. */ - onAnimationEnd?: () => void; + onAnimationEnd?: (event: React.AnimationEvent) => void; children?: React.ReactNode; } @@ -313,12 +313,19 @@ export const Modal = React.forwardRef( refShouldClose.current = false; }; - const handleAnimationEnd = () => { + const handleAnimationEnd = ( + event: React.AnimationEvent, + ) => { + // Ignore animations bubbling up from elements inside the modal + if (event.target !== event.currentTarget) { + return; + } + if (!open) { setShowPortal(false); } - onAnimationEnd?.(); + onAnimationEnd?.(event); }; const containerModal = container || refContainer.current; diff --git a/website/src/docs/index.mdx b/website/src/docs/index.mdx index 055d96e2..52065c70 100644 --- a/website/src/docs/index.mdx +++ b/website/src/docs/index.mdx @@ -219,7 +219,7 @@ By default, the Modal will be rendered at the end of the html body tag. If you w | **onClose\*** | `() => void` | | Callback fired when the Modal is requested to be closed by a click on the overlay or when user press esc key. | | **onEscKeyDown\*** | `(event: KeyboardEvent) => void` | | Callback fired when the escape key is pressed. | | **onOverlayClick\*** | `(event: React.MouseEvent) => void` | | Callback fired when the overlay is clicked. | -| **onAnimationEnd\*** | `() => void` | | Callback fired when the Modal has exited and the animation is finished. | +| **onAnimationEnd\*** | `(event: React.AnimationEvent) => void` | | Callback fired when the Modal has exited and the animation is finished. | ## License