Skip to content

fix(react-tooltip): In StrictMode, Tooltip now shows correctly on elements that are focused when created #34331

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from

Conversation

behowell
Copy link
Contributor

Previous Behavior

Tooltip uses a custom utilitiy useTimeout (useBrowserTimer) to manage the timer that delays showing the Tooltip. This utility creates the timer through an imperative function, but clears the timer in a useEffect cleanup function. This results in the tooltip never being shown if it is unmounted and remounted after the Tooltip was triggered but before it was shown. This can happen in StrictMode for a component that receives initial focus when it is created, such as a MenuItem.

New Behavior

Add a ref that saves the arguments to the pending visibility change, whenever a delayed visibility change is triggered. Then, add a useEffect that runs when the component is mounted, and checks the ref to see if there is a pending change that was never completed. If so, restart the timer.

Discussion

There are a number of different options for a fix here, with varying levels of impact on runtime performance.

The fix in this PR has the least impact on runtime performance and behavior. However, it adds an additional suboptimal useEffect call to restart the timer on mount and "fix" the useEffect in useBrowserTimer that cancels the timer on unmount.

The "correct" fix would be to move the timer to be started and cleared entirely within a useEffect. Then use a state variable to track that the tooltip was triggered, which would cause the useEffect to be run upon re-rendering. Example code is below. This has a major drawback of re-rendering the tooltipped component any time the Tooltip timer is started or stopped, which happens on every pointerEnter, pointerLeave, focus, or blur event, in addition to re-rendering whenever the tooltip shows or hides (and in React <=17, it actually adds two extra renders per show or hide, because state changes are not coalesced when using timers).

The net effect is that showing a tooltip goes from 1 re-render to 2 or 3 re-renders (same for hiding the tooltip), and a quick mouseover/mouseout goes from 0 re-renders to 2-re-renders.

type PendingVisible = {
  ev: React.PointerEvent<HTMLElement> | React.FocusEvent<HTMLElement> | undefined;
  visible: boolean;
  delayUntil: number;
};
const [pendingVisible, setPendingVisible] = React.useState<PendingVisible | undefined>(undefined);

const setDelayedVisible = React.useCallback(
  (ev, visible, delay) => setPendingVisible({ ev, visible, delayUntil: Date.now() + delay }),
  [],
);

// Run the timer when there is a pending visible change
React.useEffect(() => {
  if (pendingVisible) {
    const { ev, visible, delayUntil } = pendingVisible;
    const delay = Math.max(0, delayUntil - Date.now());
    const id = targetWindow?.setTimeout(() => setVisible(ev, { visible }), delay);
    return () => {
      targetWindow?.clearTimeout(id);
    };
  }
}, [pendingVisible, targetWindow, setVisible]);

There are a few other possible fixes that involve moving the timer to be within a separate child component of the Tooltip. E.g. we could move all of the visibility state management into a new "TooltipContent" component that would replace the div currently used for the content slot. The major benefit to that would be that the tooltipped component would never have any re-renders caused by the tooltip (it currently re-renders once every time the tooltip shows or hides). Unfortunately that would most likely be a breaking change to the API of Tooltip, so it's not really an option for a bug fix.

Related Issue(s)

Copy link

github-actions bot commented Apr 25, 2025

📊 Bundle size report

Package & Exports Baseline (minified/GZIP) PR Change
react-components
react-components: entire library
1.238 MB
310.613 kB
1.238 MB
310.731 kB
265 B
118 B
react-tooltip
Tooltip
57.261 kB
19.971 kB
57.526 kB
20.071 kB
265 B
100 B
Unchanged fixtures
Package & Exports Size (minified/GZIP)
react-avatar
Avatar
49.41 kB
15.856 kB
react-avatar
AvatarGroup
20.21 kB
8.004 kB
react-avatar
AvatarGroupItem
63.554 kB
20.073 kB
react-checkbox
Checkbox
35.221 kB
12.11 kB
react-combobox
Combobox (including child components)
106.288 kB
34.738 kB
react-combobox
Dropdown (including child components)
106.913 kB
34.661 kB
react-components
react-components: Button, FluentProvider & webLightTheme
69.737 kB
20.275 kB
react-components
react-components: Accordion, Button, FluentProvider, Image, Menu, Popover
225.747 kB
65.487 kB
react-components
react-components: FluentProvider & webLightTheme
44.573 kB
14.637 kB
react-datepicker-compat
DatePicker Compat
226.173 kB
64.134 kB
react-dialog
Dialog (including children components)
102.132 kB
30.684 kB
react-field
Field
23.548 kB
8.941 kB
react-input
Input
28.117 kB
9.479 kB
react-list
List
89.509 kB
26.675 kB
react-list
ListItem
112.847 kB
33.467 kB
react-persona
Persona
56.301 kB
17.736 kB
react-portal-compat
PortalCompatProvider
8.39 kB
2.64 kB
react-progress
ProgressBar
17.188 kB
6.921 kB
react-radio
Radio
32.775 kB
10.379 kB
react-radio
RadioGroup
15.865 kB
6.464 kB
react-select
Select
27.835 kB
10.162 kB
react-slider
Slider
38.332 kB
12.851 kB
react-spinbutton
SpinButton
35.318 kB
11.787 kB
react-swatch-picker
@fluentui/react-swatch-picker - package
106.721 kB
30.838 kB
react-switch
Switch
35.422 kB
11.344 kB
react-table
DataGrid
161.3 kB
45.735 kB
react-table
Table (Primitives only)
42.77 kB
13.889 kB
react-table
Table as DataGrid
132.057 kB
36.619 kB
react-table
Table (Selection only)
70.64 kB
20.037 kB
react-table
Table (Sort only)
69.283 kB
19.651 kB
react-tag-picker
@fluentui/react-tag-picker - package
187.176 kB
56.223 kB
react-tags
InteractionTag
15.538 kB
6.249 kB
react-tags
Tag
30.339 kB
9.927 kB
react-tags
TagGroup
83.851 kB
24.879 kB
react-textarea
Textarea
26.684 kB
9.786 kB
react-timepicker-compat
TimePicker
109.264 kB
36.293 kB
react-tree
FlatTree
148.956 kB
42.731 kB
react-tree
PersonaFlatTree
149.708 kB
42.868 kB
react-tree
PersonaTree
145.907 kB
41.717 kB
react-tree
Tree
145.161 kB
41.581 kB
🤖 This report was generated against f8d982e2e04682954850e17674f7e770b8d552f8

Copy link

Pull request demo site: URL

@@ -0,0 +1,7 @@
{
Copy link

@github-actions github-actions bot Apr 25, 2025

Choose a reason for hiding this comment

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

🕵🏾‍♀️ visual changes to review in the Visual Change Report

vr-tests-react-components/Avatar Converged 1 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Avatar Converged.badgeMask - RTL.normal.chromium.png 3 Changed
vr-tests-react-components/Positioning 2 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Positioning.Positioning end.updated 2 times.chromium.png 182 Changed
vr-tests-react-components/Positioning.Positioning end.chromium.png 837 Changed
vr-tests-react-components/TagPicker 2 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/TagPicker.disabled - Dark Mode.disabled input hover.chromium.png 659 Changed
vr-tests-react-components/TagPicker.disabled.disabled input hover.chromium.png 678 Changed

There were 2 duplicate changes discarded. Check the build logs for more information.

@behowell behowell marked this pull request as ready for review April 25, 2025 23:03
@behowell behowell requested a review from a team as a code owner April 25, 2025 23:03
@tudorpopams tudorpopams requested a review from marcosmoura April 29, 2025 12:09
Copy link
Contributor

@marcosmoura marcosmoura left a comment

Choose a reason for hiding this comment

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

This seems like a good option for now.

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

Successfully merging this pull request may close these issues.

[Bug]: Tooltips of MenuItem is not showing when open menu button by keyboard
2 participants