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 useBoundingClientRect from '@theatre/studio/uiComponents/useBoundingClientRect'
|
||||||
import ArrowContext from './ArrowContext'
|
import ArrowContext from './ArrowContext'
|
||||||
import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
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 minimumDistanceOfArrowToEdgeOfPopover = 8
|
||||||
|
|
||||||
const TooltipWrapper: React.FC<{
|
const TooltipWrapper: React.FC<{
|
||||||
target: HTMLElement | SVGElement
|
target: HTMLElement | SVGElement
|
||||||
|
onClickOutside?: (e: MouseEvent) => void
|
||||||
children: () => React.ReactElement
|
children: () => React.ReactElement
|
||||||
|
onPointerOutside?: {
|
||||||
|
threshold: number
|
||||||
|
callback: (e: MouseEvent) => void
|
||||||
|
}
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
const originalElement = props.children()
|
const originalElement = props.children()
|
||||||
const [ref, container] = useRefAndState<HTMLElement | SVGElement | null>(null)
|
const [ref, container] = useRefAndState<HTMLElement | SVGElement | null>(null)
|
||||||
|
@ -75,9 +83,17 @@ const TooltipWrapper: React.FC<{
|
||||||
container.style.top = pos.top + 'px'
|
container.style.top = pos.top + 'px'
|
||||||
setArrowContextValue(arrowStyle)
|
setArrowContextValue(arrowStyle)
|
||||||
|
|
||||||
return () => {}
|
if (props.onPointerOutside) {
|
||||||
|
return onPointerOutside(
|
||||||
|
container,
|
||||||
|
props.onPointerOutside.threshold,
|
||||||
|
props.onPointerOutside.callback,
|
||||||
|
)
|
||||||
|
}
|
||||||
}, [containerRect, container, props.target, targetRect, windowSize])
|
}, [containerRect, container, props.target, targetRect, windowSize])
|
||||||
|
|
||||||
|
useOnClickOutside(container, props.onClickOutside ?? noop)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ArrowContext.Provider value={arrowContextValue}>
|
<ArrowContext.Provider value={arrowContextValue}>
|
||||||
{cloneElement(originalElement, {ref, style})}
|
{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 {createPortal} from 'react-dom'
|
||||||
import {PortalContext} from 'reakit'
|
import {PortalContext} from 'reakit'
|
||||||
import TooltipWrapper from './TooltipWrapper'
|
import TooltipWrapper from './TooltipWrapper'
|
||||||
|
@ -20,6 +20,7 @@ export default function usePopover(
|
||||||
opts: {
|
opts: {
|
||||||
closeWhenPointerIsDistant?: boolean
|
closeWhenPointerIsDistant?: boolean
|
||||||
pointerDistanceThreshold?: number
|
pointerDistanceThreshold?: number
|
||||||
|
closeOnClickOutside?: boolean
|
||||||
},
|
},
|
||||||
render: () => React.ReactElement,
|
render: () => React.ReactElement,
|
||||||
): [node: React.ReactNode, open: OpenFn, close: CloseFn, isOpen: boolean] {
|
): [node: React.ReactNode, open: OpenFn, close: CloseFn, isOpen: boolean] {
|
||||||
|
@ -39,11 +40,29 @@ export default function usePopover(
|
||||||
setState({isOpen: false})
|
setState({isOpen: false})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const onClickOutside = useCallback(() => {
|
||||||
|
if (opts.closeOnClickOutside !== false) {
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
}, [opts.closeOnClickOutside])
|
||||||
|
|
||||||
const portalLayer = useContext(PortalContext)
|
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 ? (
|
const node = state.isOpen ? (
|
||||||
createPortal(
|
createPortal(
|
||||||
<TooltipWrapper children={render} target={state.target} />,
|
<TooltipWrapper
|
||||||
|
children={render}
|
||||||
|
target={state.target}
|
||||||
|
onClickOutside={onClickOutside}
|
||||||
|
onPointerOutside={onPointerOutside}
|
||||||
|
/>,
|
||||||
portalLayer!,
|
portalLayer!,
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -7,6 +7,7 @@ import TooltipWrapper from './TooltipWrapper'
|
||||||
import {createPortal} from 'react-dom'
|
import {createPortal} from 'react-dom'
|
||||||
import {useTooltipOpenState} from './TooltipContext'
|
import {useTooltipOpenState} from './TooltipContext'
|
||||||
import {PortalContext} from 'reakit'
|
import {PortalContext} from 'reakit'
|
||||||
|
import noop from '@theatre/shared/utils/noop'
|
||||||
|
|
||||||
export default function useTooltip(
|
export default function useTooltip(
|
||||||
opts: {enabled?: boolean; delay?: number},
|
opts: {enabled?: boolean; delay?: number},
|
||||||
|
@ -48,7 +49,11 @@ export default function useTooltip(
|
||||||
const node =
|
const node =
|
||||||
enabled && isOpen && targetNode ? (
|
enabled && isOpen && targetNode ? (
|
||||||
createPortal(
|
createPortal(
|
||||||
<TooltipWrapper children={render} target={targetNode} />,
|
<TooltipWrapper
|
||||||
|
children={render}
|
||||||
|
target={targetNode}
|
||||||
|
onClickOutside={noop}
|
||||||
|
/>,
|
||||||
portalLayer!,
|
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(
|
export default function onPointerOutside(
|
||||||
container: Element | null,
|
node: Element,
|
||||||
threshold: number,
|
threshold: number,
|
||||||
onPointerOutside: () => void,
|
onPointerOutside: (e: MouseEvent) => void,
|
||||||
) {
|
) {
|
||||||
const containerRect = useBoundingClientRect(container)
|
const containerRect = node.getBoundingClientRect()
|
||||||
|
|
||||||
useEffect(() => {
|
const onMouseMove = (e: MouseEvent) => {
|
||||||
if (!containerRect) return
|
if (
|
||||||
|
e.clientX < containerRect.left - threshold ||
|
||||||
const onMouseMove = (e: MouseEvent) => {
|
e.clientX > containerRect.left + containerRect.width + threshold ||
|
||||||
if (
|
e.clientY < containerRect.top - threshold ||
|
||||||
e.clientX < containerRect.left - threshold ||
|
e.clientY > containerRect.top + containerRect.height + threshold
|
||||||
e.clientX > containerRect.left + containerRect.width + threshold ||
|
) {
|
||||||
e.clientY < containerRect.top - threshold ||
|
onPointerOutside(e)
|
||||||
e.clientY > containerRect.top + containerRect.height + threshold
|
|
||||||
) {
|
|
||||||
onPointerOutside()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
window.addEventListener('mousemove', onMouseMove)
|
window.addEventListener('mousemove', onMouseMove)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('mousemove', onMouseMove)
|
window.removeEventListener('mousemove', onMouseMove)
|
||||||
}
|
}
|
||||||
}, [containerRect, threshold])
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue