diff --git a/theatre/studio/src/uiComponents/Popover/TooltipWrapper.tsx b/theatre/studio/src/uiComponents/Popover/TooltipWrapper.tsx index 662d960..4d3b357 100644 --- a/theatre/studio/src/uiComponents/Popover/TooltipWrapper.tsx +++ b/theatre/studio/src/uiComponents/Popover/TooltipWrapper.tsx @@ -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(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 ( {cloneElement(originalElement, {ref, style})} diff --git a/theatre/studio/src/uiComponents/Popover/usePopover.tsx b/theatre/studio/src/uiComponents/Popover/usePopover.tsx index 1d8b09d..6bd8dda 100644 --- a/theatre/studio/src/uiComponents/Popover/usePopover.tsx +++ b/theatre/studio/src/uiComponents/Popover/usePopover.tsx @@ -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( - , + , portalLayer!, ) ) : ( diff --git a/theatre/studio/src/uiComponents/Popover/useTooltip.tsx b/theatre/studio/src/uiComponents/Popover/useTooltip.tsx index e6379c1..2f302a1 100644 --- a/theatre/studio/src/uiComponents/Popover/useTooltip.tsx +++ b/theatre/studio/src/uiComponents/Popover/useTooltip.tsx @@ -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( - , + , portalLayer!, ) ) : ( diff --git a/theatre/studio/src/uiComponents/onPointerOutside.ts b/theatre/studio/src/uiComponents/onPointerOutside.ts index 3855701..df5ba46 100644 --- a/theatre/studio/src/uiComponents/onPointerOutside.ts +++ b/theatre/studio/src/uiComponents/onPointerOutside.ts @@ -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) + } }