Popovers now close automatically on mouseleave

This commit is contained in:
Aria Minaei 2021-08-20 15:24:36 +02:00
parent 2a45c68374
commit 59416d068b
4 changed files with 64 additions and 27 deletions

View file

@ -4,12 +4,20 @@ import useWindowSize from 'react-use/esm/useWindowSize'
import useBoundingClientRect from '@theatre/studio/uiComponents/useBoundingClientRect'
import ArrowContext from './ArrowContext'
import useRefAndState from '@theatre/studio/utils/useRefAndState'
import useOnClickOutside from '@theatre/studio/uiComponents/useOnClickOutside'
import onPointerOutside from '@theatre/studio/uiComponents/onPointerOutside'
import noop from '@theatre/shared/utils/noop'
const minimumDistanceOfArrowToEdgeOfPopover = 8
const TooltipWrapper: React.FC<{
target: HTMLElement | SVGElement
onClickOutside?: (e: MouseEvent) => void
children: () => React.ReactElement
onPointerOutside?: {
threshold: number
callback: (e: MouseEvent) => void
}
}> = (props) => {
const originalElement = props.children()
const [ref, container] = useRefAndState<HTMLElement | SVGElement | null>(null)
@ -75,9 +83,17 @@ const TooltipWrapper: React.FC<{
container.style.top = pos.top + 'px'
setArrowContextValue(arrowStyle)
return () => {}
if (props.onPointerOutside) {
return onPointerOutside(
container,
props.onPointerOutside.threshold,
props.onPointerOutside.callback,
)
}
}, [containerRect, container, props.target, targetRect, windowSize])
useOnClickOutside(container, props.onClickOutside ?? noop)
return (
<ArrowContext.Provider value={arrowContextValue}>
{cloneElement(originalElement, {ref, style})}

View file

@ -1,4 +1,4 @@
import React, {useCallback, useContext, useState} from 'react'
import React, {useCallback, useContext, useMemo, useState} from 'react'
import {createPortal} from 'react-dom'
import {PortalContext} from 'reakit'
import TooltipWrapper from './TooltipWrapper'
@ -20,6 +20,7 @@ export default function usePopover(
opts: {
closeWhenPointerIsDistant?: boolean
pointerDistanceThreshold?: number
closeOnClickOutside?: boolean
},
render: () => React.ReactElement,
): [node: React.ReactNode, open: OpenFn, close: CloseFn, isOpen: boolean] {
@ -39,11 +40,29 @@ export default function usePopover(
setState({isOpen: false})
}, [])
const onClickOutside = useCallback(() => {
if (opts.closeOnClickOutside !== false) {
close()
}
}, [opts.closeOnClickOutside])
const portalLayer = useContext(PortalContext)
const onPointerOutside = useMemo(() => {
if (opts.closeOnClickOutside === false) return undefined
return {
threshold: opts.pointerDistanceThreshold ?? 100,
callback: close,
}
}, [opts.closeWhenPointerIsDistant])
const node = state.isOpen ? (
createPortal(
<TooltipWrapper children={render} target={state.target} />,
<TooltipWrapper
children={render}
target={state.target}
onClickOutside={onClickOutside}
onPointerOutside={onPointerOutside}
/>,
portalLayer!,
)
) : (

View file

@ -7,6 +7,7 @@ import TooltipWrapper from './TooltipWrapper'
import {createPortal} from 'react-dom'
import {useTooltipOpenState} from './TooltipContext'
import {PortalContext} from 'reakit'
import noop from '@theatre/shared/utils/noop'
export default function useTooltip(
opts: {enabled?: boolean; delay?: number},
@ -48,7 +49,11 @@ export default function useTooltip(
const node =
enabled && isOpen && targetNode ? (
createPortal(
<TooltipWrapper children={render} target={targetNode} />,
<TooltipWrapper
children={render}
target={targetNode}
onClickOutside={noop}
/>,
portalLayer!,
)
) : (

View file

@ -1,31 +1,28 @@
import {useEffect} from 'react'
import useBoundingClientRect from './useBoundingClientRect'
/**
* Calls the callback when the mouse pointer moves outside the
* bounds of the node.
*/
export default function onPointerOutside(
container: Element | null,
node: Element,
threshold: number,
onPointerOutside: () => void,
onPointerOutside: (e: MouseEvent) => void,
) {
const containerRect = useBoundingClientRect(container)
const containerRect = node.getBoundingClientRect()
useEffect(() => {
if (!containerRect) return
const onMouseMove = (e: MouseEvent) => {
if (
e.clientX < containerRect.left - threshold ||
e.clientX > containerRect.left + containerRect.width + threshold ||
e.clientY < containerRect.top - threshold ||
e.clientY > containerRect.top + containerRect.height + threshold
) {
onPointerOutside()
}
const onMouseMove = (e: MouseEvent) => {
if (
e.clientX < containerRect.left - threshold ||
e.clientX > containerRect.left + containerRect.width + threshold ||
e.clientY < containerRect.top - threshold ||
e.clientY > containerRect.top + containerRect.height + threshold
) {
onPointerOutside(e)
}
}
window.addEventListener('mousemove', onMouseMove)
window.addEventListener('mousemove', onMouseMove)
return () => {
window.removeEventListener('mousemove', onMouseMove)
}
}, [containerRect, threshold])
return () => {
window.removeEventListener('mousemove', onMouseMove)
}
}