Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

improve hover outside handler logic #115

Merged
merged 10 commits into from Feb 16, 2021

Conversation

czabaj
Copy link
Contributor

@czabaj czabaj commented Feb 5, 2021

This fixes the flickering problem when trigger="hover" and Trigger and Tooltip overlaps.

Partially relates #114

First commit also fixes some memoization bugs.

This fixes flickering Tooltip with `trigger="hover"` when the Tooltip
overlaps the Trigger and cursor is moved to the overlaping section.
Hovering overlaping section triggers `mouseleave` on the Trigger,
which runs `hideTooltip()` but as the Tooltip disappears, the cursor
encounters the Trigger again, `mouseenter` is triggered which runs
`showTooltip()` and so it continues ad infinituum.

This solution relies on `mousemove` instead and compares the cursor
position with DOMRect of the Trigger. Also, when the `interactive` prop
is enabled, it takes DOMRect of the Tooltip into consideration.

This solution fixes another bug, which will occur with `trigger="hover"`
in conjunction with `interactive` and big offset. The offset will create
gap which will hide the Tooltip before the user hovers over it, so the
`interactive` would not work, since the Tooltip will never be hoverable.
This change virtualy increases the DOMRect of the Trigger to reach the
Tooltip DOMRect so hovering over gap keeps the Tooltip opened.
@codecov
Copy link

codecov bot commented Feb 5, 2021

Codecov Report

Merging #115 (29426fb) into master (3dfa356) will increase coverage by 2.77%.
The diff coverage is 92.10%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #115      +/-   ##
==========================================
+ Coverage   90.97%   93.75%   +2.77%     
==========================================
  Files           2        2              
  Lines         133      160      +27     
  Branches       38       54      +16     
==========================================
+ Hits          121      150      +29     
+ Misses         12       10       -2     
Impacted Files Coverage Δ
src/usePopperTooltip.ts 92.80% <88.00%> (+2.70%) ⬆️
src/utils.ts 97.14% <100.00%> (+1.68%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 3dfa356...29426fb. Read the comment docs.

};
}, [triggerRef, isTriggeredBy, showTooltip, hideTooltip]);
React.useEffect(() => {
if (!visible || triggerRef == null || !isTriggeredBy('hover')) return;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This follows the same condition as hover but it turns on only when visible is truthy.

@mohsinulhaq
Copy link
Owner

So the memo fix was definitely needed, but I'm wondering if instead of avoiding calculations on every mousemove, we could avoid flickering by using pointer-events: none, as you also had talked about. While the gap issue can be solved by passing a delayHide, as mentioned in the docs so that the user gets ample time to move over to the tooltip.

What are the pros and cons here according to you?

@czabaj
Copy link
Contributor Author

czabaj commented Feb 7, 2021

@mohsinulhaq the pointer-events: none is a very simple fix but it only works with an arrow, you simply cannot apply pointer-events: none to the Tooltip as it wouldn't be able to be interactive, thus it won't work with negative offset, which is a very rare use case, but it adds another little bit to the caveats of the library.

The second problem with the potential gap between Trigger and the Tooltip in interactive mode is another caveat which users of the library must be aware of and the delayHide might not work very well in all scenarios.

The solution proposed here solves all the problems quite clearly without any caveats. If you are concerned with performance, I admit the getBoundingClientRect might be costly, but I did not measure its impact. Tooltip is not active most of the time, so a slight increase in its performance cost is fine by me. I could implement throttling for the mousemove event if you wish, although it is not trivial to implement event throttling in React without memory leaks, so I would rather not complicate this solution if it is not necessary.

@mohsinulhaq
Copy link
Owner

@czabaj then how about attaching to mouseleave event when interactive is false, adding pointer-events none to both tooltip and arrow (we already add that to tooltip with followCursor, you can add one more condition there) and attach to mousemove event only when interactive is true.

@czabaj
Copy link
Contributor Author

czabaj commented Feb 7, 2021

@mohsinulhaq that sounds complicated, wouldn't be better to be free of thinking about when and why attach pointer-events: none? With this change, you can even remove the pointer-events: none for followCursor mode and make the logic simple IMHO. And, there is still the problem with the gap between Trigger and Tooltip, which this solution solves nicely and it could be even improved further, like increase the hover area for better usability.

The more I think about it, the more I'm sure that this logic clearly expresses what you need to achieve -- hide the tooltip when the cursor leaves some area. The CSS z-index is an obstacle, which this solution gets rid of completely. The only drawback is theoreticly performance, but I don't think it is that critical and if you think it is, throttling the mousemove event is possible.

@mohsinulhaq
Copy link
Owner

@czabaj If you could re-use the same logic to integrate with the virtual element construct from react-popper that we are using for follow-cursor, that would be even better.

@czabaj
Copy link
Contributor Author

czabaj commented Feb 8, 2021

@czabaj If you could re-use the same logic to integrate with the virtual element construct from react-popper that we are using for follow-cursor, that would be even better.

Can you please elaborate more on this? I see the possibility to use only a single mousemove event for both cases, is that what you mean?

@mohsinulhaq
Copy link
Owner

@czabaj yep, that's exactly what I meant

@czabaj
Copy link
Contributor Author

czabaj commented Feb 9, 2021

@mohsinulhaq look at the changes, I also added tests for isMouseOutside

@czabaj
Copy link
Contributor Author

czabaj commented Feb 11, 2021

@mohsinulhaq hold on, I identified one bug. The mousemove event calls hideTooltip every time the mouse moves outside the elements until the visible = false. When there is hideDelay set, then every move outside the hover area prolongs the delay by calling hideTooltip thus the Tooltip hides effectively after the delay after the mouse stops moving.

I must call hideTooltip only once.

@czabaj
Copy link
Contributor Author

czabaj commented Feb 11, 2021

@mohsinulhaq I added check which calls hideTooltip only when there is change mouseInside => mouseOutside

@mohsinulhaq
Copy link
Owner

ok thanks @czabaj, will review it soon

@mohsinulhaq
Copy link
Owner

@czabaj can you allow edits to the PR/branch. I have some changes to push to this before merging.

@czabaj
Copy link
Contributor Author

czabaj commented Feb 13, 2021

@mohsinulhaq the "Allow edits by maintainers" is checked, is there any more options?

@mohsinulhaq
Copy link
Owner

@czabaj I have removed the pointer-events none we were using for follow-cursor, because I don't think it's needed anymore after your change. Correct me if I'm wrong.

@czabaj
Copy link
Contributor Author

czabaj commented Feb 14, 2021

@mohsinulhaq correct.

@czabaj
Copy link
Contributor Author

czabaj commented Feb 14, 2021

@mohsinulhaq as finishing touch, I improved the test for the "gap between" and added ESLint suppression pragma for useCalback dependencies. Feel free to revert the ESLint suppression, but it shall be there IMHO, for it expresses that violation of the rule is intended (ESLint complaints about the isArray ternary expression and usage of the array right away, as it is not able to assess correctness).

@mohsinulhaq mohsinulhaq changed the title Fix/tooltip flicker improve hover outside handler logic Feb 14, 2021
@mohsinulhaq
Copy link
Owner

@czabaj shall I go ahead and merge it?

@czabaj
Copy link
Contributor Author

czabaj commented Feb 16, 2021

@mohsinulhaq I'm confident. Go ahead!

@mohsinulhaq mohsinulhaq merged commit babbffc into mohsinulhaq:master Feb 16, 2021
@czabaj
Copy link
Contributor Author

czabaj commented Feb 16, 2021

@mohsinulhaq It was a pleasure, I like the selection of tooling in the repo and the code is easy to follow. If this creates some regression, don't hesitate to mention me in the issue. I will be glad to help.

@mohsinulhaq
Copy link
Owner

Thanks 😄

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

Successfully merging this pull request may close these issues.

None yet

2 participants