-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Description
Provide a general summary of the feature here
There is a problem with the current useHover implementation that makes it hard to use with popovers that open inside a portal.
I have a case, where I'm building an app nav menu, that should be collapsed and opens full size on hover. Similar to how gmail menu behaves, when collapsed. My menu also have some buttons, which open popovers, when pressed. When I move my pointer to the opened popover, isHovered becomes false, despite the pointerleave event is not generated.
🤔 Expected Behavior?
I'm expecting, that I could somehow configure the hook behaviour to control, what I'm considering a hover state.
😯 Current Behavior
I've found, that the current behaviour occurs because of this check
| addGlobalListener(getOwnerDocument(event.target), 'pointerover', e => { |
This special case may be valid for some scenarios, but in my situation, the
nodeContains check is exactly what ruins my hover state, cause my popover is rendered in a portal.
💁 Possible Solution
First: Hack
For anyone, who comes here, I've found a workaround, that works in most scenarios. I've added a listener, that works before the library's one and stops the event propagation to avoid the react-aria hover end logic. But this is obviously not a good way ti handle this situations.
const { addGlobalListener, removeGlobalListener } = useGlobalListeners();
useEffect(() => {
const handlePointerOver = (e: PointerEvent) => {
const isInsideFloatingElement = !!(e.target as HTMLElement).closest(
// This is the data attribute I use to mark popovers
'[data-is-floating-element=true]'
);
if (isInsideFloatingElement) {
e.stopImmediatePropagation();
e.stopPropagation();
}
};
addGlobalListener(document, 'pointerover', handlePointerOver, {
capture: true,
});
return () => {
removeGlobalListener(document, 'pointerover', handlePointerOver);
};
}, []);Second: improve hook API
I really like the pattern that floating-ui uses for press outside behaviour. It allows you to perform you custom checks on the events it listens to. It gives you the additional level of control, but keeps things simple and incapsulated.
useHover may also receive a prop like shouldEndHover?: (event: PointerEvent) => boolean
The check can be rewritten like:
if (state.isHovered && state.target && !nodeContains(state.target, e.target as Element) && shouldEndHover?.(e) ?? true)Additional problem
The problem, I didn't find a reliable solution for is that only pointerleave event is generated, when I move my pointer out of the popover boundaries, but when I move it back to the popover, no pointerenter event is generated, so the hover state is lost. But this is a rare case, that probably won't affect users as much as collapsing menu, when they just move their mouse from the button to the popover.
The only way that now I can think of to solve it is provide the triggerHoverStart function from the hook, so I can register my own pointerenter handler on the popover and start the hover manually, when user returns his pointer to it.
🔦 Context
Outside click has always been a tricky thing in React, I remember the days I used the deprecated findDOMNodeAPI for it. Other solutions also were challenging to use with portals and often lead to inconsistence behaviour with multiple popovers. But since I've started using floating-ui, I've never had an unsolvable events conflict again. This library gave me the level of control over events I wanted.
That's why I this the proposed change may benefit the library flexibility. If you agree to it, I can make a PR with it.
💻 Examples
No response
🧢 Your Company/Team
No response
🕷 Tracking Issue
No response