Skip to content

fix(Popper): prevent flash of incorrectly positioned popper on open#12177

Open
fallmo wants to merge 8 commits intopatternfly:mainfrom
fallmo:main
Open

fix(Popper): prevent flash of incorrectly positioned popper on open#12177
fallmo wants to merge 8 commits intopatternfly:mainfrom
fallmo:main

Conversation

@fallmo
Copy link
Contributor

@fallmo fallmo commented Dec 14, 2025

Description

Fixes an issue where popper elements (dropdowns, select, etc.) briefly flash at the top-left of the screen before appearing in their correct position.

bug4.mp4

Issue

When a popper opens, there's a race condition between:

  1. The popper element being rendered (initially with opacity: 0)
  2. Popper.js calculating and applying the correct position
  3. The opacity transitioning to 1

Previously, the opacity would transition to 1 immediately after setting internalIsVisible, which could occur before Popper.js finished calculating and applying the position transform.

Fix

Added two requestAnimationFrame calls in the show() function:

  • The first ensures React has committed DOM changes and the popper element exists
  • The second ensures Popper.js has calculated positions and applied transforms

Only after both frames do we allow the opacity transition to begin. This guarantees the browser has painted the correctly positioned element before making it visible.

Testing

The issue occurs intermittently and can be hard to reproduce reliably.
If you haven't encountered it, go to the Menus > Dropdown page, open and close the dropdowns back to back, then repeat with Menus > Select. The issue should reproduce within a few attempts.

Summary by CodeRabbit

  • Bug Fixes

    • Improved popper display timing so tooltips, dropdowns and popovers wait for layout and animation frames before becoming visible, yielding more accurate placement and smoother reveal/hide transitions.
  • Tests

    • Updated unit and e2e tests to wait for popper visibility transitions and extended timeouts where needed, reducing flakiness and improving snapshot/visual stability.

fallmo and others added 3 commits November 22, 2025 00:34
Update ref types to HTMLSpanElement and initialize with null to fix TS errors

Signed-off-by: Mohamed Fall <ps.hackmaster@gmail.com>
Use requestAnimationFrame to ensure Popper.js has calculated and applied position transforms before transitioning opacity to 1.

Signed-off-by: Mohamed Fall <ps.hackmaster@gmail.com>
@coderabbitai
Copy link

coderabbitai bot commented Dec 14, 2025

Walkthrough

Popper's show/hide timing now uses requestAnimationFrame sequencing with explicit cancellation; a new utility clearAnimationFrame (and its mock) was added. Unit and Cypress tests were updated to wait for opacity/positioning or use extended timeouts to accommodate the RAF-based timing changes.

Changes

Cohort / File(s) Summary
Popper visibility timing
packages/react-core/src/helpers/Popper/Popper.tsx
Reworked show/hide flows to use a two-frame requestAnimationFrame sequence before applying opacity/onShown; added rafRef and explicit RAF cancellation in hide/unmount paths. No public API changes.
Animation-frame utilities
packages/react-core/src/helpers/util.ts, packages/react-core/src/helpers/__mocks__/util.ts
Added clearAnimationFrame(animationFrameRef: React.RefObject<number>) to cancel pending rAFs; added corresponding mock clearAnimationFrame in __mocks__.
Unit tests — synchronization
packages/react-core/src/components/DatePicker/__tests__/DatePicker.test.tsx, packages/react-core/src/components/Nav/__tests__/Nav.test.tsx, packages/react-core/src/components/SearchInput/__tests__/SearchInput.test.tsx
Imported waitFor and added waitFor assertions to wait for popper panel opacity to reach 1 after interactions, synchronizing tests with RAF-based visibility timing.
Integration tests — timeouts
packages/react-integration/cypress/integration/button.spec.ts, packages/react-integration/cypress/integration/overflowmenu.spec.ts, packages/react-integration/cypress/integration/tabsdisable.spec.ts, packages/react-integration/cypress/integration/searchinput.spec.ts
Extended Cypress get/visibility assertion timeouts (to 6000ms) and added comments to account for asynchronous RAF-based positioning, reducing flakiness.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • mcoker
  • lboehling
  • thatblindgeye
  • wise-king-sullyman
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the main change: preventing a visual flash of incorrectly positioned popper elements by fixing timing issues in the Popper component's show flow.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d5053dd and 1bb6a2b.

📒 Files selected for processing (1)
  • packages/react-core/src/helpers/Popper/Popper.tsx (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build
  • GitHub Check: Build, test & deploy

@patternfly-build
Copy link
Collaborator

patternfly-build commented Dec 14, 2025

@fallmo fallmo marked this pull request as draft December 14, 2025 13:21
Update Jest snapshot tests to wait for popper elements to complete
their opacity transition after the double requestAnimationFrame fix.
This ensures snapshots are taken after poppers are fully visible
(opacity: 1) rather than during the transition phase (opacity: 0).

Signed-off-by: Mohamed Fall <ps.hackmaster@gmail.com>
…n 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>
@fallmo fallmo force-pushed the main branch 2 times, most recently from a71702d to f89b313 Compare December 20, 2025 12:30
…nc nature

Signed-off-by: Mohamed Fall <ps.hackmaster@gmail.com>
@fallmo fallmo marked this pull request as ready for review December 20, 2025 13:01
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
packages/react-integration/cypress/integration/overflowmenu.spec.ts (1)

35-35: LGTM - Consistent timeout extensions for menu visibility.

All four menu visibility checks appropriately extend the timeout to 6000ms for RAF-based positioning. Optionally, consider adding explanatory comments similar to the tooltip tests for consistency.

Also applies to: 72-72, 110-110, 145-145

packages/react-core/src/helpers/util.ts (1)

527-536: LGTM - Well-implemented utility for RAF cleanup.

The clearAnimationFrames function correctly mirrors the pattern of the existing clearTimeouts utility and properly cancels pending animation frames. The implementation is safe and follows best practices.

Optional: Enhance JSDoc for clarity

Consider expanding the JSDoc to describe the function's purpose:

 /**
+ * Cancels all pending animation frames tracked by the provided refs.
+ *
  * @param {React.RefObject<any>[]} animationFrameRefs - Animation frame refs to clear
  */
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1bb6a2b and a9a1aaf.

📒 Files selected for processing (9)
  • packages/react-core/src/components/DatePicker/__tests__/DatePicker.test.tsx (2 hunks)
  • packages/react-core/src/components/Nav/__tests__/Nav.test.tsx (2 hunks)
  • packages/react-core/src/components/SearchInput/__tests__/SearchInput.test.tsx (2 hunks)
  • packages/react-core/src/helpers/Popper/Popper.tsx (5 hunks)
  • packages/react-core/src/helpers/__mocks__/util.ts (1 hunks)
  • packages/react-core/src/helpers/util.ts (1 hunks)
  • packages/react-integration/cypress/integration/button.spec.ts (2 hunks)
  • packages/react-integration/cypress/integration/overflowmenu.spec.ts (4 hunks)
  • packages/react-integration/cypress/integration/tabsdisable.spec.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
packages/react-core/src/helpers/__mocks__/util.ts (1)
packages/react-core/src/helpers/util.ts (1)
  • clearAnimationFrames (530-536)
packages/react-core/src/helpers/util.ts (1)
packages/react-core/src/helpers/__mocks__/util.ts (1)
  • clearAnimationFrames (5-5)
🔇 Additional comments (15)
packages/react-integration/cypress/integration/tabsdisable.spec.ts (1)

43-44: LGTM - Appropriate test synchronization for RAF-based positioning.

The extended timeout and explanatory comment align with the PR's objective to accommodate asynchronous positioning via requestAnimationFrame.

packages/react-integration/cypress/integration/button.spec.ts (1)

12-13: LGTM - Consistent test synchronization across tooltip tests.

Both tooltip visibility checks appropriately extend the timeout to accommodate RAF-based positioning.

Also applies to: 22-22

packages/react-core/src/components/DatePicker/__tests__/DatePicker.test.tsx (2)

1-1: LGTM - Added waitFor for async assertion.

Appropriate import addition to support opacity transition synchronization.


96-100: LGTM - Proper synchronization for RAF-based positioning.

The waitFor block correctly waits for the popper's opacity transition to complete before snapshot assertion, aligning with the new two-frame RAF sequence in Popper.

packages/react-core/src/helpers/__mocks__/util.ts (1)

5-5: LGTM - Appropriate no-op mock for test environment.

The mock follows the established pattern of other utility mocks in this file and appropriately stubs the clearAnimationFrames function for test environments.

packages/react-core/src/components/Nav/__tests__/Nav.test.tsx (2)

2-2: LGTM - Added waitFor import for async assertion.

Appropriate import addition to support flyout opacity transition synchronization.


245-249: LGTM - Proper synchronization for flyout positioning.

The waitFor block correctly waits for the popper's opacity transition after hover, ensuring the flyout is fully visible before snapshot assertion.

packages/react-core/src/components/SearchInput/__tests__/SearchInput.test.tsx (2)

2-2: LGTM - Added waitFor import for async assertion.

Appropriate import addition to support panel opacity transition synchronization.


154-158: LGTM - Proper synchronization for panel positioning.

The waitFor block correctly waits for the popper panel's opacity transition after the advanced search panel opens, ensuring proper visibility before snapshot assertion.

packages/react-core/src/helpers/Popper/Popper.tsx (6)

6-6: LGTM!

Import of clearAnimationFrames utility enables consistent RAF cleanup alongside the existing timeout management.


237-237: LGTM!

The rafRef properly tracks the pending requestAnimationFrame ID, addressing the previously identified race condition concern.


276-282: LGTM!

Cleanup effect properly cancels both timeouts and animation frames on unmount, preventing state updates on unmounted components.


471-482: LGTM!

The clearAnimationFrames call ensures any pending show animation is cancelled when the exit delay changes, maintaining consistent state.


484-500: Well-implemented fix for the positioning flash issue.

The double requestAnimationFrame pattern correctly ensures:

  1. React has committed DOM changes (first RAF)
  2. Popper.js has calculated and applied transforms (second RAF)

The RAF tracking addresses the race condition identified in the previous review — if hide() is called at any point, clearAnimationFrames([rafRef]) will cancel whichever RAF is currently pending.


502-514: LGTM!

The clearAnimationFrames([rafRef]) call in hide() correctly cancels any pending RAF callbacks from show(), preventing the popper from becoming visible when it should be hidden.

Copy link
Contributor

@nicolethoen nicolethoen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall I like this change but I do have two concerns/callouts:

  • Any PF consumers who may have written unit tests related to testing popper dependent components are at risk of having their unit tests now fail. I think that if they are in that situation, it'd indicate they are not following testing best practices, but that's the sort of thing I'd love to process with @wise-king-sullyman. It's also likely that if they were trying to test their popper components, that those test could be flakey themselves since they were needing to contend with the flashing issue as well... so this may be a net improvement for even the projects it affects. 🤔

  • I think searchinput.spec.ts also needs to be updated on line 56 to add the timeout

@fallmo
Copy link
Contributor Author

fallmo commented Jan 10, 2026

Overall I like this change but I do have two concerns/callouts:

  • Any PF consumers who may have written unit tests related to testing popper dependent components are at risk of having their unit tests now fail. I think that if they are in that situation, it'd indicate they are not following testing best practices, but that's the sort of thing I'd love to process with @wise-king-sullyman. It's also likely that if they were trying to test their popper components, that those test could be flakey themselves since they were needing to contend with the flashing issue as well... so this may be a net improvement for even the projects it affects. 🤔
  • I think searchinput.spec.ts also needs to be updated on line 56 to add the timeout

Thanks for the review and thanks for the callouts. Good catch on searchinput.spec.ts, I went ahead and added a timeout.

…positioning

Signed-off-by: Mohamed Fall <ps.hackmaster@gmail.com>
Copy link
Collaborator

@wise-king-sullyman wise-king-sullyman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for making this PR! I just have a few questions/concerns.

const transitionTimerRef = useRef(null);
const showTimerRef = useRef(null);
const hideTimerRef = useRef(null);
const rafRef = useRef<number>(null);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does rafRef mean? The ref part I understand, but I have no idea what raf is referring to here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here raf is short for requestAnimationFrame. rafRef stores the ID returned by requestAnimationFrame, which lets us cancel the scheduled frame on cleanup. Would it make sense to rename rafRef to something a bit more descriptive, like animationFrameRef?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey sorry for the delay in circling back to this, yes I think that would be a better name.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've went ahead and changed it.

}
});
};

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that you're having this take an array as its arg and then forEaching through to cancel each ref in the array, but then in Popper it looks like you're just wrapping the ref by itself in an array with nothing else in it each time.

Am I missing/misunderstanding something here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, you're absolutely right here. The clearAnimationFrames function takes an array, but in practice I only pass one value. I was following the pattern I saw with the clearTimeouts function, while also accounting for the possibility that there be varied uses of the function in the future.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would probably lean towards simplifying the function since unlike with clearTimeouts we don't actually need to be able to pass multiple refs. If in the future we do need to we can always add that functionality.

Also I would adjust the name to reflect that it's just singular when you make that change.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've changed this too.

});
cy.get('.pf-v6-c-tooltip').should('be.visible');
// Tooltip visibility is async due to requestAnimationFrame-based positioning
cy.get('.pf-v6-c-tooltip', { timeout: 6000 }).should('be.visible');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have some real concerns with this change breaking a lot of tests for consumers if this change is needed. I'm not saying that's an absolute blocker, but it does give me pause. Would love to hear thoughts from others on this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point. Given the current behavior this felt like the safest tradeoff, but I’d definitely welcome other suggestions.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these test changes actually needed? I pulled your branch down and have run the tests a good few times with the timeouts removed and I'm not seeing any failures.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, in normal circumstances these changes aren't necessary; the raf calculations happen fast enough that a delay is pretty much non-existent. It's just that in many CI environments, the runners are super slow (cpu-constrained), as a result anything even slightly asynchronous may need a timeout.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah. That actually might make things even more tricky for consumers if they see their tests start failing on CI all of a sudden and they can't locally replicate. Especially since I'm having trouble reproducing the issue I'm not sure if the juice would be worth the squeeze here outside of a breaking change.

@nicolethoen @kmcfaul @thatblindgeye what do you all think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we do implement the changes here, we'd need to at least call out in release highlights that tests may need to be updated with a timeout for Popper related components. Though that may still not prevent confusion if consumer tests suddenly fail.

Even though it's only for some tests, 6 seconds waiting is fairly long, and can potentially still be a lot of updates for consumers to need to make. I also am not able to reproduce the raised issue after several attempts, so I'd be curious what version of PatternFly this bug was seen in (not to say it isn't an issue, it just makes things trickier when we can't reproduce it to confirm a) a fix is needed, and b) what that fix should be).

@fallmo
Copy link
Contributor Author

fallmo commented Feb 7, 2026

Thanks for making this PR! I just have a few questions/concerns.

Hey, thanks for the review !

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/react-core/src/helpers/__mocks__/util.ts`:
- Line 5: The mock clearAnimationFrame currently has no parameters; update the
exported mock function clearAnimationFrame to accept the same parameter as the
real implementation (animationFrameRef: React.RefObject<number>), check
animationFrameRef?.current, call cancelAnimationFrame on that value if present,
and then clear animationFrameRef.current (set to null) so Popper.tsx cleanup
calls (from Popper.tsx) match the real behavior and TypeScript types.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 120d043a-8591-470f-a296-76aa55cb89b6

📥 Commits

Reviewing files that changed from the base of the PR and between c3a02a5 and e5e7048.

📒 Files selected for processing (3)
  • packages/react-core/src/helpers/Popper/Popper.tsx
  • packages/react-core/src/helpers/__mocks__/util.ts
  • packages/react-core/src/helpers/util.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/react-core/src/helpers/util.ts
  • packages/react-core/src/helpers/Popper/Popper.tsx


export const clearTimeouts = () => {};

export const clearAnimationFrame = () => {};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Real clearAnimationFrame signature =="
cat -n packages/react-core/src/helpers/util.ts | head -30

echo
echo "== Mock clearAnimationFrame signature =="
cat -n packages/react-core/src/helpers/__mocks__/util.ts

echo
echo "== Jest manual mocks in react-core =="
find packages/react-core -name '__mocks__' -type d

echo
echo "== Test files using helpers/util mock =="
rg -l "__mocks__/util" packages/react-core --type=ts --type=tsx

echo
echo "== Actual clearAnimationFrame call sites =="
rg -n "clearAnimationFrame\(" packages/react-core/src --type=ts --type=tsx -B2 -A2

Repository: patternfly/patternfly-react

Length of output: 1928


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Real clearAnimationFrame in util.ts =="
rg -n "clearAnimationFrame" packages/react-core/src/helpers/util.ts -A5 -B2

echo
echo "== Mock util.ts complete file =="
cat packages/react-core/src/helpers/__mocks__/util.ts

echo
echo "== Test files importing helpers util =="
rg -l "from.*helpers/util" packages/react-core/src --type ts

echo
echo "== Popper component clearAnimationFrame usage =="
rg -n "clearAnimationFrame" packages/react-core/src/components/Popper -A3 -B3 --type ts

Repository: patternfly/patternfly-react

Length of output: 3514


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== All clearAnimationFrame calls in codebase =="
rg -n "clearAnimationFrame\(" packages/react-core/src -A2 -B2

echo
echo "== Check if Popper folder exists =="
find packages/react-core/src -name "*Popper*" -o -name "*popper*" | head -20

echo
echo "== Test files that might use the mock =="
rg -l "__mocks__" packages/react-core/src --type ts | head -20

Repository: patternfly/patternfly-react

Length of output: 2759


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Check helpers/__mocks__/index.ts =="
cat packages/react-core/src/helpers/__mocks__/index.ts

echo
echo "== Popper test file looking for mock setup =="
find packages/react-core -path "*/Popper*" -name "*.test.*" -o -path "*/Popper*" -name "*.spec.*"

echo
echo "== Check for jest.mock in test files that use Popper =="
rg -l "jest\.mock.*helpers/util" packages/react-core/src --type ts

echo
echo "== Check Popper.tsx imports =="
head -20 packages/react-core/src/helpers/Popper/Popper.tsx

Repository: patternfly/patternfly-react

Length of output: 1199


Mock clearAnimationFrame must accept and handle the animationFrameRef parameter

The mock at line 5 exports a zero-arg no-op, but the real implementation takes animationFrameRef: React.RefObject<number> and calls cancelAnimationFrame(). Popper.tsx passes animationFrameRef at cleanup (lines 279, 474, 487, 505), so the mock will cause TypeScript errors in tests and allow queued animation frame callbacks to execute after unmount.

Fix
+import type * as React from 'react';
+
 export const getUniqueId = () => 'unique_id_mock';
 
 export const clearTimeouts = () => {};
 
-export const clearAnimationFrame = () => {};
+export const clearAnimationFrame = (animationFrameRef: React.RefObject<number>) => {
+  if (animationFrameRef.current) {
+    cancelAnimationFrame(animationFrameRef.current);
+  }
+};
 
 export const getLanguageDirection = () => 'ltr';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react-core/src/helpers/__mocks__/util.ts` at line 5, The mock
clearAnimationFrame currently has no parameters; update the exported mock
function clearAnimationFrame to accept the same parameter as the real
implementation (animationFrameRef: React.RefObject<number>), check
animationFrameRef?.current, call cancelAnimationFrame on that value if present,
and then clear animationFrameRef.current (set to null) so Popper.tsx cleanup
calls (from Popper.tsx) match the real behavior and TypeScript types.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants