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 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})}

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 {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!,
) )
) : ( ) : (

View file

@ -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!,
) )
) : ( ) : (

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( 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])
} }