[$8000] Mac / Safari - Copy to clipboard Tooltip doesn't show copied on clicking the clipboard icon.

If you haven’t already, check out our contributing guidelines for onboarding and email<EMAIL_ADDRESS>to request to join our Slack channel!

Action Performed:

Go to any chat
Click on the participant title to open the details view
Click on the Copy to clipboard icon :clipboard:  in the email.
Notice that the tooltip is shown for Copy to clipboard  but not Copied after the text is copied.
(Videos to compare Chrome and Safari are attached)

Expected Result:
Copied  tooltip should be shown
Actual Result:
Copied tooltip after the text is copied doesn't show up
Workaround:
unknown
Platform:

Where is this issue occurring?

Web - Mac / Safari

Version Number: 1.2.33-2
Reproducible in staging?: Y
Reproducible in production?: Y
Email or phone of affected tester (no customers):
Logs: https://stackoverflow.com/c/expensify/questions/4856
Notes/Photos/Videos:
https://user-images.githubusercontent.com/43996225/204621989-5fad386a-c68b-4480-942b-158c8a71e4fe.mov
https://user-images.githubusercontent.com/43996225/204621991-264ae997-d575-4e1e-8cac-f35b5ccd3b88.mov
https://user-images.githubusercontent.com/43996225/204622210-9f209d52-af51-4250-a377-2cb5ed25e153.mp4
Expensify/Expensify Issue URL:
Issue reported by: @mananjadhav
Slack conversation: https://expensify.slack.com/archives/C049HHMV9SM/p1669714466881009
View all open jobs on GitHub
Upwork Automation - Do Not Edit

Upwork Job URL: https://www.upwork.com/jobs/~014886ff5b8f1db983
Upwork Job ID:<PHONE_NUMBER>548734976
Last Price Increase: 2022-12-29

Proposal
Issue
This PR:
https://github.com/Expensify/App/pull/12572
introduced resetsOnClickOutside to the Hoverable component. This causes this issue on Safari.
Fix
Remove the resetsOnClickOutside property. It's not really checking if the click is outside. It resets the state nonetheless.
diff --git a/src/components/Tooltip/index.js b/src/components/Tooltip/index.js
index 8d3a34549..af46f8154 100644
--- a/src/components/Tooltip/index.js
+++ b/src/components/Tooltip/index.js
@@ -189,7 +189,6 @@ class Tooltip extends PureComponent {
                     containerStyles={this.props.containerStyles}
                     onHoverIn={this.showTooltip}
                     onHoverOut={this.hideTooltip}
-                    resetsOnClickOutside
                 >
                     {child}
                 </Hoverable>

Result
https://user-images.githubusercontent.com/30054992/204679923-eddf82f3-9a4e-42dc-8923-9991c998ab26.mov

Proposal
I am seeing other places also similar behavior(eg. mini context menu) and not showing copied text for messages, We can remove the event listener mousedown from Hoverable component.
https://github.com/Expensify/App/blob/b35dba944cff88d5a2d7221dfa626f42d505d1c1/src/components/Hoverable/index.js#L24

Proposal
As resetsOnClickOutside added to solve this issue, we can pass value false for our use case(for copy actions)
https://github.com/Expensify/App/blob/1527fdcf443713bb058e6e76504006d74f6b1aa8/src/components/Tooltip/index.js#L192
resetsOnClickOutside={!_.isUndefined(this.props.resetsOnClickOutside) ? this.props.resetsOnClickOutside : true}

https://github.com/Expensify/App/blob/1527fdcf443713bb058e6e76504006d74f6b1aa8/src/components/ContextMenuItem.js#L77
<Tooltip text={text} resetsOnClickOutside={false}>

Proposal
diff --git a/src/components/Hoverable/hoverablePropTypes.js b/src/components/Hoverable/hoverablePropTypes.js
index 07b8a8741e..c17fa804b6 100644
--- a/src/components/Hoverable/hoverablePropTypes.js
+++ b/src/components/Hoverable/hoverablePropTypes.js
@@ -19,9 +19,6 @@ const propTypes = {

     /** Function that executes when the mouse leaves the children. */
     onHoverOut: PropTypes.func,
-
-    // If the mouse clicks outside, should we dismiss hover?
-    resetsOnClickOutside: PropTypes.bool,
 };

 const defaultProps = {
@@ -29,7 +26,6 @@ const defaultProps = {
     containerStyles: [],
     onHoverIn: () => {},
     onHoverOut: () => {},
-    resetsOnClickOutside: false,
 };

 export {
diff --git a/src/components/Hoverable/index.js b/src/components/Hoverable/index.js
index f06ed56027..b4703d7cfc 100644
--- a/src/components/Hoverable/index.js
+++ b/src/components/Hoverable/index.js
@@ -16,13 +16,9 @@ class Hoverable extends Component {
         };

         this.wrapperView = null;
-
-        this.resetHoverStateOnOutsideClick = this.resetHoverStateOnOutsideClick.bind(this);
     }

     componentDidMount() {
-        document.addEventListener('mousedown', this.resetHoverStateOnOutsideClick);
-
         // we like to Block the hover on touch devices but we keep it for Hybrid devices so
         // following logic blocks hover on touch devices.
         this.disableHover = () => {
@@ -38,7 +34,6 @@ class Hoverable extends Component {
     }

     componentWillUnmount() {
-        document.removeEventListener('mousedown', this.resetHoverStateOnOutsideClick);
         document.removeEventListener('touchstart', this.disableHover);
         document.removeEventListener('touchmove', this.enableHover);
     }
@@ -50,7 +45,8 @@ class Hoverable extends Component {
      */
     setIsHovered(isHovered) {
         if (isHovered !== this.state.isHovered && !(isHovered && this.hoverDisabled)) {
-            this.setState({isHovered}, isHovered ? this.props.onHoverIn : this.props.onHoverOut);
+            this.setState({isHovered});
+            isHovered ? this.props.onHoverIn() : this.props.onHoverOut();
         }

         // we reset the Hover block in case touchmove was not first after touctstart
@@ -59,28 +55,6 @@ class Hoverable extends Component {
         }
     }

-    /**
-     * If the user clicks outside this component, we want to make sure that the hover state is set to false.
-     * There are some edge cases where the mouse can leave the component without the `onMouseLeave` event firing,
-     * leaving this Hoverable in the incorrect state.
-     * One such example is when a modal is opened while this component is hovered, and you click outside the component
-     * to close the modal.
-     *
-     * @param {Object} event - A click event
-     */
-    resetHoverStateOnOutsideClick(event) {
-        if (!this.state.isHovered) {
-            return;
-        }
-        if (this.props.resetsOnClickOutside) {
-            this.setIsHovered(false);
-            return;
-        }
-        if (this.wrapperView && !this.wrapperView.contains(event.target)) {
-            this.setIsHovered(false);
-        }
-    }
-
     render() {
         if (this.props.absolute && React.isValidElement(this.props.children)) {
             return React.cloneElement(React.Children.only(this.props.children), {
diff --git a/src/components/Tooltip/index.js b/src/components/Tooltip/index.js
index 8d3a345492..af46f8154d 100644
--- a/src/components/Tooltip/index.js
+++ b/src/components/Tooltip/index.js
@@ -189,7 +189,6 @@ class Tooltip extends PureComponent {
                     containerStyles={this.props.containerStyles}
                     onHoverIn={this.showTooltip}
                     onHoverOut={this.hideTooltip}
-                    resetsOnClickOutside
                 >
                     {child}
                 </Hoverable>

Details
First of all, in this GH issue the bug is https://github.com/Expensify/App/issues/12025 (we are basically reopening the issue)
The applied fix https://github.com/Expensify/App/pull/12572 is hacky and causes this problem. Why hacky? because it does not address the root issue and the proposed fix can be bypassed by pressing enter instead of a mouse click.
The root issue is the use of <Freeze /> where the callback of setState is suspended thus the this.props.onHoverOut is not called and the tooltip become stuck.
Ideally and the simplest solution is to exclude <Hoverable /> Unfortunately this does not seem as a feature that exists and I don't think we should get rid of <Freeze /> either, so we have to fix it in another approach.
In my proposal, I have basically reverted the previous PR and decoupled the functions calls to be called immediately and not in the callback. This however means that the functions may get called before the isHovered state is actually updated, given that this is not a serious matter I'd say that's an okay price to pay, not ideal but better.

Thanks for the proposal everyone. I am somewhat inclined towards @s77rt's proposal but don't like the fact that it is hacky.

The root issue is the use of  where the callback of setState is suspended

I couldn't confirm this statement. I did a quick search and couldn't find any related issues as well. Can you point me to some doc, issue where we can confirm.
I am going to also tag @rushatgabhane @getusha to see if they've come across this when they solved the linked issue.
@allroundexperts @Pujan92 I don't want to revert this without knowing the impact or causing regression. Do you have any strong arguments for your proposals?

@mananjadhav Just disable the <Freeze /> by passing prop freeze={false} You will notice it will work just fine.
Unfortunately I can't find a clear statement about the setState callback, that's something I deducted
Docs may be worth checking:
https://github.com/software-mansion/react-freeze#when-component-subtree-is-frozen-what-happens-to-state-changes-that-are-executed-on-that-subtree
https://beta.reactjs.org/apis/react/Suspense

@Pujan92 I don't want to revert this without knowing the impact or causing regression. Do you have any strong arguments for your proposals?

In my proposal, I am conveying to pass value to the resetsOnClickOutside property for our use case and I think it isn't related to revert.

@mananjadhav
After some inspection i have confirmed that this only happens when we click it on the edge/corners of the clickable.
https://user-images.githubusercontent.com/75031127/205059959-ba6fed8a-d9a0-4120-b15d-52044a388bc8.mp4
Can anyone confirm this? it helps

@getusha Are you testing on Safari?

Testing on chrome.
IMO It can't be platform specific issue.

It's browser specific, my guess is that onMouseEnter is triggered again on Chrome but not on Safari.. But this is not the root issue.

Not exactly what I meant, the event is supported by Safari but not fired at the same conditions as Chrome

This is a regression from #12572 , the proposed solution behave like onclick event , it does reset the hovered state on all click events even when pointer is inside the Hoverable , and I think that's wrong
        if (this.props.resetsOnClickOutside) {
             this.setIsHovered(false);
             return;
         }

I agree with @Ollyws https://github.com/Expensify/App/issues/12025#issuecomment-1286890133 and @s77rt https://github.com/Expensify/App/issues/13146#issuecomment-1332734440 for their proposal , the root issue for the other issue #12025 is that the state isn't updating (frozen) when navigation to other screens , the onMouseLeave event is firing correclty but the state isn't updating nor triggering the callback to hide the tooltip

Can you try this
componentDidMount() {         document.addEventListener('click', this.resetHoverStateOnOutsideClick);         // we like to Block the hover on touch devices but we keep it for Hybrid devices so         // following logic blocks hover on touch devices.         this.disableHover = () => {             this.hoverDisabled = true;         };         this.enableHover = () => {             this.hoverDisabled = false;         };         document.addEventListener('touchstart', this.disableHover);         // Remember Touchend fires beforemouseevents so we have to use alternative.         document.addEventListener('touchmove', this.enableHover);     }     componentWillUnmount() {         document.removeEventListener('click', this.resetHoverStateOnOutsideClick);         document.removeEventListener('touchstart', this.disableHover);         document.removeEventListener('touchmove', this.enableHover);     }

@francoisl No, actually issue is there in all browser. In any browser you click and hold the mouse(any movement on mouse will show tooltip, otherwise not) on same position it will happens. I tried all the mouse event actions, 'click' does the job. Tooltip need some event to trigger. Safari event trigger is more sensitive, thats why its more visible in that, other browsers too have this problem but less visible.

I like @vijeeshin 's approach, can you implement it and attach video results on your proposal?

Here is the proof
https://user-images.githubusercontent.com/772974/205429817-7b166c62-0779-48e1-92b6-7044f825cd90.mov

@vijeeshin it breaks everything.

Resize the screen and click the back icon The Tooltip is not dismissed.

@getusha
Will check that

There is no need of resetHoverStateOnOutsideClick This must be removed, it's only a workaround.
The real problem is on the fact that the element is suspended and callback setState does not get called.

@s77rt after applying the solution
The Tooltip is not dismissed unless we move the mouse,
IMO it is new issue which is equivalent to this issue.
https://user-images.githubusercontent.com/75031127/205433530-35ada6c5-27b5-466c-bd8f-529b3074837e.mp4

@getusha Yes indeed, I have noticed that, I had an idea to solve this already.
Will get back to this soon

Here is another work around
in ContextMenuItem.js
-77 <Tooltip text={text}>
+77 <Tooltip text={text} isDelayButtonStateComplete={this.props.isDelayButtonStateComplete}>
in Tooltip.js
-40 componentDidUpdate(prevProps) {
        if (this.props.windowWidth === prevProps.windowWidth && this.props.windowHeight === prevProps.windowHeight) {
            return;
        }

        this.getWrapperPositionPromise = makeCancellablePromise(this.getWrapperPosition());
        this.getWrapperPositionPromise.promise
            .then(({x, y}) => this.setState({xOffset: x, yOffset: y}));
    }

+40 componentDidUpdate(prevProps) {
        if(prevProps.isDelayButtonStateComplete !== this.props.isDelayButtonStateComplete) {
              this.showTooltip();
              setTimeout(()=>{
                  this.hideTooltip();
              },1000);
        }
        if (this.props.windowWidth === prevProps.windowWidth && this.props.windowHeight === prevProps.windowHeight) {
            return;
        }

        this.getWrapperPositionPromise = makeCancellablePromise(this.getWrapperPosition());
        this.getWrapperPositionPromise.promise
            .then(({x, y}) => this.setState({xOffset: x, yOffset: y}));
    }

Proposal (Updated)
diff --git a/src/components/Hoverable/hoverablePropTypes.js b/src/components/Hoverable/hoverablePropTypes.js
index 07b8a8741e..c17fa804b6 100644
--- a/src/components/Hoverable/hoverablePropTypes.js
+++ b/src/components/Hoverable/hoverablePropTypes.js
@@ -19,9 +19,6 @@ const propTypes = {

     /** Function that executes when the mouse leaves the children. */
     onHoverOut: PropTypes.func,
-
-    // If the mouse clicks outside, should we dismiss hover?
-    resetsOnClickOutside: PropTypes.bool,
 };

 const defaultProps = {
@@ -29,7 +26,6 @@ const defaultProps = {
     containerStyles: [],
     onHoverIn: () => {},
     onHoverOut: () => {},
-    resetsOnClickOutside: false,
 };

 export {
diff --git a/src/components/Hoverable/index.js b/src/components/Hoverable/index.js
index f06ed56027..b4703d7cfc 100644
--- a/src/components/Hoverable/index.js
+++ b/src/components/Hoverable/index.js
@@ -16,13 +16,9 @@ class Hoverable extends Component {
         };

         this.wrapperView = null;
-
-        this.resetHoverStateOnOutsideClick = this.resetHoverStateOnOutsideClick.bind(this);
     }

     componentDidMount() {
-        document.addEventListener('mousedown', this.resetHoverStateOnOutsideClick);
-
         // we like to Block the hover on touch devices but we keep it for Hybrid devices so
         // following logic blocks hover on touch devices.
         this.disableHover = () => {
@@ -38,7 +34,6 @@ class Hoverable extends Component {
     }

     componentWillUnmount() {
-        document.removeEventListener('mousedown', this.resetHoverStateOnOutsideClick);
         document.removeEventListener('touchstart', this.disableHover);
         document.removeEventListener('touchmove', this.enableHover);
     }
@@ -50,7 +45,8 @@ class Hoverable extends Component {
      */
     setIsHovered(isHovered) {
         if (isHovered !== this.state.isHovered && !(isHovered && this.hoverDisabled)) {
-            this.setState({isHovered}, isHovered ? this.props.onHoverIn : this.props.onHoverOut);
+            this.setState({isHovered});
+            isHovered ? this.props.onHoverIn() : this.props.onHoverOut();
         }

         // we reset the Hover block in case touchmove was not first after touctstart
@@ -59,28 +55,6 @@ class Hoverable extends Component {
         }
     }

-    /**
-     * If the user clicks outside this component, we want to make sure that the hover state is set to false.
-     * There are some edge cases where the mouse can leave the component without the `onMouseLeave` event firing,
-     * leaving this Hoverable in the incorrect state.
-     * One such example is when a modal is opened while this component is hovered, and you click outside the component
-     * to close the modal.
-     *
-     * @param {Object} event - A click event
-     */
-    resetHoverStateOnOutsideClick(event) {
-        if (!this.state.isHovered) {
-            return;
-        }
-        if (this.props.resetsOnClickOutside) {
-            this.setIsHovered(false);
-            return;
-        }
-        if (this.wrapperView && !this.wrapperView.contains(event.target)) {
-            this.setIsHovered(false);
-        }
-    }
-
     render() {
         if (this.props.absolute && React.isValidElement(this.props.children)) {
             return React.cloneElement(React.Children.only(this.props.children), {
diff --git a/src/components/Tooltip/index.js b/src/components/Tooltip/index.js
index 8d3a345492..af46f8154d 100644
--- a/src/components/Tooltip/index.js
+++ b/src/components/Tooltip/index.js
@@ -189,7 +189,6 @@ class Tooltip extends PureComponent {
                     containerStyles={this.props.containerStyles}
                     onHoverIn={this.showTooltip}
                     onHoverOut={this.hideTooltip}
-                    resetsOnClickOutside
                 >
                     {child}
                 </Hoverable>
diff --git a/src/libs/Navigation/AppNavigator/modalCardStyleInterpolator.js b/src/libs/Navigation/AppNavigator/modalCardStyleInterpolator.js
index 433620deec..aa9591548f 100644
--- a/src/libs/Navigation/AppNavigator/modalCardStyleInterpolator.js
+++ b/src/libs/Navigation/AppNavigator/modalCardStyleInterpolator.js
@@ -13,9 +13,9 @@ export default (
         },
     },
 ) => {
-    const translateX = Animated.multiply(progress.interpolate({
+    const right = Animated.multiply(progress.interpolate({
         inputRange: [0, 1],
-        outputRange: [isSmallScreenWidth ? screen.width : variables.sideBarWidth, 0],
+        outputRange: [isSmallScreenWidth ? -screen.width : -variables.sideBarWidth, 0],
         extrapolate: 'clamp',
     }), inverted);

@@ -25,7 +25,7 @@ export default (
     if (isFullScreenModal && !isSmallScreenWidth) {
         cardStyle.opacity = opacity;
     } else {
-        cardStyle.transform = [{translateX}];
+        cardStyle.right = right;
     }

     return ({

Update
Fixed this issue https://github.com/Expensify/App/issues/13146#issuecomment-1336120352
The issue is that the tooltip stays open upon opening a modal till you move the mouse as shown in @getusha 's video.
The reason for that is the use of CSS translate as it does not fire mouse events (mouseleave in this case) I have found a very similar issue to demonstrate and proof my findings: https://stackoverflow.com/questions/57507374/mouseleave-event-is-not-fired-when-element-transformed
Luckily the modals use a fixed position and we can easily swap transform translate with right  and that's exactly what I did.
Now the only downside of my proposal is that as stated before, the callback functions onHoverIn and onHoverOut may be called before isHovered state is actually updated.

Hi @mananjadhav , do you have an update on the reviews?

@mananjadhav did you have time to review the proposals? If needed, we can also bring this to Slack for second opinions.

hi @francoisl I am going to do that today.

Proposal
We can use key prop to recreate new instance of tooltip
Here is the more information
https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html#recommendation-fully-uncontrolled-component-with-a-key
diff --git a/src/components/ContextMenuItem.js b/src/components/ContextMenuItem.js
index c87fc5e9b4..f0080fcd90 100644
--- a/src/components/ContextMenuItem.js
+++ b/src/components/ContextMenuItem.js
@@ -75,7 +75,7 @@ class ContextMenuItem extends Component {
         return (
             this.props.isMini
                 ? (
-                    <Tooltip text={text}>
+                    <Tooltip key={text} text={text}>
                         <Pressable
                             focusable
                             accessibilityLabel={text}

https://user-images.githubusercontent.com/114683042/207600695-07bf9cf0-8726-4622-84ef-b71568490a48.mov

Tooltips are available for web only. We can add key into tooltip component so that other platforms not impact.
diff --git a/src/components/Tooltip/index.js b/src/components/Tooltip/index.js
index 8d3a345492..2824703dd9 100644
--- a/src/components/Tooltip/index.js
+++ b/src/components/Tooltip/index.js
@@ -190,6 +190,7 @@ class Tooltip extends PureComponent {
                     onHoverIn={this.showTooltip}
                     onHoverOut={this.hideTooltip}
                     resetsOnClickOutside
+                    key={this.props.text}
                 >
                     {child}
                 </Hoverable>

@jatinsonijs we have discussed this proposal before for another issue related to tooltip https://github.com/Expensify/App/issues/11486#issuecomment-1286885163

I don't think key={this.props.text} is a valid solution. This completely re-renders tooltip and sometimes breaks animation while changing text.

What about this one @fedirjh

Tooltips are available for web only. We can add key into tooltip component so that other platforms not impact.
diff --git a/src/components/Tooltip/index.js b/src/components/Tooltip/index.js
index 8d3a345492..2824703dd9 100644
--- a/src/components/Tooltip/index.js
+++ b/src/components/Tooltip/index.js
@@ -190,6 +190,7 @@ class Tooltip extends PureComponent {
                     onHoverIn={this.showTooltip}
                     onHoverOut={this.hideTooltip}
                     resetsOnClickOutside
+                    key={this.props.text}
                 >
                     {child}
                 </Hoverable>

@jatinsonijs can you check if this glitch https://github.com/Expensify/App/issues/11486#issuecomment-1286888933 exist after applying your updated solution

@getusha actually its already happening no matter if my solution is applied or not in the Reimburse expenses screen which was showed in video of linked issue.
And yes my first solution is sometime breaking animation. But second one working fine on another places.

in this issue I think ok to add key in Hoverable to re-render it. Bcz on press isHovered turn to false and onHoverOut will be triggered and in parent component tooltip will hide. so until we will in same area tooltip will not show again. with key it will create new instance of Hoverable which will trigger onHoverIn again and tooltip will visible.

@jatinsonijs this is the expected behavior (that's how it was working before), as you can see the tooltip is rendered once and t-only the text changes on click , so we don't have to re-render any component here , instead we have identify the root cause that bring this issue and fix it .
https://user-images.githubusercontent.com/36869046/207617916-39a7b5da-02eb-4130-8c91-ea2a99542d40.mov

Yes @fedirjh I already added the root cause. this issue only happen on edges outside of icon. when your cursor on icon (success icon green one) its already working fine no need to change. But if cursor is little outside its create issue bcz there is no change trigger.

@mananjadhav
I think something related to safari internal behaviour and React
https://reactjs.org/docs/events.html
The onMouseEnter and onMouseLeave events propagate from the element being left to the one being entered instead of ordinary bubbling and do not have a capture phase.
I have created basic example to check how its working in newly created project.
https://codesandbox.io/s/sad-monad-jegjrq
Safari not able to detect element changes until we move mouse.

 componentDidMount() {
        document.addEventListener('keydown', this.resetHoverStateOnOutsideClick);

        // we like to Block the hover on touch devices but we keep it for Hybrid devices so
        // following logic blocks hover on touch devices.
        this.disableHover = () => {
            this.hoverDisabled = true;
        };
        this.enableHover = () => {
            this.hoverDisabled = false;
        };
        document.addEventListener('touchstart', this.disableHover);

        // Remember Touchend fires before `mouse` events so we have to use alternative.
        document.addEventListener('touchmove', this.enableHover);
    }

    componentWillUnmount() {
        document.removeEventListener('keydown', this.resetHoverStateOnOutsideClick);
        document.removeEventListener('touchstart', this.disableHover);
        document.removeEventListener('touchmove', this.enableHover);
    }

keydown will trigger 3 events at once

@mananjadhav @jatinsonijs Issue reproducible in the chat history page too. Should we log as a separate issue?
https://user-images.githubusercontent.com/43996225/208757228-3b50f442-f506-4500-b1bb-35dcd5eefb42.mp4

@kavimuru it's reproducible in Pin/unpin chat too . It's the same issue

@mananjadhav do you have an update on the reviews here? As a heads up we've got one more week before this becomes an internal issue.

Thanks for the patience here team.
Here's the summary of all the proposals that have come so far.

Passing resetsOnClickOutside=false to specific tooltips or removing it completely.
While this works, it is more of a workaround. As you can see this is reproducible to more than one places and I don't think disabling this is the right way to handle this.

Replacing with click event
Definitely not the way to go. It breaks things and I don't see how click event will help for the hover components.

Adding setTimeout
Let's avoid workaround solutions for this one.

Proposals related to adding text or border/edges
Don't work and address the issue mentioned

keydown event
Again not sure how it helps for hover.

Removing resetsOnClickOutside and removing props hover callbacks outside of the state callback, would lead to race conditions.
I tested this and along with the other aspects of removing the code, moving to the right instead of transform, it didn't work for me. I could've missed something, I couldn't get enough evidence that Freeze stops the callbacks, and as I mentioned, let's not add race conditions by calling props before the state is updated.

@francoisl What are your thoughts?

Passing resetsOnClickOutside=false to specific tooltips

@mananjadhav can you plz give more information why you think its a workaround. Logically its sound correct don't dismiss toggle on click as there is different text on click.

an you plz give more information why you think its a workaround. Logically its sound correct don't dismiss tooltip on click as there is different text on click.

don't want to end up with a case, where the tooltip doesn't close when the page has moved or modal is opened.

@mananjadhav Proposals 1-5 are patching a patch (workaround for another workaround)
In my proposal I'm trying to fix the root cause and resetsOnClickOutside itself is a workaround that must be removed.

moving to the right instead of transform, it didn't work for me. I could've missed something,

are you applying the changes manually? you should just use git apply

I couldn't get enough evidence that Freeze

I have already provided info to verify that Freeze is the issue.

let's not add race conditions

I understand that with my proposal the callbacks may be called before the state is updated but this is as you said a race condition, a race condition that does not have negative effects because this only happens when the tooltip is not in view. Besides once the tooltip is on view again the state will be updated, it's like pressing pause then play, everything will always be in sync.

Yes @mananjadhav I had same concern when adding this prop. But wherever we have two text for tooltip we not have navigation there. It may case issue when both condition applied there is two text like (copy - copied) and navigation also.

Thanks for the comments team.
@jatinsonijs You're right that we have toggled Tooltip and hence here I am looking for a solution that works for both the cases (toggled tooltip + navigation). I don't want to fix this as a workaround or one off and then open another issue in the future for a similar case. I hope it makes sense. Let's try to solve the root cause.

@s77rt  Of all the proposals I was almost okay with your proposal. But I have my concerns related to race condition and also updating the modal styles (sorry I just saw I didn't mention in the previous summary).
https://github.com/Expensify/App/blob/70d87b0e6662c9fc06e85c0df35e3766fa4e1d04/src/components/Hoverable/index.js#L63-L67
If you read  the comment, Modal is just an example. Are we sure we're not breaking anything at all in any scenario?

I'll also let @francoisl review the proposal.

@mananjadhav The comments are wrong/misleading
I will explain it better:

There are some edge cases where the mouse can leave the component without the onMouseLeave event firing

This is not true, (actually it's partially true; I will explain in my 2nd point) onMouseLeave will always fire when the mouse leave the component, however the callback for that event is setIsHovered in which we call onHoverIn/onHoverOut in the setState callback, but <Freeze> will freeze the component preventing the executing of those functions which gives the impression that onMouseLeave has not been fired.
The only time where onMouseLeave won't be fired is if the mouse stays still and we move the component instead, especially if we "move" using CSS translate but it will work just fine if we actually making the movement using the CSS right

As far as I have tested, everything is working as expected (including modals and the drawer)

Thanks for the great summary @mananjadhav.
@s77rt's let me know if I'm missing something, but I don't think the translateX change you explained in this proposal works as expected – for example when navigating back and forth to your profile details, the tooltip remains visible until the animation finishes:
https://user-images.githubusercontent.com/2229301/209231228-8044800f-26cd-4c8c-b407-16e871c66a8d.mov

@francoisl Are you sure you applied my proposal correctly? It looks as if you are using CSS translateX or are you asking about the behaviour of translateX?
Here is how it looks after applying the proposal:
https://user-images.githubusercontent.com/16493223/209237570-a577c7d5-903a-4f9a-8a54-8994d51f20fb.mp4

Yes, I directly applied your proposal with git apply. I used the term translateX to refer to the part of your proposal that updates modalCardStyleInterpolator.js - apologies if that was confusing.
I was testing on Safari 16.1 (186<IP_ADDRESS>.12), if that matters.

Not a proposal
@s77rt still creating issue, plz check if I'm missing anything
diff --git a/src/components/Hoverable/hoverablePropTypes.js b/src/components/Hoverable/hoverablePropTypes.js
index 07b8a8741e..c17fa804b6 100644
--- a/src/components/Hoverable/hoverablePropTypes.js
+++ b/src/components/Hoverable/hoverablePropTypes.js
@@ -19,9 +19,6 @@ const propTypes = {

     /** Function that executes when the mouse leaves the children. */
     onHoverOut: PropTypes.func,
-
-    // If the mouse clicks outside, should we dismiss hover?
-    resetsOnClickOutside: PropTypes.bool,
 };

 const defaultProps = {
@@ -29,7 +26,6 @@ const defaultProps = {
     containerStyles: [],
     onHoverIn: () => {},
     onHoverOut: () => {},
-    resetsOnClickOutside: false,
 };

 export {
diff --git a/src/components/Hoverable/index.js b/src/components/Hoverable/index.js
index f06ed56027..b4703d7cfc 100644
--- a/src/components/Hoverable/index.js
+++ b/src/components/Hoverable/index.js
@@ -16,13 +16,9 @@ class Hoverable extends Component {
         };

         this.wrapperView = null;
-
-        this.resetHoverStateOnOutsideClick = this.resetHoverStateOnOutsideClick.bind(this);
     }

     componentDidMount() {
-        document.addEventListener('mousedown', this.resetHoverStateOnOutsideClick);
-
         // we like to Block the hover on touch devices but we keep it for Hybrid devices so
         // following logic blocks hover on touch devices.
         this.disableHover = () => {
@@ -38,7 +34,6 @@ class Hoverable extends Component {
     }

     componentWillUnmount() {
-        document.removeEventListener('mousedown', this.resetHoverStateOnOutsideClick);
         document.removeEventListener('touchstart', this.disableHover);
         document.removeEventListener('touchmove', this.enableHover);
     }
@@ -50,7 +45,8 @@ class Hoverable extends Component {
      */
     setIsHovered(isHovered) {
         if (isHovered !== this.state.isHovered && !(isHovered && this.hoverDisabled)) {
-            this.setState({isHovered}, isHovered ? this.props.onHoverIn : this.props.onHoverOut);
+            this.setState({isHovered});
+            isHovered ? this.props.onHoverIn() : this.props.onHoverOut();
         }

         // we reset the Hover block in case touchmove was not first after touctstart
@@ -59,28 +55,6 @@ class Hoverable extends Component {
         }
     }

-    /**
-     * If the user clicks outside this component, we want to make sure that the hover state is set to false.
-     * There are some edge cases where the mouse can leave the component without the `onMouseLeave` event firing,
-     * leaving this Hoverable in the incorrect state.
-     * One such example is when a modal is opened while this component is hovered, and you click outside the component
-     * to close the modal.
-     *
-     * @param {Object} event - A click event
-     */
-    resetHoverStateOnOutsideClick(event) {
-        if (!this.state.isHovered) {
-            return;
-        }
-        if (this.props.resetsOnClickOutside) {
-            this.setIsHovered(false);
-            return;
-        }
-        if (this.wrapperView && !this.wrapperView.contains(event.target)) {
-            this.setIsHovered(false);
-        }
-    }
-
     render() {
         if (this.props.absolute && React.isValidElement(this.props.children)) {
             return React.cloneElement(React.Children.only(this.props.children), {
diff --git a/src/components/Tooltip/index.js b/src/components/Tooltip/index.js
index 8d3a345492..af46f8154d 100644
--- a/src/components/Tooltip/index.js
+++ b/src/components/Tooltip/index.js
@@ -189,7 +189,6 @@ class Tooltip extends PureComponent {
                     containerStyles={this.props.containerStyles}
                     onHoverIn={this.showTooltip}
                     onHoverOut={this.hideTooltip}
-                    resetsOnClickOutside
                 >
                     {child}
                 </Hoverable>
diff --git a/src/libs/Navigation/AppNavigator/modalCardStyleInterpolator.js b/src/libs/Navigation/AppNavigator/modalCardStyleInterpolator.js
index 1ec48a3563..af49ab82de 100644
--- a/src/libs/Navigation/AppNavigator/modalCardStyleInterpolator.js
+++ b/src/libs/Navigation/AppNavigator/modalCardStyleInterpolator.js
@@ -14,9 +14,9 @@ export default (
         },
     },
 ) => {
-    const translateX = Animated.multiply(progress.interpolate({
+    const right = Animated.multiply(progress.interpolate({
         inputRange: [0, 1],
-        outputRange: [isSmallScreenWidth ? screen.width : variables.sideBarWidth, 0],
+        outputRange: [isSmallScreenWidth ? -screen.width : -variables.sideBarWidth, 0],
         extrapolate: 'clamp',
     }), inverted);

@@ -26,7 +26,7 @@ export default (
     if (isFullScreenModal && !isSmallScreenWidth) {
         cardStyle.opacity = opacity;
     } else {
-        cardStyle.transform = [{translateX}];
+        cardStyle.right = right;
     }

     return ({

@francoisl Indeed, I see the issue on Safari. Basically the use of right over translateX has no effect on Safari.
Related bug (very old): https://bugs.webkit.org/show_bug.cgi?id=4117
Meaning: on Safari the tooltip will hide only if you move the mouse afterwards

Quick Update:
Unfortunately the CSS fix that replace translateX with right to solve the issue that the tooltip stays visible until you move the mouse only works for Chrome and not Safari.
I have found other similar CSS fixes but none worked on Safari. Time to get this fixed using some JS.
I will post my updated proposal in few mins...

Proposal (Updated)
diff --git a/src/components/Hoverable/hoverablePropTypes.js b/src/components/Hoverable/hoverablePropTypes.js
index 07b8a8741e..c17fa804b6 100644
--- a/src/components/Hoverable/hoverablePropTypes.js
+++ b/src/components/Hoverable/hoverablePropTypes.js
@@ -19,9 +19,6 @@ const propTypes = {

     /** Function that executes when the mouse leaves the children. */
     onHoverOut: PropTypes.func,
-
-    // If the mouse clicks outside, should we dismiss hover?
-    resetsOnClickOutside: PropTypes.bool,
 };

 const defaultProps = {
@@ -29,7 +26,6 @@ const defaultProps = {
     containerStyles: [],
     onHoverIn: () => {},
     onHoverOut: () => {},
-    resetsOnClickOutside: false,
 };

 export {
diff --git a/src/components/Hoverable/index.js b/src/components/Hoverable/index.js
index f06ed56027..3fb3d1f59c 100644
--- a/src/components/Hoverable/index.js
+++ b/src/components/Hoverable/index.js
@@ -16,13 +16,9 @@ class Hoverable extends Component {
         };

         this.wrapperView = null;
-
-        this.resetHoverStateOnOutsideClick = this.resetHoverStateOnOutsideClick.bind(this);
     }

     componentDidMount() {
-        document.addEventListener('mousedown', this.resetHoverStateOnOutsideClick);
-
         // we like to Block the hover on touch devices but we keep it for Hybrid devices so
         // following logic blocks hover on touch devices.
         this.disableHover = () => {
@@ -35,12 +31,17 @@ class Hoverable extends Component {

         // Remember Touchend fires before `mouse` events so we have to use alternative.
         document.addEventListener('touchmove', this.enableHover);
+
+        this.hide = () => {
+            this.setIsHovered(false);
+        };
+        document.addEventListener('hidehoverable', this.hide);
     }

     componentWillUnmount() {
-        document.removeEventListener('mousedown', this.resetHoverStateOnOutsideClick);
         document.removeEventListener('touchstart', this.disableHover);
         document.removeEventListener('touchmove', this.enableHover);
+        document.removeEventListener('hidehoverable', this.hide);
     }

     /**
@@ -50,7 +51,12 @@ class Hoverable extends Component {
      */
     setIsHovered(isHovered) {
         if (isHovered !== this.state.isHovered && !(isHovered && this.hoverDisabled)) {
-            this.setState({isHovered}, isHovered ? this.props.onHoverIn : this.props.onHoverOut);
+            this.setState({isHovered});
+            if (isHovered) {
+                this.props.onHoverIn();
+            } else {
+                this.props.onHoverOut();
+            }
         }

         // we reset the Hover block in case touchmove was not first after touctstart
@@ -59,28 +65,6 @@ class Hoverable extends Component {
         }
     }

-    /**
-     * If the user clicks outside this component, we want to make sure that the hover state is set to false.
-     * There are some edge cases where the mouse can leave the component without the `onMouseLeave` event firing,
-     * leaving this Hoverable in the incorrect state.
-     * One such example is when a modal is opened while this component is hovered, and you click outside the component
-     * to close the modal.
-     *
-     * @param {Object} event - A click event
-     */
-    resetHoverStateOnOutsideClick(event) {
-        if (!this.state.isHovered) {
-            return;
-        }
-        if (this.props.resetsOnClickOutside) {
-            this.setIsHovered(false);
-            return;
-        }
-        if (this.wrapperView && !this.wrapperView.contains(event.target)) {
-            this.setIsHovered(false);
-        }
-    }
-
     render() {
         if (this.props.absolute && React.isValidElement(this.props.children)) {
             return React.cloneElement(React.Children.only(this.props.children), {
diff --git a/src/components/Tooltip/index.js b/src/components/Tooltip/index.js
index 8d3a345492..af46f8154d 100644
--- a/src/components/Tooltip/index.js
+++ b/src/components/Tooltip/index.js
@@ -189,7 +189,6 @@ class Tooltip extends PureComponent {
                     containerStyles={this.props.containerStyles}
                     onHoverIn={this.showTooltip}
                     onHoverOut={this.hideTooltip}
-                    resetsOnClickOutside
                 >
                     {child}
                 </Hoverable>
diff --git a/src/libs/Navigation/NavigationRoot.js b/src/libs/Navigation/NavigationRoot.js
index 2c8b4bf5f5..81fc9bb236 100644
--- a/src/libs/Navigation/NavigationRoot.js
+++ b/src/libs/Navigation/NavigationRoot.js
@@ -9,6 +9,7 @@ import FullScreenLoadingIndicator from '../../components/FullscreenLoadingIndica
 import themeColors from '../../styles/themes/default';
 import styles from '../../styles/styles';
 import UnreadIndicatorUpdater from '../UnreadIndicatorUpdater';
+import dispatchEvent from '../dispatchEvent';
 import Log from '../Log';

 // https://reactnavigation.org/docs/themes
@@ -60,7 +61,10 @@ const NavigationRoot = (props) => {
                     style={styles.navigatorFullScreenLoading}
                 />
             )}
-            onStateChange={parseAndLogRoute}
+            onStateChange={(state) => {
+                parseAndLogRoute(state);
+                dispatchEvent('hidehoverable');
+            }}
             onReady={props.onReady}
             theme={navigationTheme}
             ref={navigationRef}
diff --git a/src/libs/dispatchEvent/index.js b/src/libs/dispatchEvent/index.js
new file mode 100644
index<PHONE_NUMBER>..4906e41af0
--- /dev/null
+++ b/src/libs/dispatchEvent/index.js
@@ -0,0 +1,3 @@
+export default function dispatchEvent(event) {
+    document.dispatchEvent(new Event(event));
+}
diff --git a/src/libs/dispatchEvent/index.native.js b/src/libs/dispatchEvent/index.native.js
new file mode 100644
index<PHONE_NUMBER>..22ca7a3f0b
--- /dev/null
+++ b/src/libs/dispatchEvent/index.native.js
@@ -0,0 +1 @@
+export default function dispatchEvent() {}

Update
This updated proposal fixes the issue that the tooltip does not hide unless you move the mouse

Added a dispatchEvent lib
on Hoverable we listen to hidehoverable event and as the callback we set the hover state to false
on NavigationRoot we dispatch the event in case we changed the route, this should cover all the cases where the tooltip is not hiding (basically in modals)

https://user-images.githubusercontent.com/16493223/209357852-62c638a3-bd33-410b-8e56-4e5996ff172a.mp4
cc @mananjadhav @francoisl

@s77rt its still not consistent between chrome and safari

I strongly disagree with @s77rt completely removing code which explains the exact issue we're experiencing currently on it's comment. every code comment is placed on purpose.

@jatinsonijs Can provide an example?
@getusha I assume you are referring to resetHoverStateOnOutsideClick right? As I stated this is a workaround that was introduced to fix #12025 Instead of making another workaround it's better to fix the root issue

that wasn't introduced recently to fix that certain issue but the code was present, i just did the application and modifications to it.

@getusha That's not my point. I'm just saying we should not patch another patch especially that the root cause has been identified

@jatinsonijs Can provide an example?

Global plus button, composer plus button, emoji picker etc...

@jatinsonijs Indeed, on Chrome the tooltip will hide once a modal is shown (not onPress)
This can be fixed by firing the event onModalWillShow

Updated proposal
diff --git a/src/components/Hoverable/hoverablePropTypes.js b/src/components/Hoverable/hoverablePropTypes.js
index 07b8a8741e..c17fa804b6 100644
--- a/src/components/Hoverable/hoverablePropTypes.js
+++ b/src/components/Hoverable/hoverablePropTypes.js
@@ -19,9 +19,6 @@ const propTypes = {

     /** Function that executes when the mouse leaves the children. */
     onHoverOut: PropTypes.func,
-
-    // If the mouse clicks outside, should we dismiss hover?
-    resetsOnClickOutside: PropTypes.bool,
 };

 const defaultProps = {
@@ -29,7 +26,6 @@ const defaultProps = {
     containerStyles: [],
     onHoverIn: () => {},
     onHoverOut: () => {},
-    resetsOnClickOutside: false,
 };

 export {
diff --git a/src/components/Hoverable/index.js b/src/components/Hoverable/index.js
index f06ed56027..3fb3d1f59c 100644
--- a/src/components/Hoverable/index.js
+++ b/src/components/Hoverable/index.js
@@ -16,13 +16,9 @@ class Hoverable extends Component {
         };

         this.wrapperView = null;
-
-        this.resetHoverStateOnOutsideClick = this.resetHoverStateOnOutsideClick.bind(this);
     }

     componentDidMount() {
-        document.addEventListener('mousedown', this.resetHoverStateOnOutsideClick);
-
         // we like to Block the hover on touch devices but we keep it for Hybrid devices so
         // following logic blocks hover on touch devices.
         this.disableHover = () => {
@@ -35,12 +31,17 @@ class Hoverable extends Component {

         // Remember Touchend fires before `mouse` events so we have to use alternative.
         document.addEventListener('touchmove', this.enableHover);
+
+        this.hide = () => {
+            this.setIsHovered(false);
+        };
+        document.addEventListener('hidehoverable', this.hide);
     }

     componentWillUnmount() {
-        document.removeEventListener('mousedown', this.resetHoverStateOnOutsideClick);
         document.removeEventListener('touchstart', this.disableHover);
         document.removeEventListener('touchmove', this.enableHover);
+        document.removeEventListener('hidehoverable', this.hide);
     }

     /**
@@ -50,7 +51,12 @@ class Hoverable extends Component {
      */
     setIsHovered(isHovered) {
         if (isHovered !== this.state.isHovered && !(isHovered && this.hoverDisabled)) {
-            this.setState({isHovered}, isHovered ? this.props.onHoverIn : this.props.onHoverOut);
+            this.setState({isHovered});
+            if (isHovered) {
+                this.props.onHoverIn();
+            } else {
+                this.props.onHoverOut();
+            }
         }

         // we reset the Hover block in case touchmove was not first after touctstart
@@ -59,28 +65,6 @@ class Hoverable extends Component {
         }
     }

-    /**
-     * If the user clicks outside this component, we want to make sure that the hover state is set to false.
-     * There are some edge cases where the mouse can leave the component without the `onMouseLeave` event firing,
-     * leaving this Hoverable in the incorrect state.
-     * One such example is when a modal is opened while this component is hovered, and you click outside the component
-     * to close the modal.
-     *
-     * @param {Object} event - A click event
-     */
-    resetHoverStateOnOutsideClick(event) {
-        if (!this.state.isHovered) {
-            return;
-        }
-        if (this.props.resetsOnClickOutside) {
-            this.setIsHovered(false);
-            return;
-        }
-        if (this.wrapperView && !this.wrapperView.contains(event.target)) {
-            this.setIsHovered(false);
-        }
-    }
-
     render() {
         if (this.props.absolute && React.isValidElement(this.props.children)) {
             return React.cloneElement(React.Children.only(this.props.children), {
diff --git a/src/components/Modal/BaseModal.js b/src/components/Modal/BaseModal.js
index 323df19c77..9deee2073a 100644
--- a/src/components/Modal/BaseModal.js
+++ b/src/components/Modal/BaseModal.js
@@ -8,6 +8,7 @@ import * as StyleUtils from '../../styles/StyleUtils';
 import themeColors from '../../styles/themes/default';
 import {propTypes as modalPropTypes, defaultProps as modalDefaultProps} from './modalPropTypes';
 import * as Modal from '../../libs/actions/Modal';
+import dispatchEvent from '../../libs/dispatchEvent';
 import getModalStyles from '../../styles/getModalStyles';
 import variables from '../../styles/variables';

@@ -96,6 +97,7 @@ class BaseModal extends PureComponent {
                     }
                     this.props.onModalShow();
                 }}
+                onModalWillShow={() => dispatchEvent('hidehoverable')}
                 propagateSwipe={this.props.propagateSwipe}
                 onModalHide={this.hideModal}
                 onSwipeComplete={this.props.onClose}
diff --git a/src/components/Tooltip/index.js b/src/components/Tooltip/index.js
index 8d3a345492..af46f8154d 100644
--- a/src/components/Tooltip/index.js
+++ b/src/components/Tooltip/index.js
@@ -189,7 +189,6 @@ class Tooltip extends PureComponent {
                     containerStyles={this.props.containerStyles}
                     onHoverIn={this.showTooltip}
                     onHoverOut={this.hideTooltip}
-                    resetsOnClickOutside
                 >
                     {child}
                 </Hoverable>
diff --git a/src/libs/Navigation/NavigationRoot.js b/src/libs/Navigation/NavigationRoot.js
index 2c8b4bf5f5..81fc9bb236 100644
--- a/src/libs/Navigation/NavigationRoot.js
+++ b/src/libs/Navigation/NavigationRoot.js
@@ -9,6 +9,7 @@ import FullScreenLoadingIndicator from '../../components/FullscreenLoadingIndica
 import themeColors from '../../styles/themes/default';
 import styles from '../../styles/styles';
 import UnreadIndicatorUpdater from '../UnreadIndicatorUpdater';
+import dispatchEvent from '../dispatchEvent';
 import Log from '../Log';

 // https://reactnavigation.org/docs/themes
@@ -60,7 +61,10 @@ const NavigationRoot = (props) => {
                     style={styles.navigatorFullScreenLoading}
                 />
             )}
-            onStateChange={parseAndLogRoute}
+            onStateChange={(state) => {
+                parseAndLogRoute(state);
+                dispatchEvent('hidehoverable');
+            }}
             onReady={props.onReady}
             theme={navigationTheme}
             ref={navigationRef}
diff --git a/src/libs/dispatchEvent/index.js b/src/libs/dispatchEvent/index.js
new file mode 100644
index<PHONE_NUMBER>..4906e41af0
--- /dev/null
+++ b/src/libs/dispatchEvent/index.js
@@ -0,0 +1,3 @@
+export default function dispatchEvent(event) {
+    document.dispatchEvent(new Event(event));
+}
diff --git a/src/libs/dispatchEvent/index.native.js b/src/libs/dispatchEvent/index.native.js
new file mode 100644
index<PHONE_NUMBER>..22ca7a3f0b
--- /dev/null
+++ b/src/libs/dispatchEvent/index.native.js
@@ -0,0 +1 @@
+export default function dispatchEvent() {}

So far I think the behaviour is the same on Chrome and Safari. I don't really like the idea of calling dispatchEvent('hidehoverable') in multiple places, but if it's just on the navigation root and the modal, I think it's not that bad

Proposal
The occurrence of the successText variable, which represents a change in the state of the tooltip text, triggers the necessary action of passing it to the Tooltip component or to the Hoverable component at a later stage in the process.

diff --git a/src/components/ContextMenuItem.js b/src/components/ContextMenuItem.js
index c87fc5e9b4..e981bad8d6 100644
--- a/src/components/ContextMenuItem.js
+++ b/src/components/ContextMenuItem.js
@@ -9,6 +9,7 @@ import * as StyleUtils from '../styles/StyleUtils';
 import getButtonState from '../libs/getButtonState';
 import withDelayToggleButtonState, {withDelayToggleButtonStatePropTypes} from './withDelayToggleButtonState';
 import variables from '../styles/variables';

 const propTypes = {
     /** Icon Component */
@@ -75,7 +76,7 @@ class ContextMenuItem extends Component {
         return (
             this.props.isMini
                 ? (
-                    <Tooltip text={text}>
+                    <Tooltip text={text} successText={this.props.successText}>
                         <Pressable
                             focusable
                             accessibilityLabel={text}

diff --git a/src/components/Tooltip/index.js b/src/components/Tooltip/index.js
index 8d3a345492..4ed8827df8 100644
--- a/src/components/Tooltip/index.js
+++ b/src/components/Tooltip/index.js
@@ -190,6 +190,7 @@ class Tooltip extends PureComponent {
                     onHoverIn={this.showTooltip}
                     onHoverOut={this.hideTooltip}
                     resetsOnClickOutside
+                    successText={this.props.successText}
                 >
                     {child}
                 </Hoverable>

If the value of the successText variable is present (indicating a state change of the tooltip text), set the value of the isHovered variable to true in order to display a different message within the tooltip.
diff --git a/src/components/Hoverable/index.js b/src/components/Hoverable/index.js
index f06ed56027..d52927e3a3 100644
--- a/src/components/Hoverable/index.js
+++ b/src/components/Hoverable/index.js
@@ -72,6 +72,10 @@ class Hoverable extends Component {
         if (!this.state.isHovered) {
             return;
         }
+        if(this.props.successText){
+            this.setIsHovered(true);
+            return;
+        }
         if (this.props.resetsOnClickOutside) {
             this.setIsHovered(false);
             return;

Result
https://user-images.githubusercontent.com/75031127/209375489-64654ee0-159e-4aee-a42a-f9eb8980d13c.mov

Please NOTE: @rushatgabhane have fixed a regressions related to this https://github.com/Expensify/App/issues/12025 after the solution. so I am so against removing the code.

The above solution fixes the issue described completely and i haven't experienced a regression related to this so far.
cc @mananjadhav @francoisl  @rushatgabhane

@getusha Your solution looks clean. But won't this create a future regression? If a you click a button that have successText and that button changes the route. You will recreate https://github.com/Expensify/App/issues/12025

I don't think so. @s77rt

@getusha You are basically disabling resetHoverStateOnOutsideClick so I think it will recreate the issue.

I don't think we will have something like navigation with successText that's so weird, because the user don't have the chance to see the changed tooltip text if the user is unable to see the changed text? doesn't make any sense to me.
If you can produce any regressions at the current state please let me know otherwise let's hear from C+ reviewers please. thanks.

Proposal
Since we have few tooltips that have text change when we click onside them (Pin/Unpin , Copied/copyToClipboard) , we can reverse the logic that reset click outside to prevent reset o'clock onside for those component :
/src/components/Hoverable/index.js
diff --git a/src/components/Hoverable/index.js b/src/components/Hoverable/index.js
index f06ed56027..965391361a 100644
--- a/src/components/Hoverable/index.js
+++ b/src/components/Hoverable/index.js
@@ -69,16 +69,10 @@ class Hoverable extends Component {
      * @param {Object} event - A click event
      */
     resetHoverStateOnOutsideClick(event) {
-        if (!this.state.isHovered) {
+        if (!this.state.isHovered || (this.props.preventResetOnClickOnside && this.wrapperView && this.wrapperView.contains(event.target))) {
             return;
         }
-        if (this.props.resetsOnClickOutside) {
-            this.setIsHovered(false);
-            return;
-        }
-        if (this.wrapperView && !this.wrapperView.contains(event.target)) {
-            this.setIsHovered(false);
-        }
+        this.setIsHovered(false);
     }

src/components/Hoverable/hoverablePropTypes.js
diff --git a/src/components/Hoverable/hoverablePropTypes.js b/src/components/Hoverable/hoverablePropTypes.js
index 07b8a8741e..f9f317064f 100644
--- a/src/components/Hoverable/hoverablePropTypes.js
+++ b/src/components/Hoverable/hoverablePropTypes.js
@@ -21,7 +21,7 @@ const propTypes = {
     onHoverOut: PropTypes.func,

     // If the mouse clicks outside, should we dismiss hover?
-    resetsOnClickOutside: PropTypes.bool,
+    preventResetOnClickOnside: PropTypes.bool,
 };

 const defaultProps = {
@@ -29,7 +29,7 @@ const defaultProps = {
     containerStyles: [],
     onHoverIn: () => {},
     onHoverOut: () => {},
-    resetsOnClickOutside: false,
+    preventResetOnClickOnside: false,
 };

/src/components/Tooltip/index.js
diff --git a/src/components/Tooltip/index.js b/src/components/Tooltip/index.js
index 8d3a345492..274bc56de7 100644
--- a/src/components/Tooltip/index.js
+++ b/src/components/Tooltip/index.js
@@ -189,7 +189,8 @@ class Tooltip extends PureComponent {
                     containerStyles={this.props.containerStyles}
                     onHoverIn={this.showTooltip}
                     onHoverOut={this.hideTooltip}
-                    resetsOnClickOutside
+                    preventResetOnClickOnside={this.props.preventResetOnClickOnside}
                 >
                     {child}
                 </Hoverable>

/src/components/Tooltip/tooltipPropTypes.js
diff --git a/src/components/Tooltip/tooltipPropTypes.js b/src/components/Tooltip/tooltipPropTypes.js
index 627f1c0171..df2ccff792 100644
--- a/src/components/Tooltip/tooltipPropTypes.js
+++ b/src/components/Tooltip/tooltipPropTypes.js
@@ -32,6 +32,8 @@ const propTypes = {

     /** Maximum number of lines to show in tooltip */
     numberOfLines: PropTypes.number,
+
+    preventResetOnClickOnside: PropTypes.bool,
 };

 const defaultProps = {
@@ -42,6 +44,7 @@ const defaultProps = {
     text: '',
     maxWidth: variables.sideBarWidth,
     numberOfLines: CONST.TOOLTIP_MAX_LINES,
+    preventResetOnClickOnside: false,
 };

/src/pages/home/report/ReportActionItem.js
diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js
index 7158f15f37..88fe471f23 100644
--- a/src/pages/home/report/ReportActionItem.js
+++ b/src/pages/home/report/ReportActionItem.js
@@ -184,7 +184,7 @@ class ReportActionItem extends Component {
                 onSecondaryInteraction={this.showPopover}
                 preventDefaultContentMenu={!this.props.draftMessage}
             >
-                <Hoverable>
+                <Hoverable preventResetOnClickOnside>
                     {hovered => (
                         <View accessibilityLabel="Chat message">
                             {this.props.shouldDisplayNewIndicator && (

/src/components/ContextMenuItem.js
diff --git a/src/components/ContextMenuItem.js b/src/components/ContextMenuItem.js
index c87fc5e9b4..f1932a8a75 100644
--- a/src/components/ContextMenuItem.js
+++ b/src/components/ContextMenuItem.js
@@ -75,7 +75,7 @@ class ContextMenuItem extends Component {
         return (
             this.props.isMini
                 ? (
-                    <Tooltip text={text}>
+                    <Tooltip preventResetOnClickOnside text={text}>
                         <Pressable
                             focusable
                             accessibilityLabel={text}

/src/components/CopyTextToClipboard.js
diff --git a/src/components/CopyTextToClipboard.js b/src/components/CopyTextToClipboard.js
index 594e696361..ab4a81032c 100644
--- a/src/components/CopyTextToClipboard.js
+++ b/src/components/CopyTextToClipboard.js
@@ -59,7 +59,10 @@ class CopyTextToClipboard extends React.Component {
                 suppressHighlighting
             >
                 <Text style={this.props.textStyles}>{this.props.text}</Text>
-                <Tooltip text={this.props.translate(`reportActionContextMenu.${this.state.showCheckmark ? 'copied' : 'copyToClipboard'}`)}>
+                <Tooltip
+                    preventResetOnClickOnside
+                    text={this.props.translate(`reportActionContextMenu.${this.state.showCheckmark ? 'copied' : 'copyToClipboard'}`)}
+                >
                     <Icon
                         src={this.state.showCheckmark ? Expensicons.Checkmark : Expensicons.Clipboard}
                         fill={this.state.showCheckmark ? themeColors.iconSuccessFill : themeColors.icon}

I don't think we will have something like navigation with successText that's so weird,

@getusha  I can't be so sure about this and if this assumption is correct, then I would just set resetsOnClickOutside=false and be done with it.
But I did try applying your proposal and it seems to be working. I think this worked for the cases I tried.
@fedirjh That's a good insight. This makes sense. I am going to review your proposal either today or Monday and update here.
Thank you so much for the continuous engagement on this team! and Happy Holidays!

@mananjadhav, @fedirjh solution is same as resetsOnClickOutside false but in reverse order. So same case apply on it also. Navigation + toggled tooltip will not work.

Proposal
We should combine resetsOnClickOutside and navigate state change. In this case it will handle both cases normal tooltips will work as it working now, toggled tooltip + navigation and modals. For toggled tooltip we have to pass resetsOnClickOutside false. And on screen changes we can hide tooltip by navigate state change listener. Not need to create custom events we can use react-navigation listener.
I have just added on one for now to show example we can add resetsOnClickOutside on all toggled tooltips. There is only few tooltips which required it.
diff --git a/src/components/ContextMenuItem.js b/src/components/ContextMenuItem.js
index c87fc5e9b4..c28c43dc0c 100644
--- a/src/components/ContextMenuItem.js
+++ b/src/components/ContextMenuItem.js
@@ -75,7 +75,7 @@ class ContextMenuItem extends Component {
         return (
             this.props.isMini
                 ? (
-                    <Tooltip text={text}>
+                    <Tooltip text={text} resetsOnClickOutside={false}>
                         <Pressable
                             focusable
                             accessibilityLabel={text}
diff --git a/src/components/Hoverable/index.js b/src/components/Hoverable/index.js
index f06ed56027..f1a5f47abf 100644
--- a/src/components/Hoverable/index.js
+++ b/src/components/Hoverable/index.js
@@ -2,6 +2,7 @@ import _ from 'underscore';
 import React, {Component} from 'react';
 import {View} from 'react-native';
 import {propTypes, defaultProps} from './hoverablePropTypes';
+import navigationRef from '../../libs/Navigation/navigationRef';

 /**
  * It is necessary to create a Hoverable component instead of relying solely on Pressable support for hover state,
@@ -16,6 +17,7 @@ class Hoverable extends Component {
         };

         this.wrapperView = null;
+        this.navigatorUnsubscribe = null;

         this.resetHoverStateOnOutsideClick = this.resetHoverStateOnOutsideClick.bind(this);
     }
@@ -35,12 +37,22 @@ class Hoverable extends Component {

         // Remember Touchend fires before `mouse` events so we have to use alternative.
         document.addEventListener('touchmove', this.enableHover);
+
+        if (navigationRef) {
+            this.navigatorUnsubscribe = navigationRef.addListener('state', () => {
+                this.setIsHovered(false);
+            });
+        }
     }

     componentWillUnmount() {
         document.removeEventListener('mousedown', this.resetHoverStateOnOutsideClick);
         document.removeEventListener('touchstart', this.disableHover);
         document.removeEventListener('touchmove', this.enableHover);
+
+        if (this.navigatorUnsubscribe) {
+            this.navigatorUnsubscribe();
+        }
     }

     /**
diff --git a/src/components/Tooltip/index.js b/src/components/Tooltip/index.js
index 8d3a345492..9ebe73ef21 100644
--- a/src/components/Tooltip/index.js
+++ b/src/components/Tooltip/index.js
@@ -189,7 +189,7 @@ class Tooltip extends PureComponent {
                     containerStyles={this.props.containerStyles}
                     onHoverIn={this.showTooltip}
                     onHoverOut={this.hideTooltip}
-                    resetsOnClickOutside
+                    resetsOnClickOutside={this.props.resetsOnClickOutside}
                 >
                     {child}
                 </Hoverable>
diff --git a/src/components/Tooltip/tooltipPropTypes.js b/src/components/Tooltip/tooltipPropTypes.js
index 627f1c0171..dfd97a75f9 100644
--- a/src/components/Tooltip/tooltipPropTypes.js
+++ b/src/components/Tooltip/tooltipPropTypes.js
@@ -32,6 +32,9 @@ const propTypes = {

     /** Maximum number of lines to show in tooltip */
     numberOfLines: PropTypes.number,
+
+    // If the mouse clicks outside, should we dismiss hover?
+    resetsOnClickOutside: PropTypes.bool,
 };

 const defaultProps = {
@@ -42,6 +45,7 @@ const defaultProps = {
     text: '',
     maxWidth: variables.sideBarWidth,
     numberOfLines: CONST.TOOLTIP_MAX_LINES,
+    resetsOnClickOutside: true,
 };

 export {

Optionally we can change name of resetsOnClickOutside tooltip prop to dismissOnInsideClick.

I don't think we will have something like navigation with successText that's so weird,

@getusha I can't be so sure about this and if this assumption is correct, then I would just set resetsOnClickOutside=false and be done with it. But I did try applying your proposal and it seems to be working. I think this worked for the cases I tried.
@fedirjh That's a good insight. This makes sense. I am going to review your proposal either today or Monday and update here.
Thank you so much for the continuous engagement on this team! and Happy Holidays!

@mananjadhav to break it down. there might be a navigation without successText which is obvious and if we set resetOnClickOutside false in general, it might create issues described in the comment. but when we have successText and apply my solution the chance that we want to show the updated tooltip let's say while navigating is 0, it is not happening IMO. because if the user is unable to see the updated tooltip as i said it's pointless right?

@getusha Navigation was just an example, the issue will happen if Freeze is triggered in any other ways.
I like your proposal being simple but you are basically reverting the PR mentioned above (without providing an alt solution). I think that your proposal will likely cause future regressions. Besides it's not global (and it's impossible to make it global), we will have to pass successText={this.props.successText} to every Tooltip that may change it's text.
I have been working on another proposal (following another approach) that should work exactly as expected and hopefully it's bullet-proof. Posting in few mins...

@s77rt if you have real regressions that would be great, rather than just assuming

Proposal 2
diff --git a/src/components/ContextMenuItem.js b/src/components/ContextMenuItem.js
index c87fc5e9b4..b7732eb033 100644
--- a/src/components/ContextMenuItem.js
+++ b/src/components/ContextMenuItem.js
@@ -75,7 +75,7 @@ class ContextMenuItem extends Component {
         return (
             this.props.isMini
                 ? (
-                    <Tooltip text={text}>
+                    <Tooltip text={text} shouldResetOnMouseDown shouldResetOnMouseUp>
                         <Pressable
                             focusable
                             accessibilityLabel={text}
diff --git a/src/components/Hoverable/hoverablePropTypes.js b/src/components/Hoverable/hoverablePropTypes.js
index 07b8a8741e..4f82dbdc6f 100644
--- a/src/components/Hoverable/hoverablePropTypes.js
+++ b/src/components/Hoverable/hoverablePropTypes.js
@@ -20,8 +20,11 @@ const propTypes = {
     /** Function that executes when the mouse leaves the children. */
     onHoverOut: PropTypes.func,

-    // If the mouse clicks outside, should we dismiss hover?
-    resetsOnClickOutside: PropTypes.bool,
+    /* On mouse down, should we reset the hover state? */
+    shouldResetOnMouseDown: PropTypes.bool,
+
+    /* On mouse up, should we reset the hover state? */
+    shouldResetOnMouseUp: PropTypes.bool,
 };

 const defaultProps = {
@@ -29,7 +32,8 @@ const defaultProps = {
     containerStyles: [],
     onHoverIn: () => {},
     onHoverOut: () => {},
-    resetsOnClickOutside: false,
+    shouldResetOnMouseDown: false,
+    shouldResetOnMouseUp: false,
 };

 export {
diff --git a/src/components/Hoverable/index.js b/src/components/Hoverable/index.js
index f06ed56027..d21c6a6118 100644
--- a/src/components/Hoverable/index.js
+++ b/src/components/Hoverable/index.js
@@ -1,6 +1,6 @@
 import _ from 'underscore';
 import React, {Component} from 'react';
-import {View} from 'react-native';
+import {View, InteractionManager} from 'react-native';
 import {propTypes, defaultProps} from './hoverablePropTypes';

 /**
@@ -17,11 +17,13 @@ class Hoverable extends Component {

         this.wrapperView = null;

-        this.resetHoverStateOnOutsideClick = this.resetHoverStateOnOutsideClick.bind(this);
+        this.resetHoverStateOnMouseDown = this.resetHoverStateOnMouseDown.bind(this);
+        this.resetHoverStateOnMouseUp = this.resetHoverStateOnMouseUp.bind(this);
     }

     componentDidMount() {
-        document.addEventListener('mousedown', this.resetHoverStateOnOutsideClick);
+        document.addEventListener('mousedown', this.resetHoverStateOnMouseDown);
+        document.addEventListener('mouseup', this.resetHoverStateOnMouseUp);

         // we like to Block the hover on touch devices but we keep it for Hybrid devices so
         // following logic blocks hover on touch devices.
@@ -38,7 +40,8 @@ class Hoverable extends Component {
     }

     componentWillUnmount() {
-        document.removeEventListener('mousedown', this.resetHoverStateOnOutsideClick);
+        document.removeEventListener('mousedown', this.resetHoverStateOnMouseDown);
+        document.removeEventListener('mouseup', this.resetHoverStateOnMouseUp);
         document.removeEventListener('touchstart', this.disableHover);
         document.removeEventListener('touchmove', this.enableHover);
     }
@@ -66,13 +69,13 @@ class Hoverable extends Component {
      * One such example is when a modal is opened while this component is hovered, and you click outside the component
      * to close the modal.
      *
-     * @param {Object} event - A click event
+     * @param {Object} event - A mouse event
      */
-    resetHoverStateOnOutsideClick(event) {
+    resetHoverStateOnMouseDown(event) {
         if (!this.state.isHovered) {
             return;
         }
-        if (this.props.resetsOnClickOutside) {
+        if (this.props.shouldResetOnMouseDown) {
             this.setIsHovered(false);
             return;
         }
@@ -81,6 +84,23 @@ class Hoverable extends Component {
         }
     }

+    /**
+     * @param {Object} event - A mouse event
+     */
+    resetHoverStateOnMouseUp(event) {
+        if (this.state.isHovered) {
+            return;
+        }
+        if (this.props.shouldResetOnMouseUp) {
+            InteractionManager.runAfterInteractions(() => {
+                const e = document.elementFromPoint(event.clientX, event.clientY);
+                if (this.wrapperView && this.wrapperView.matches(':hover') && (this.wrapperView.contains(e) || this.wrapperView.isEqualNode(e))) {
+                    this.setIsHovered(true);
+                }
+            });
+        }
+    }
+
     render() {
         if (this.props.absolute && React.isValidElement(this.props.children)) {
             return React.cloneElement(React.Children.only(this.props.children), {
diff --git a/src/components/Tooltip/index.js b/src/components/Tooltip/index.js
index 8d3a345492..d30d210c1a 100644
--- a/src/components/Tooltip/index.js
+++ b/src/components/Tooltip/index.js
@@ -189,7 +189,8 @@ class Tooltip extends PureComponent {
                     containerStyles={this.props.containerStyles}
                     onHoverIn={this.showTooltip}
                     onHoverOut={this.hideTooltip}
-                    resetsOnClickOutside
+                    shouldResetOnMouseDown={this.props.shouldResetOnMouseDown}
+                    shouldResetOnMouseUp={this.props.shouldResetOnMouseUp}
                 >
                     {child}
                 </Hoverable>
diff --git a/src/components/Tooltip/tooltipPropTypes.js b/src/components/Tooltip/tooltipPropTypes.js
index 627f1c0171..f2920c48d8 100644
--- a/src/components/Tooltip/tooltipPropTypes.js
+++ b/src/components/Tooltip/tooltipPropTypes.js
@@ -32,6 +32,12 @@ const propTypes = {

     /** Maximum number of lines to show in tooltip */
     numberOfLines: PropTypes.number,
+
+    /* On mouse down, should we reset the hover state? (Hoverable) */
+    shouldResetOnMouseDown: PropTypes.bool,
+
+    /* On mouse up, should we reset the hover state? (Hoverable) */
+    shouldResetOnMouseUp: PropTypes.bool,
 };

 const defaultProps = {
@@ -42,6 +48,8 @@ const defaultProps = {
     text: '',
     maxWidth: variables.sideBarWidth,
     numberOfLines: CONST.TOOLTIP_MAX_LINES,
+    shouldResetOnMouseDown: true,
+    shouldResetOnMouseUp: false,
 };

 export {

Details
If we are going to keep the mousedown event, then I have a proposal that introduce mouseup event to negate the side effects of the mousedown event. I will break my proposal into two parts:
Main Change:

Removed resetsOnClickOutside prop and resetHoverStateOnOutsideClick method from Hoverable
Added resetHoverStateOnMouseDown and resetHoverStateOnMouseUp methods to Hoverable
Added shouldResetOnMouseDown and shouldResetOnMouseUp props to Hoverable

Extra / Optimisation Change:

Added shouldResetOnMouseDown and shouldResetOnMouseUp props to Tooltip
On ContextMenuItem pass shouldResetOnMouseUp as prop to Tooltip

The Main Change is enough to fix the issue, basically we will reset the hover state to true if we are hovering the element. We do the condition check only after interactions are done to avoid tooltip being stuck while animations are running (as in changing routes) <-- That's all
The Extra / Optimisation Change has been only introduced to make the changes as little as possible, by explicitly passing shouldResetOnMouseUp to the effected Tooltips.

If we want a global solution, we can apply just the main change and pass both shouldResetOnMouseDown and shouldResetOnMouseUp props to Hoverable in Tooltip ( I have tested this and it works fine )
If we want to play it safe and make as little changes as possible then we can apply both parts of my proposal

@s77rt it is hacky that you're using this.wrapperView.matches(':hover') in my opinion
and lastly we're just setting the isHovered: true which is very very concerning based on your general assumptions.

@getusha I don't think you know how to classify hacky solutions. is this.wrapperView.contains(e) hacky as well?
We set isHovered: true because it is.

@s77rt your solution will not work with safari bcz safari do not change hover state also until mouse move I have already tested it.

@getusha I don't think you know how to classify hacky solutions. is this.wrapperView.contains(e) hacky as well? We set isHovered: true because it is.

Cool, Good luck! 🙂

@jatinsonijs Can you upload a video? I have tested it already, maybe i need to double check

have to created a scenario toggled tooltip + navigation ?

@jatinsonijs You mean a tooltip that has successText and also do navigation?

Yes @s77rt

@jatinsonijs Like that?
https://user-images.githubusercontent.com/16493223/209439791-2d4e0afc-306c-4e07-ad71-086729829c89.mp4

@s77rt just wondering have you seen thecopied tooltip text while doing that? if not also do you think it was necessary?
I don't need the answer just to make sure you will add it into your consideration.

@getusha https://github.com/Expensify/App/issues/13146#issuecomment-1364528668

@s77rt
https://user-images.githubusercontent.com/114683042/209440004-3159d420-9c1d-461c-8b82-b886a5f323db.mov

@jatinsonijs I see the problem Thanks! Shouldn't be hard to fix. I hope

Proposal 2 (Updated)
diff --git a/src/components/Hoverable/hoverablePropTypes.js b/src/components/Hoverable/hoverablePropTypes.js
index 07b8a8741e..4f82dbdc6f 100644
--- a/src/components/Hoverable/hoverablePropTypes.js
+++ b/src/components/Hoverable/hoverablePropTypes.js
@@ -20,8 +20,11 @@ const propTypes = {
     /** Function that executes when the mouse leaves the children. */
     onHoverOut: PropTypes.func,

-    // If the mouse clicks outside, should we dismiss hover?
-    resetsOnClickOutside: PropTypes.bool,
+    /* On mouse down, should we reset the hover state? */
+    shouldResetOnMouseDown: PropTypes.bool,
+
+    /* On mouse up, should we reset the hover state? */
+    shouldResetOnMouseUp: PropTypes.bool,
 };

 const defaultProps = {
@@ -29,7 +32,8 @@ const defaultProps = {
     containerStyles: [],
     onHoverIn: () => {},
     onHoverOut: () => {},
-    resetsOnClickOutside: false,
+    shouldResetOnMouseDown: false,
+    shouldResetOnMouseUp: false,
 };

 export {
diff --git a/src/components/Hoverable/index.js b/src/components/Hoverable/index.js
index f06ed56027..9cf0a77e19 100644
--- a/src/components/Hoverable/index.js
+++ b/src/components/Hoverable/index.js
@@ -1,6 +1,6 @@
 import _ from 'underscore';
 import React, {Component} from 'react';
-import {View} from 'react-native';
+import {View, InteractionManager} from 'react-native';
 import {propTypes, defaultProps} from './hoverablePropTypes';

 /**
@@ -18,6 +18,9 @@ class Hoverable extends Component {
         this.wrapperView = null;

         this.resetHoverStateOnOutsideClick = this.resetHoverStateOnOutsideClick.bind(this);
+
+        this.resetHoverStateOnMouseDown = this.resetHoverStateOnMouseDown.bind(this);
+        this.resetHoverStateOnMouseUp = this.resetHoverStateOnMouseUp.bind(this);
     }

     componentDidMount() {
@@ -66,21 +69,43 @@ class Hoverable extends Component {
      * One such example is when a modal is opened while this component is hovered, and you click outside the component
      * to close the modal.
      *
-     * @param {Object} event - A click event
+     * @param {Object} event - A mouse event
      */
     resetHoverStateOnOutsideClick(event) {
         if (!this.state.isHovered) {
             return;
         }
-        if (this.props.resetsOnClickOutside) {
+        if (this.wrapperView && !this.wrapperView.contains(event.target)) {
             this.setIsHovered(false);
+        }
+    }
+
+    resetHoverStateOnMouseDown() {
+        if (!this.state.isHovered) {
             return;
         }
-        if (this.wrapperView && !this.wrapperView.contains(event.target)) {
+        if (this.props.shouldResetOnMouseDown) {
             this.setIsHovered(false);
         }
     }

+    /**
+     * @param {Object} event - A mouse event
+     */
+    resetHoverStateOnMouseUp(event) {
+        if (this.state.isHovered) {
+            return;
+        }
+        if (this.props.shouldResetOnMouseUp) {
+            InteractionManager.runAfterInteractions(() => {
+                const e = document.elementFromPoint(event.clientX, event.clientY);
+                if (this.wrapperView && (this.wrapperView.contains(e) || this.wrapperView.isEqualNode(e))) {
+                    this.setIsHovered(true);
+                }
+            });
+        }
+    }
+
     render() {
         if (this.props.absolute && React.isValidElement(this.props.children)) {
             return React.cloneElement(React.Children.only(this.props.children), {
@@ -95,6 +120,8 @@ class Hoverable extends Component {
                 },
                 onMouseEnter: () => this.setIsHovered(true),
                 onMouseLeave: () => this.setIsHovered(false),
+                onMouseDown: this.resetHoverStateOnMouseDown,
+                onMouseUp: this.resetHoverStateOnMouseUp,
             });
         }
         return (
@@ -103,6 +130,8 @@ class Hoverable extends Component {
                 ref={el => this.wrapperView = el}
                 onMouseEnter={() => this.setIsHovered(true)}
                 onMouseLeave={() => this.setIsHovered(false)}
+                onMouseDown={this.resetHoverStateOnMouseDown}
+                onMouseUp={this.resetHoverStateOnMouseUp}
             >
                 { // If this.props.children is a function, call it to provide the hover state to the children.
                     _.isFunction(this.props.children)
diff --git a/src/components/Tooltip/index.js b/src/components/Tooltip/index.js
index 8d3a345492..1dc7a669e5 100644
--- a/src/components/Tooltip/index.js
+++ b/src/components/Tooltip/index.js
@@ -189,7 +189,8 @@ class Tooltip extends PureComponent {
                     containerStyles={this.props.containerStyles}
                     onHoverIn={this.showTooltip}
                     onHoverOut={this.hideTooltip}
-                    resetsOnClickOutside
+                    shouldResetOnMouseDown
+                    shouldResetOnMouseUp
                 >
                     {child}
                 </Hoverable>

Update
Same as the initial proposal just re-organised the code and made it global.
Pros

Fixes this issue globally
No race conditions
Takes into consideration future use cases (dynamic tooltip with navigation)

Cons

If you click a button that has a tooltip and this button opens a modal, the tooltip will stay visible until you move the mouse. This is more noticeable on small screens. FWIW I can fix this on Chrome (All Screens) and on Safari (Big Screens Only). Leaving this issue only for Safari (Small Screens)

@mananjadhav @francoisl Thoughts?

I have revised my explanation to improve clarity and addressed my concerns. I hope this provides a clearer understanding.
https://github.com/Expensify/App/issues/13146#issuecomment-1364460209

I have updated my proposal (fixed the mentioned cons)

I have updated my proposal (fixed the mentioned cons)

@s77rt there is still some issues with it. Weird behaviour of tooltip.

As I understood we have following solutions:

Normal tooltip already working fine.
Toggled tooltip can be handle with resetHoverStateOnOutsideClick false.
Creating issue when toggled tooltip + navigation or any action which present UI. Which is not happening bcz we not have anything like this for now. In future it can happen.
Toggled tooltip: resetHoverStateOnOutsideClick false + navigation state listener will solve all above cases.
exclude where toggled tooltip + UI present by Modal, Popover, Or position absolute. It should be enough for this issue.

I think we should not make it complex. We don't know it will really needed or not in future for such cases. If it's needed we can add a utility function and can give control to consumer whenever they want to hide tooltip they can hide. Consumer can hide by reference or we can create listener. extending my above proposal.
diff --git a/src/components/ContextMenuItem.js b/src/components/ContextMenuItem.js
index c87fc5e9b4..c28c43dc0c 100644
--- a/src/components/ContextMenuItem.js
+++ b/src/components/ContextMenuItem.js
@@ -75,7 +75,7 @@ class ContextMenuItem extends Component {
         return (
             this.props.isMini
                 ? (
-                    <Tooltip text={text}>
+                    <Tooltip text={text} resetsOnClickOutside={false}>
                         <Pressable
                             focusable
                             accessibilityLabel={text}
diff --git a/src/components/Hoverable/index.js b/src/components/Hoverable/index.js
index f06ed56027..b89c1999f7 100644
--- a/src/components/Hoverable/index.js
+++ b/src/components/Hoverable/index.js
@@ -1,7 +1,8 @@
 import _ from 'underscore';
 import React, {Component} from 'react';
-import {View} from 'react-native';
+import {View, DeviceEventEmitter} from 'react-native';
 import {propTypes, defaultProps} from './hoverablePropTypes';
+import navigationRef from '../../libs/Navigation/navigationRef';

 /**
  * It is necessary to create a Hoverable component instead of relying solely on Pressable support for hover state,
@@ -16,8 +17,11 @@ class Hoverable extends Component {
         };

         this.wrapperView = null;
+        this.navigatorUnsubscribe = null;
+        this.hideTooltipUnsubscribe = null;

         this.resetHoverStateOnOutsideClick = this.resetHoverStateOnOutsideClick.bind(this);
+        this.hideTooltip = this.hideTooltip.bind(this);
     }

     componentDidMount() {
@@ -35,12 +39,25 @@ class Hoverable extends Component {

         // Remember Touchend fires before `mouse` events so we have to use alternative.
         document.addEventListener('touchmove', this.enableHover);
+
+        this.hideTooltipUnsubscribe = DeviceEventEmitter.addListener('hide_tooltip', this.hideTooltip);
+
+        if (navigationRef && !this.props.resetsOnClickOutside) {
+            this.navigatorUnsubscribe = navigationRef.addListener('state', this.hideTooltip);
+        }
     }

     componentWillUnmount() {
         document.removeEventListener('mousedown', this.resetHoverStateOnOutsideClick);
         document.removeEventListener('touchstart', this.disableHover);
         document.removeEventListener('touchmove', this.enableHover);
+
+        if (this.navigatorUnsubscribe) {
+            this.navigatorUnsubscribe();
+        }
+        if (this.hideTooltipUnsubscribe) {
+            this.hideTooltipUnsubscribe.remove();
+        }
     }

     /**
@@ -59,6 +76,10 @@ class Hoverable extends Component {
         }
     }

+    hideTooltip() {
+        this.setIsHovered(false);
+    }
+
     /**
      * If the user clicks outside this component, we want to make sure that the hover state is set to false.
      * There are some edge cases where the mouse can leave the component without the `onMouseLeave` event firing,
diff --git a/src/components/Tooltip/index.js b/src/components/Tooltip/index.js
index 8d3a345492..9ebe73ef21 100644
--- a/src/components/Tooltip/index.js
+++ b/src/components/Tooltip/index.js
@@ -189,7 +189,7 @@ class Tooltip extends PureComponent {
                     containerStyles={this.props.containerStyles}
                     onHoverIn={this.showTooltip}
                     onHoverOut={this.hideTooltip}
-                    resetsOnClickOutside
+                    resetsOnClickOutside={this.props.resetsOnClickOutside}
                 >
                     {child}
                 </Hoverable>
diff --git a/src/components/Tooltip/tooltipPropTypes.js b/src/components/Tooltip/tooltipPropTypes.js
index 627f1c0171..dfd97a75f9 100644
--- a/src/components/Tooltip/tooltipPropTypes.js
+++ b/src/components/Tooltip/tooltipPropTypes.js
@@ -32,6 +32,9 @@ const propTypes = {

     /** Maximum number of lines to show in tooltip */
     numberOfLines: PropTypes.number,
+
+    // If the mouse clicks outside, should we dismiss hover?
+    resetsOnClickOutside: PropTypes.bool,
 };

 const defaultProps = {
@@ -42,6 +45,7 @@ const defaultProps = {
     text: '',
     maxWidth: variables.sideBarWidth,
     numberOfLines: CONST.TOOLTIP_MAX_LINES,
+    resetsOnClickOutside: true,
 };

 export {
diff --git a/src/components/TooltipUtil/index.js b/src/components/TooltipUtil/index.js
new file mode 100644
index<PHONE_NUMBER>..c1fe3497bf
--- /dev/null
+++ b/src/components/TooltipUtil/index.js
@@ -0,0 +1,9 @@
+import {DeviceEventEmitter} from 'react-native';
+
+const hideTooltip = () => {
+    DeviceEventEmitter.emit('hide_tooltip');
+};
+
+export default {
+    hideTooltip,
+};
diff --git a/src/components/TooltipUtil/index.native.js b/src/components/TooltipUtil/index.native.js
new file mode 100644
index<PHONE_NUMBER>..6336d4bd4e
--- /dev/null
+++ b/src/components/TooltipUtil/index.native.js
@@ -0,0 +1,5 @@
+const hideTooltip = () => {};
+
+export default {
+    hideTooltip,
+};

@jatinsonijs Can you be more specific about the issue?

From my point of view, I think we should always fix issues globally.
If we apply a fix that only fix certain tooltips, it's more likely that we forget some. Also in the future if we add a new tooltip the issue will arise again.

From my point of view, I think we should always fix issues globally.
If we apply a fix that only fix certain tooltips, it's more likely that we forget some. Also in the future if we add a new tooltip the issue will arise again.

Yes @s77rt, I don't think we can forget with today IDE's we can easily search all tooltips and can apply changes where needed. and for future it can be added as regression tests. Why should we make less optimal to handle globally. And it's also not guaranteed document.elementFromPoint and isEqualNode always work perfectly as state by @getusha. Even its not working fine now.

I see your point. As for the regression test I don't think we can do it for such a case, can we?
elementFromPoint always works as expected. Taking in consideration hidden elements and non accessible elements. I just didn't reply to @getusha statement because I feel he just wanted to find an excuse to not go with my proposal without proving proofs/sources/scenarios.
The main objective here is to fix the issue the right way, does not much matter by whom.
I just don't like the idea of partial fixes.
I think my proposal is the best so far just because the fix is global. The wrost case scenario is that the tooltip may stay visible until modal is fully closed which is I think a small price to pay. Even the probability of occurrences of that scenario is not that high.

Yes @s77rt, I understood your concern let me send you issues which I'm taking about if its ok for you and for team then better to fix globally.

https://user-images.githubusercontent.com/114683042/209541625-beb971b9-9247-4eff-a8bc-6af0e5371d3f.mp4
Screenshot from video:

I see your point. As for the regression test I don't think we can do it for such a case, can we?
elementFromPoint always works as expected. Taking in consideration hidden elements and non accessible elements. I just didn't reply to @getusha statement because I feel he just wanted to find an excuse to not go with my proposal without providing proofs/sources/scenarios.
The main objective here is to fix the issue the right way, does not much matter by whom.
I just don't like the idea of partial fixes.
I think my proposal is the best so far just because the fix is global. The wrost case scenario is that the tooltip may stay visible until modal is fully closed which is I think a small price to pay. Even the probability of occurrences of that scenario is not that high.

@s77rt i am not comfortable you making this kind assumptions, it is not great to disregard and disrespect your peers opinion, As i worked on the previous issue (related to this), i just didn't like the approach and was sharing my concern. I was also trying to see the best solution without having a proposal. Please let's respect each others opinion and idea let's not argue. Also please if you have proved regressions related to my proposal i would be happy to improve also take the teams opinion and try to improve.
Also for the statement let's fix the issue globally, i don't know what that means but if it is related to my proposal, it is not "impossible" or difficult as you tried to express that we can add successText on the tooltip everywhere and it's good practice IMO. i will add it for every place and update my proposal. Please let's not argue again, Thanks.

@jatinsonijs Thanks for taking the time for that. As I see the issue is only on modal closing, the tooltip will hide after the modal is fully closed. As for the emoji this is because as you mentioned already Safari does not reset hover state. The emoji button will stay highlighted on Safari.

I think we can fix those two little issues by adding events listeners as i did with proposal 1

The emoji button will stay highlighted on Safari.

@s77rt, I see you are not using hover for your solution, you are using different way - getting points of elements and matching. Why it's not working for this emoji picker its not clear to me. If it's working for other place why not here.

@jatinsonijs I'm still looking into this. We can differently fix this using event listeners, just looking if we can get it even better with less code being changed.

@s77rt I think we still have good reason for do only where its needed 90% tooltips are simple (not toggled) why we should do extra process for them.

@jatinsonijs Yes you are right, but the issue may arise in the future and I don't think we can catch it in regressions tests because those tests make sure that something was working will still be working, and can't guarantee that something new will work as you'd expect.
If we can find a global solution that may be better.

If we really don't need a global solution, I think my proposal is still valid. Somehow similar to https://github.com/Expensify/App/issues/13146#issuecomment-1364531930

@s77rt may be I have used wrong name, I mean PR checklist something related to this like if you have added toggled tooltip in your work must check in safari and chrome etc... I saw for Input there is issue occur with emoji due to line height so they have added this in in their test list.

@jatinsonijs Ah I see. Yeah that totally makes sense.

Hoverable component is low level component so we shouldn't do much work on it. It may affect performance of app.

Agree, that's why I was trying to fix the issue without using event listeners..

Agree, that's why I was trying to fix the issue without using event listeners..

yeah mousedown and mouseup are also event listener, in my proposal I have added condition on navigation event listen when we have toggled tooltip only then it should listen for state change otherwise not. so for normal tooltip it will work same as working now.

Applied my solution on this  and Updated my proposal
Described why the solution approach is the most suitable for this situation. I have explained my reasoning for this decision in order to provide clarity and transparency in my approach.
@mananjadhav
Update: Also explained Why I have chosen this way (using the changed text / successText instead of boolean value) and what makes it different from resetsOnClickOutside=false Here

@getusha I don't think resetsOnClickOutside is not good here, If someone can forget to add it they can also forget to add successText. And personally its seem unusual to me we are using successText as a boolean but passing as a text and there is not any another use of it.
We can use different name for resetsOnClickOutside like toggleable, dismissOnInsideClick, preventInsideDismiss etc...

I am travelling for a few days and I can only check the updated conversations by day after tomorrow. Thanks for the patience and the active convseration folks.

Proposal 2 (Updated)
Global Solution

View Details
diff --git a/src/components/Hoverable/hoverablePropTypes.js b/src/components/Hoverable/hoverablePropTypes.js
index 07b8a8741e..4f82dbdc6f 100644
--- a/src/components/Hoverable/hoverablePropTypes.js
+++ b/src/components/Hoverable/hoverablePropTypes.js
@@ -20,8 +20,11 @@ const propTypes = {
     /** Function that executes when the mouse leaves the children. */
     onHoverOut: PropTypes.func,

-    // If the mouse clicks outside, should we dismiss hover?
-    resetsOnClickOutside: PropTypes.bool,
+    /* On mouse down, should we reset the hover state? */
+    shouldResetOnMouseDown: PropTypes.bool,
+
+    /* On mouse up, should we reset the hover state? */
+    shouldResetOnMouseUp: PropTypes.bool,
 };

 const defaultProps = {
@@ -29,7 +32,8 @@ const defaultProps = {
     containerStyles: [],
     onHoverIn: () => {},
     onHoverOut: () => {},
-    resetsOnClickOutside: false,
+    shouldResetOnMouseDown: false,
+    shouldResetOnMouseUp: false,
 };

 export {
diff --git a/src/components/Hoverable/index.js b/src/components/Hoverable/index.js
index f06ed56027..0eb7f3b44d 100644
--- a/src/components/Hoverable/index.js
+++ b/src/components/Hoverable/index.js
@@ -1,6 +1,6 @@
 import _ from 'underscore';
 import React, {Component} from 'react';
-import {View} from 'react-native';
+import {View, InteractionManager} from 'react-native';
 import {propTypes, defaultProps} from './hoverablePropTypes';

 /**
@@ -18,6 +18,9 @@ class Hoverable extends Component {
         this.wrapperView = null;

         this.resetHoverStateOnOutsideClick = this.resetHoverStateOnOutsideClick.bind(this);
+
+        this.resetHoverStateOnMouseDown = this.resetHoverStateOnMouseDown.bind(this);
+        this.resetHoverStateOnMouseUp = this.resetHoverStateOnMouseUp.bind(this);
     }

     componentDidMount() {
@@ -66,21 +69,46 @@ class Hoverable extends Component {
      * One such example is when a modal is opened while this component is hovered, and you click outside the component
      * to close the modal.
      *
-     * @param {Object} event - A click event
+     * @param {Object} event - A mouse event
      */
     resetHoverStateOnOutsideClick(event) {
         if (!this.state.isHovered) {
             return;
         }
-        if (this.props.resetsOnClickOutside) {
+        if (this.wrapperView && !this.wrapperView.contains(event.target)) {
             this.setIsHovered(false);
+        }
+    }
+
+    /**
+     * @param {Object} event - A mouse event
+     */
+    resetHoverStateOnMouseDown() {
+        if (!this.state.isHovered) {
             return;
         }
-        if (this.wrapperView && !this.wrapperView.contains(event.target)) {
+        if (this.props.shouldResetOnMouseDown) {
             this.setIsHovered(false);
         }
     }

+    /**
+     * @param {Object} event - A mouse event
+     */
+    resetHoverStateOnMouseUp(event) {
+        if (this.state.isHovered) {
+            return;
+        }
+        if (this.props.shouldResetOnMouseUp) {
+            InteractionManager.runAfterInteractions(() => {
+                const e = document.elementFromPoint(event.clientX, event.clientY);
+                if (this.wrapperView && this.wrapperView.matches(':hover') && (this.wrapperView === e || this.wrapperView.contains(e))) {
+                    this.setIsHovered(true);
+                }
+            });
+        }
+    }
+
     render() {
         if (this.props.absolute && React.isValidElement(this.props.children)) {
             return React.cloneElement(React.Children.only(this.props.children), {
@@ -95,6 +123,8 @@ class Hoverable extends Component {
                 },
                 onMouseEnter: () => this.setIsHovered(true),
                 onMouseLeave: () => this.setIsHovered(false),
+                onMouseDown: this.resetHoverStateOnMouseDown,
+                onMouseUp: this.resetHoverStateOnMouseUp,
             });
         }
         return (
@@ -103,6 +133,8 @@ class Hoverable extends Component {
                 ref={el => this.wrapperView = el}
                 onMouseEnter={() => this.setIsHovered(true)}
                 onMouseLeave={() => this.setIsHovered(false)}
+                onMouseDown={this.resetHoverStateOnMouseDown}
+                onMouseUp={this.resetHoverStateOnMouseUp}
             >
                 { // If this.props.children is a function, call it to provide the hover state to the children.
                     _.isFunction(this.props.children)
diff --git a/src/components/Tooltip/index.js b/src/components/Tooltip/index.js
index 8d3a345492..1dc7a669e5 100644
--- a/src/components/Tooltip/index.js
+++ b/src/components/Tooltip/index.js
@@ -189,7 +189,8 @@ class Tooltip extends PureComponent {
                     containerStyles={this.props.containerStyles}
                     onHoverIn={this.showTooltip}
                     onHoverOut={this.hideTooltip}
-                    resetsOnClickOutside
+                    shouldResetOnMouseDown
+                    shouldResetOnMouseUp
                 >
                     {child}
                 </Hoverable>

Unfortunately the global solution is not perfect as there are cases where the tooltip stays visible until you mvoe the mouse. IMO not a big drawback, but still not perfect. Can be fixed but only partially.

Non-Global Solution

View Details
diff --git a/src/components/ContextMenuItem.js b/src/components/ContextMenuItem.js
index c87fc5e9b4..b7732eb033 100644
--- a/src/components/ContextMenuItem.js
+++ b/src/components/ContextMenuItem.js
@@ -75,7 +75,7 @@ class ContextMenuItem extends Component {
         return (
             this.props.isMini
                 ? (
-                    <Tooltip text={text}>
+                    <Tooltip text={text} shouldResetOnMouseDown shouldResetOnMouseUp>
                         <Pressable
                             focusable
                             accessibilityLabel={text}
diff --git a/src/components/CopyTextToClipboard.js b/src/components/CopyTextToClipboard.js
index 594e696361..55a4fe49b3 100644
--- a/src/components/CopyTextToClipboard.js
+++ b/src/components/CopyTextToClipboard.js
@@ -59,7 +59,7 @@ class CopyTextToClipboard extends React.Component {
                 suppressHighlighting
             >
                 <Text style={this.props.textStyles}>{this.props.text}</Text>
-                <Tooltip text={this.props.translate(`reportActionContextMenu.${this.state.showCheckmark ? 'copied' : 'copyToClipboard'}`)}>
+                <Tooltip text={this.props.translate(`reportActionContextMenu.${this.state.showCheckmark ? 'copied' : 'copyToClipboard'}`)} shouldResetOnMouseDown shouldResetOnMouseUp>
                     <Icon
                         src={this.state.showCheckmark ? Expensicons.Checkmark : Expensicons.Clipboard}
                         fill={this.state.showCheckmark ? themeColors.iconSuccessFill : themeColors.icon}
diff --git a/src/components/Hoverable/hoverablePropTypes.js b/src/components/Hoverable/hoverablePropTypes.js
index 07b8a8741e..4f82dbdc6f 100644
--- a/src/components/Hoverable/hoverablePropTypes.js
+++ b/src/components/Hoverable/hoverablePropTypes.js
@@ -20,8 +20,11 @@ const propTypes = {
     /** Function that executes when the mouse leaves the children. */
     onHoverOut: PropTypes.func,

-    // If the mouse clicks outside, should we dismiss hover?
-    resetsOnClickOutside: PropTypes.bool,
+    /* On mouse down, should we reset the hover state? */
+    shouldResetOnMouseDown: PropTypes.bool,
+
+    /* On mouse up, should we reset the hover state? */
+    shouldResetOnMouseUp: PropTypes.bool,
 };

 const defaultProps = {
@@ -29,7 +32,8 @@ const defaultProps = {
     containerStyles: [],
     onHoverIn: () => {},
     onHoverOut: () => {},
-    resetsOnClickOutside: false,
+    shouldResetOnMouseDown: false,
+    shouldResetOnMouseUp: false,
 };

 export {
diff --git a/src/components/Hoverable/index.js b/src/components/Hoverable/index.js
index f06ed56027..0eb7f3b44d 100644
--- a/src/components/Hoverable/index.js
+++ b/src/components/Hoverable/index.js
@@ -1,6 +1,6 @@
 import _ from 'underscore';
 import React, {Component} from 'react';
-import {View} from 'react-native';
+import {View, InteractionManager} from 'react-native';
 import {propTypes, defaultProps} from './hoverablePropTypes';

 /**
@@ -18,6 +18,9 @@ class Hoverable extends Component {
         this.wrapperView = null;

         this.resetHoverStateOnOutsideClick = this.resetHoverStateOnOutsideClick.bind(this);
+
+        this.resetHoverStateOnMouseDown = this.resetHoverStateOnMouseDown.bind(this);
+        this.resetHoverStateOnMouseUp = this.resetHoverStateOnMouseUp.bind(this);
     }

     componentDidMount() {
@@ -66,21 +69,46 @@ class Hoverable extends Component {
      * One such example is when a modal is opened while this component is hovered, and you click outside the component
      * to close the modal.
      *
-     * @param {Object} event - A click event
+     * @param {Object} event - A mouse event
      */
     resetHoverStateOnOutsideClick(event) {
         if (!this.state.isHovered) {
             return;
         }
-        if (this.props.resetsOnClickOutside) {
+        if (this.wrapperView && !this.wrapperView.contains(event.target)) {
             this.setIsHovered(false);
+        }
+    }
+
+    /**
+     * @param {Object} event - A mouse event
+     */
+    resetHoverStateOnMouseDown() {
+        if (!this.state.isHovered) {
             return;
         }
-        if (this.wrapperView && !this.wrapperView.contains(event.target)) {
+        if (this.props.shouldResetOnMouseDown) {
             this.setIsHovered(false);
         }
     }

+    /**
+     * @param {Object} event - A mouse event
+     */
+    resetHoverStateOnMouseUp(event) {
+        if (this.state.isHovered) {
+            return;
+        }
+        if (this.props.shouldResetOnMouseUp) {
+            InteractionManager.runAfterInteractions(() => {
+                const e = document.elementFromPoint(event.clientX, event.clientY);
+                if (this.wrapperView && this.wrapperView.matches(':hover') && (this.wrapperView === e || this.wrapperView.contains(e))) {
+                    this.setIsHovered(true);
+                }
+            });
+        }
+    }
+
     render() {
         if (this.props.absolute && React.isValidElement(this.props.children)) {
             return React.cloneElement(React.Children.only(this.props.children), {
@@ -95,6 +123,8 @@ class Hoverable extends Component {
                 },
                 onMouseEnter: () => this.setIsHovered(true),
                 onMouseLeave: () => this.setIsHovered(false),
+                onMouseDown: this.resetHoverStateOnMouseDown,
+                onMouseUp: this.resetHoverStateOnMouseUp,
             });
         }
         return (
@@ -103,6 +133,8 @@ class Hoverable extends Component {
                 ref={el => this.wrapperView = el}
                 onMouseEnter={() => this.setIsHovered(true)}
                 onMouseLeave={() => this.setIsHovered(false)}
+                onMouseDown={this.resetHoverStateOnMouseDown}
+                onMouseUp={this.resetHoverStateOnMouseUp}
             >
                 { // If this.props.children is a function, call it to provide the hover state to the children.
                     _.isFunction(this.props.children)
diff --git a/src/components/Tooltip/index.js b/src/components/Tooltip/index.js
index 8d3a345492..d30d210c1a 100644
--- a/src/components/Tooltip/index.js
+++ b/src/components/Tooltip/index.js
@@ -189,7 +189,8 @@ class Tooltip extends PureComponent {
                     containerStyles={this.props.containerStyles}
                     onHoverIn={this.showTooltip}
                     onHoverOut={this.hideTooltip}
-                    resetsOnClickOutside
+                    shouldResetOnMouseDown={this.props.shouldResetOnMouseDown}
+                    shouldResetOnMouseUp={this.props.shouldResetOnMouseUp}
                 >
                     {child}
                 </Hoverable>
diff --git a/src/components/Tooltip/tooltipPropTypes.js b/src/components/Tooltip/tooltipPropTypes.js
index 627f1c0171..f2920c48d8 100644
--- a/src/components/Tooltip/tooltipPropTypes.js
+++ b/src/components/Tooltip/tooltipPropTypes.js
@@ -32,6 +32,12 @@ const propTypes = {

     /** Maximum number of lines to show in tooltip */
     numberOfLines: PropTypes.number,
+
+    /* On mouse down, should we reset the hover state? (Hoverable) */
+    shouldResetOnMouseDown: PropTypes.bool,
+
+    /* On mouse up, should we reset the hover state? (Hoverable) */
+    shouldResetOnMouseUp: PropTypes.bool,
 };

 const defaultProps = {
@@ -42,6 +48,8 @@ const defaultProps = {
     text: '',
     maxWidth: variables.sideBarWidth,
     numberOfLines: CONST.TOOLTIP_MAX_LINES,
+    shouldResetOnMouseDown: true,
+    shouldResetOnMouseUp: false,
 };

 export {
diff --git a/src/pages/home/HeaderView.js b/src/pages/home/HeaderView.js
index 2e048ad18b..2a2f4b7913 100644
--- a/src/pages/home/HeaderView.js
+++ b/src/pages/home/HeaderView.js
@@ -153,7 +153,7 @@ const HeaderView = (props) => {
                             )}

                             {shouldShowCallButton && <VideoChatButtonAndMenu isConcierge={isConcierge} />}
-                            <Tooltip text={props.report.isPinned ? props.translate('common.unPin') : props.translate('common.pin')}>
+                            <Tooltip text={props.report.isPinned ? props.translate('common.unPin') : props.translate('common.pin')} shouldResetOnMouseDown shouldResetOnMouseUp>
                                 <Pressable
                                     onPress={() => Report.togglePinnedState(props.report)}
                                     style={[styles.touchableButtonImage, styles.mr0]}

Same as the global but we only apply the fix for the effected tooltips (tooltips that changes text onclick).

Both solutions follow the same appraoch:

Hide tooltip on MouseDown
Show tooltip on MouseUp (if eligible; mouse is still over the element, etc.)

Recap
Global Solution

✔ Fixes this issue globally
✔ No race conditions
✔ Takes into consideration future use cases such as dynamic tooltip with navigation, etc.
❌ No regressions (Some tooltips will stay visible until you move the mouse)

Non-Global Solution

❌ Fixes this issue globally (Will have to pass shouldResetOnMouseUp to every effected <Tooltip />)
✔ No race conditions
✔ Takes into consideration future use cases such as dynamic tooltip with navigation, etc.
✔ No regressions

@s77rt solution is good, but its still not working with navigation. tooltip stuck on navigation have to move mouse to dismiss it.

@s77rt also you're using the approach that you were against on?

@jatinsonijs Thanks for testing. I have mentioned already that the tooltip will stay visible in some cases until you move the mouse. It's not stuck. On drawer navigation the tooltip will hide immediately. On modal navigation the tooltip will hide after you move the mouse. The term stuck was used on the previous issue to describe that the tooltip does not hide at all (on drawer navigation btw). Literally stuck whether you move the mouse or not. Anyway, I think we would prefer to have a non-global solution without regressions rather than a global-solution with regressions. Even though the regressions are not a big a deal.
@getusha I have 2 proposals.

But I have tested your non-global solution @s77rt as you wrote no regression like stay until mouse move right ?

@jatinsonijs The non-global solution does not produce any regressions because it's only applied on the effected tooltips and those does not have navigation. Even if they would have in the future, the tooltip will hide upon moving the mouse as opposed to @getusha 's solution, the tooltip will get actually stuck. I can even fix this by adding event listeners to navigation as I did with my proposal 1 and as you did with you last proposal but this is unnecessary. Don't you think so?

We are in same loop if its not needed resetsOnClickOutside false is enough I think, its also not stuck with this just move mouse tooltip will hide. even I think with @getusha solution it will not stuck it will stay and on mouse move will hide.

@jatinsonijs It will get stuck due to <Freeze /> which takes effects on drawer navigation (not modal navigation)

Actually @getusha solution is the same as @Pujan92 here https://github.com/Expensify/App/issues/13146#issuecomment-1332161311

Yes right its getting stuck on toggled tooltip + navigation + Freeze

Actually @getusha solution is the same as @Pujan92 here https://github.com/Expensify/App/issues/13146#issuecomment-1332161311

It's not the same and explained about your concerns, can wait for C+ review.

@getusha Your proposal causes a regression by not hiding the tooltip if the tooltip button opens a modal because your code won't reach this part. I'm not a C+ but your code does not make sense, there is a condition to check the hover state if it's true, why you are setting the state to true even if it's already true, you can just do if (this.props.resetsOnClickOutside && !this.props.successText) { and this can be simplified to only use resetsOnClickOutside which gets us back to @Pujan92 proposal.
TLDR; Excluding the regression, they are the same.

@getusha Your proposal causes a regression by not hiding the tooltip if the tooltip button opens a modal because your code won't reach this part. I'm not a C+ but your code does not make sense, there is a condition to check the hover state if it's true, why you are setting the state to true even if it's already true, you can just do if (this.props.resetsOnClickOutside && !this.props.successText) { and this can be simplified to only use resetsOnClickOutside which gets us back to @Pujan92 proposal.
TLDR; Excluding the regression, they are the same.

It can be handled through the PR, the proposal is the concept/approach of how we solve the issue IMO i stated that. And @s77rt this is not good behaviour trying to disregard proposals. Can we wait for @mananjadhav i am sure he will address that. I don't want anymore reply from you. Thanks for your understanding

Let's hold on further comments until we're able to get feedback on the existing proposals from @mananjadhav. Can that happen today?

Per @mananjadhav's request, he is traveling for the reminder of this year, so I've re-applied the external label so that we can have another C+ jump in and help out with the reviews. @mollfpr let us know if you won't be able to get to this in the next day and I'll re-assign C+ again, thanks!

@JmillsExpensify I will start reviewing ASAP, thanks!

Reviewing now...

Thank you guys for your patience, that's a lot of catch-up conversations here. Let me know if I'm missing something here.

I see that @s77rt mentioned about <Freeze /> might be causing this issue. I just try disabling the freeze props on both ReportScreen and SidebarLinks and keep the resetsOnClickOutside=true in the Tooltip but the issue still persist. Then change resetsOnClickOutside=false and keep disabling the freeze, https://github.com/Expensify/App/issues/12025 is still there. Maybe I'm testing it wrong but for now, I say the <Freeze /> is not the root cause.
Does anyone try to take a look at the onMouseEnter is not called on Safari when the toggled tooltip is used?

@s77rt I only review your non-global proposal as you said your global solution has a regression.
There's a delay in showing the toggled tooltip on the long press.
https://user-images.githubusercontent.com/25520267/209974502-5981bc1a-9ca6-4fbb-a942-fd2ffc4bdbc7.mov
Should be showing the change tooltip immediately.
https://user-images.githubusercontent.com/25520267/209975009-b532752f-741a-48e3-ae39-616838568497.mov
I don't get why we need separate the mouse-down and mouse-up events, can you elaborate more? Also, in your proposal, the mouse-down listener which is called this.resetHoverStateOnOutsideClick is still there. It makes confusion about what the event does.

@getusha Your proposal seems working fine for this issue but it can create regression if we have successText and navigation/modal action. Yes, we might be don't have it right now but there's a chance.

@fedirjh I agree with @jatinsonijs here and there are a lot of visits to another component to update and test each for regression.

@jatinsonijs from your proposal I think your solution is over-engineered, I think subscribing to the navigation state is not necessary. Also DeviceEventEmitter is depcrated.

Yes, i explained that in my comment you might seen that, which was if the tooltip updates and the user doesn't have the chance to see it due to navigation / opened modal does the successText have any value? does this make sense to you?
And thank you very much for the feedbacks.

@getusha Your proposal seems working fine for this issue but it can create regression if we have successText and navigation/modal action. Yes, we might be don't have it right now but there's a chance.

@mollfpr

<Freeze /> is the root cause of another issue that made us hide the tooltip in the first place. If we weren't using <Freeze /> we can simply remove the hide tooltip on mousedown functionality.

onMouseEnter is not called because Safari only call such events if the mouse actually moved. That's the spec.

The delay is because we are hiding the tooltip, then showing it again. I think long press is an edge case and possibly this is related the the debounce function used in the tooltip.
We should hide the tooltip otherwise there may be issues on navigation or modal opening (especially on Safari)

resetHoverStateOnOutsideClick is required to cover other cases, it's confusing because the current code is a mess. The comments says something while the function does something else. I have re-organised the code and moved some part of that function to another function resetHoverStateOnMouseDown

Besides, my proposal, can you confirm to @getusha that his proposal is the same as @Pujan92 because you skipped @Pujan92 proposal. also you didn't mention the regression, see here https://github.com/Expensify/App/issues/13146#issuecomment-1366844738

Does anyone try to take a look at the onMouseEnter is not called on Safari when the toggled tooltip is used?

@mollfpr  safari does not detect changes if anything change under mouse until user do any interaction. All above solution have this problem with toggled tooltip + navigation, That's why I have added navigation state.
It may be related to this
https://bugs.webkit.org/show_bug.cgi?id=120786

wow @s77rt

@mollfpr here is the example to check behaviour of event
Put cursor into yellow circle and wait for the rectangle to come over the circle. :hover is not triggered until you move mouse on safari but working fine on chrome.
https://jsfiddle.net/CamaroSS/ZUw65/12/

As I guess how it will work with toggled tooltip + navigation is
on press toggled tooltip will visible and after some delay navigation will perform.
Example for:
download button tooltip on hover it will show "download" on press will change to "downloading." and after download navigation will perform. In this case onmouseup logic will fail I guess @s77rt plz correct me if I'm wrong.

@mollfpr

About the delay, as suspected this is due to the debounce function. On mousedown the tooltip will hide, and it will take CONST.TIMING.TOOLTIP_SENSE which is 1 second to set the tooltip as hidden. If you long press and exceeds this value the tooltip will be considered hidden and then on mouseup we will show a hidden tooltip which takes 0.5 seconds. If you press and don't exceed the 1 second duration, the tooltip state will be still active and showing the tooltip in this state is immediate.
https://github.com/Expensify/App/blob/main/src/components/Tooltip/index.js#L104-L118
Can this be fixed? Differently
Is it worth it? I don't think so, 1 seconds is plenty of time to release the mouse button.

@s77rt i don't think it's related to the animation since we don't see that type of behavior on applying other solutions.

@jatinsonijs I'm sorry I didn't understand the case you are referring to, can you elaborate?
@getusha It is related to animation. You don't see this behaviour because you are not hiding the tooltip in the first place.

I'm saying toggled tooltip + navigation after some delay. Bcz if we are going to immediately hiding it then there is no use to add it with this case.

Ah yeah, If we do navigation while the tooltip is shown, then the tooltip will get stuck. But for this specific case since we are doing navigation after downloading is completed, we can hide the tooltip once the download is complete and before navigation. This is not really an issue.
To cover most of the cases, we first hide the tooltip, do whatever....(navigation, open modal, etc...), then show the tooltip if eligible.

@s77rt  but its manual process right consumer have to do it, It will not happen automatically.

@jatinsonijs Nothing is manual here. But perhaps I didn't understand your scenario, what manually process you are talking about?

As you are saying "we can hide the tooltip once the download is complete and before navigation." but it's not cover that case we want to show downloading also it's just an example If we are going to add toggled tooltip + navigation so I think its obvious navigation should not perform immediately. If it will going to perform after some delay how your solution will handle this just trying to understand that.

Ah okay, I see. In this case it won't work. it won't work with the delay. We can cover the delay using events, I have already done that my proposal 1. I can update my proposal 2 to add events listeners but it's a little too much. I'm trying to cover most cases with the least code.
Here are some cases that I had in mind:

a back button tooltip. If we can go back, the back button will navigate back, else the back button will changes it's tooltip text from "Back" to "You are at the first page"
a FAB that it's tooltip shows "New" if you click on it while online, open the modal, else shows "Offline"

Of course those were just some silly examples, that's why we need to hide the tooltip. Otherwise:

"Back" will be stuck due to navigation (and <Freeze />)
"New" will be stuck even though the modal is open (on Safari only)

@mollfpr What would you think if we apply my global solution and do one of the following:

Do nothing, because the regression is not much of a big deal (you can test and verify)
Add event listeners to fix the regression

a back button tooltip. If we can go back, the back button will navigate back, else the back button will changes it's tooltip text from "Back" to "You are at the first page"
a FAB that it's tooltip shows "New" if you click on it while online, open the modal, else shows "Offline"

These example I don't think need toggled tooltip these example need different text based on condition.

As my observation, Overall we can say from above proposals there is not any perfect solution for future functionality as you want.
These solution approx. doing same thing but in different way all have their own point of view.
=> https://github.com/Expensify/App/issues/13146#issuecomment-1332161311
=> https://github.com/Expensify/App/issues/13146#issuecomment-1354307351
=> https://github.com/Expensify/App/issues/13146#issuecomment-1364132723
@getusha point is also valid if we are going to navigate immediately it does not make sense to show toggled tooltip there.
And about stuck due to Freeze I think Freeze creating issue here, these solution are not creating these regression. Even it's happening in current version due to Freeze. To handle it we should use Freeze for mobile only where we don't have tooltip.
https://user-images.githubusercontent.com/114683042/210040003-cd2db102-4086-45b3-9ddb-14f639b7f5b8.mp4
You can see in this video I'm doing this test on staging.
Open any chat on chrome.
Press on copy.
Press esc key to go back.
Tooltip will stuck due to Freeze.
You can test same case with freeze false in ReportScreen. With false it working fine.

This solution is handling navigation case( delay navigation) also which is a valid IMO toggled tooltip + navigation after some time to show another tooltip.
=> https://github.com/Expensify/App/issues/13146#issuecomment-1364458950
But if navigating from Freeze component it still creating issue. and some other edge cases like modal present after toggled tooltip.

https://github.com/Expensify/App/issues/13146#issuecomment-1366646959
Global solution always have issue of tooltip will not hide until mouse move in some places.
Non global have same issue with toggled tooltip + different UI after some delay (navigation or modal) Freeze will stuck tooltip, tooltip will not hide until mouse move on non freeze component.

@jatinsonijs

The examples I showed were just to demonstrate the case, sometimes we can't predetermine the condition until we actually press the button (I think going back is one of those cases) or determining the condition is costly and we have only to do it at the last step (button press)

Disabling <Freeze /> is really not a solution for performance reasons.

The tooltip will get stuck if you navigate with keyboard, I have mentioned this already https://github.com/Expensify/App/issues/13146#issuecomment-1332734440 That's why I'm against hiding the tooltip on mouse down because it's really a workaround (check my proposal 1). But I have other reasons to why we should go with it anyway and that's where I started working on proposal 2

Your delay case solution is using events listeners, if we really want to use events listeners then we can just go with the global solution and be done with it

As my observation, Overall we can say from above proposals there is not any perfect solution for future functionality as you want.

These solution approx. doing same thing but in different way. All have their own point of view.

=> https://github.com/Expensify/App/issues/13146#issuecomment-1332161311
=> https://github.com/Expensify/App/issues/13146#issuecomment-1354307351
=> https://github.com/Expensify/App/issues/13146#issuecomment-1364132723
@getusha point is also valid if we are going to navigate immediately it does not make sense to show toggled tooltip there.
And about stuck due to React Freeze I think Freeze creating issue here not these solution. Even it's happening in current version due to Freeze. To handle it we can use Freeze for mobile only where we don't have tooltip.
https://user-images.githubusercontent.com/114683042/210040003-cd2db102-4086-45b3-9ddb-14f639b7f5b8.mp4
You can see in this video I'm doing this test on staging.
Open any chat on chrome.
Press on copy.
Press esc key to go back.
Tooltip will stuck due to Freeze.
You can test same case with freeze false in ReportScreen. With false it working fine.

This solution is handling navigation case( delay navigation) also which is a valid IMO toggled tooltip + navigation after some time to show another tooltip.
=> https://github.com/Expensify/App/issues/13146#issuecomment-1364458950
But if navigating from Freeze component it still creating issue. and some other edge cases like modal present after toggled tooltip.

https://github.com/Expensify/App/issues/13146#issuecomment-1366646959
Global solution always have issue of tooltip will not hide until mouse move in some places.
Non global have same issue with toggled tooltip + different UI after some delay (navigation or modal) due to React Freeze tooltip will stuck, tooltip will not hide until mouse move on non freeze component.

Completely agree with @jatinsonijs observations 👍