Skip to content

Commit 161cb88

Browse files
committed
fix(Popper): prevent race conditions by properly cleaning up animation frames
Add cleanup for requestAnimationFrame to prevent race conditions when popper visibility changes rapidly or component unmounts during RAF execution. Signed-off-by: Mohamed Fall <ps.hackmaster@gmail.com>
1 parent 97c8cfc commit 161cb88

File tree

3 files changed

+22
-3
lines changed

3 files changed

+22
-3
lines changed

packages/react-core/src/helpers/Popper/Popper.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as ReactDOM from 'react-dom';
33
import { usePopper } from './thirdparty/react-popper/usePopper';
44
import { Options as OffsetOptions } from './thirdparty/popper-core/modifiers/offset';
55
import { Placement, Modifier } from './thirdparty/popper-core';
6-
import { clearTimeouts } from '../util';
6+
import { clearAnimationFrames, clearTimeouts } from '../util';
77
import { css } from '@patternfly/react-styles';
88
import '@patternfly/react-styles/css/components/Popper/Popper.css';
99
import { getLanguageDirection } from '../util';
@@ -234,6 +234,7 @@ export const Popper: React.FunctionComponent<PopperProps> = ({
234234
const transitionTimerRef = useRef(null);
235235
const showTimerRef = useRef(null);
236236
const hideTimerRef = useRef(null);
237+
const rafRef = useRef<number>(null);
237238
const prevExitDelayRef = useRef<number>(undefined);
238239

239240
const refOrTrigger = refElement || triggerElement;
@@ -275,6 +276,7 @@ export const Popper: React.FunctionComponent<PopperProps> = ({
275276
useEffect(
276277
() => () => {
277278
clearTimeouts([transitionTimerRef, hideTimerRef, showTimerRef]);
279+
clearAnimationFrames([rafRef]);
278280
},
279281
[]
280282
);
@@ -469,6 +471,7 @@ export const Popper: React.FunctionComponent<PopperProps> = ({
469471
useEffect(() => {
470472
if (prevExitDelayRef.current < exitDelay) {
471473
clearTimeouts([transitionTimerRef, hideTimerRef]);
474+
clearAnimationFrames([rafRef]);
472475
hideTimerRef.current = setTimeout(() => {
473476
transitionTimerRef.current = setTimeout(() => {
474477
setInternalIsVisible(false);
@@ -481,14 +484,16 @@ export const Popper: React.FunctionComponent<PopperProps> = ({
481484
const show = () => {
482485
onShow();
483486
clearTimeouts([transitionTimerRef, hideTimerRef]);
487+
clearAnimationFrames([rafRef]);
484488
showTimerRef.current = setTimeout(() => {
485489
setInternalIsVisible(true);
486490
// Ensures React has committed the DOM changes and the popper element is rendered
487-
requestAnimationFrame(() => {
491+
rafRef.current = requestAnimationFrame(() => {
488492
// Ensures Popper.js has calculated and applied the position transform before making element visible
489-
requestAnimationFrame(() => {
493+
rafRef.current = requestAnimationFrame(() => {
490494
setOpacity(1);
491495
onShown();
496+
rafRef.current = null;
492497
});
493498
});
494499
}, entryDelay);
@@ -497,6 +502,7 @@ export const Popper: React.FunctionComponent<PopperProps> = ({
497502
const hide = () => {
498503
onHide();
499504
clearTimeouts([showTimerRef]);
505+
clearAnimationFrames([rafRef]);
500506
hideTimerRef.current = setTimeout(() => {
501507
setOpacity(0);
502508
transitionTimerRef.current = setTimeout(() => {

packages/react-core/src/helpers/__mocks__/util.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,6 @@ export const getUniqueId = () => 'unique_id_mock';
22

33
export const clearTimeouts = () => {};
44

5+
export const clearAnimationFrames = () => {};
6+
57
export const getLanguageDirection = () => 'ltr';

packages/react-core/src/helpers/util.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,17 @@ export const clearTimeouts = (timeoutRefs: React.RefObject<any>[]) => {
524524
});
525525
};
526526

527+
/**
528+
* @param {React.RefObject<any>[]} animationFrameRefs - Animation frame refs to clear
529+
*/
530+
export const clearAnimationFrames = (animationFrameRefs: React.RefObject<any>[]) => {
531+
animationFrameRefs.forEach((ref) => {
532+
if (ref.current) {
533+
cancelAnimationFrame(ref.current);
534+
}
535+
});
536+
};
537+
527538
/**
528539
* Helper function to get the language direction of a given element, useful for figuring out if left-to-right
529540
* or right-to-left specific logic should be applied.

0 commit comments

Comments
 (0)