Popovers now close automatically on mouseleave
This commit is contained in:
parent
2a45c68374
commit
59416d068b
4 changed files with 64 additions and 27 deletions
|
@ -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})}
|
||||
|
|
|
@ -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!,
|
||||
)
|
||||
) : (
|
||||
|
|
|
@ -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!,
|
||||
)
|
||||
) : (
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue