diff --git a/theatre/studio/src/uiComponents/Popover/Popover.tsx b/theatre/studio/src/uiComponents/Popover/Popover.tsx index afa34c9..c504d54 100644 --- a/theatre/studio/src/uiComponents/Popover/Popover.tsx +++ b/theatre/studio/src/uiComponents/Popover/Popover.tsx @@ -1,20 +1,24 @@ import getStudio from '@theatre/studio/getStudio' import useBoundingClientRect from '@theatre/studio/uiComponents/useBoundingClientRect' import transparentize from 'polished/lib/color/transparentize' -import React, {useLayoutEffect, useState} from 'react' +import React, {useLayoutEffect, useRef, useState} from 'react' import {createPortal} from 'react-dom' import useWindowSize from 'react-use/esm/useWindowSize' import styled from 'styled-components' +import PopoverArrow from './PopoverArrow' /** * How far from the menu should the pointer travel to auto close the menu */ const defaultPointerDistanceThreshold = 200 +export const popoverBackgroundColor = transparentize(0.2, '#111') +const minimumDistanceOfArrowToEdgeOfPopover = 8 + const Container = styled.ul` position: absolute; z-index: 10000; - background: ${transparentize(0.2, '#111')}; + background: ${popoverBackgroundColor}; color: white; padding: 0; margin: 0; @@ -34,33 +38,57 @@ const Popover: React.FC<{ props.pointerDistanceThreshold ?? defaultPointerDistanceThreshold const [container, setContainer] = useState(null) - const rect = useBoundingClientRect(container) + const arrowRef = useRef(null) + + const containerRect = useBoundingClientRect(container) + const targetRect = useBoundingClientRect(props.target) const windowSize = useWindowSize() useLayoutEffect(() => { - if (!rect || !container) return + if (!containerRect || !container || !targetRect) return - const preferredAnchorPoint = { - left: rect.width / 2, - top: 0, + const gap = 8 + const arrow = arrowRef.current! + + let verticalPlacement: 'bottom' | 'top' | 'overlay' = 'bottom' + let top = 0 + let left = 0 + if (targetRect.bottom + containerRect.height + gap < windowSize.height) { + verticalPlacement = 'bottom' + top = targetRect.bottom + gap + arrow.style.top = '0px' + } else if (targetRect.top > containerRect.height + gap) { + verticalPlacement = 'top' + top = targetRect.top - (containerRect.height + gap) + arrow.style.bottom = '0px' + arrow.style.transform = 'rotateZ(180deg)' + } else { + verticalPlacement = 'overlay' } - const pos = { - left: props.clickPoint.clientX - preferredAnchorPoint.left, - top: props.clickPoint.clientY - preferredAnchorPoint.top, + let arrowLeft = 0 + if (verticalPlacement !== 'overlay') { + const anchorLeft = targetRect.left + targetRect.width / 2 + if (anchorLeft < containerRect.width / 2) { + left = gap + arrowLeft = Math.max( + anchorLeft - gap, + minimumDistanceOfArrowToEdgeOfPopover, + ) + } else if (anchorLeft + containerRect.width / 2 > windowSize.width) { + left = windowSize.width - (gap + containerRect.width) + arrowLeft = Math.min( + anchorLeft - left, + containerRect.width - minimumDistanceOfArrowToEdgeOfPopover, + ) + } else { + left = anchorLeft - containerRect.width / 2 + arrowLeft = containerRect.width / 2 + } + arrow.style.left = arrowLeft + 'px' } - if (pos.left < 0) { - pos.left = 0 - } else if (pos.left + rect.width > windowSize.width) { - pos.left = windowSize.width - rect.width - } - - if (pos.top < 0) { - pos.top = 0 - } else if (pos.top + rect.height > windowSize.height) { - pos.top = windowSize.height - rect.height - } + const pos = {left, top} container.style.left = pos.left + 'px' container.style.top = pos.top + 'px' @@ -68,9 +96,9 @@ const Popover: React.FC<{ const onMouseMove = (e: MouseEvent) => { if ( e.clientX < pos.left - pointerDistanceThreshold || - e.clientX > pos.left + rect.width + pointerDistanceThreshold || + e.clientX > pos.left + containerRect.width + pointerDistanceThreshold || e.clientY < pos.top - pointerDistanceThreshold || - e.clientY > pos.top + rect.height + pointerDistanceThreshold + e.clientY > pos.top + containerRect.height + pointerDistanceThreshold ) { props.onRequestClose() } @@ -89,10 +117,21 @@ const Popover: React.FC<{ window.removeEventListener('mousemove', onMouseMove) window.removeEventListener('mousedown', onMouseDown, {capture: true}) } - }, [rect, container, props.clickPoint, windowSize, props.onRequestClose]) + }, [ + containerRect, + container, + props.clickPoint, + props.target, + targetRect, + windowSize, + props.onRequestClose, + ]) return createPortal( - {props.children()}, + + + {props.children()} + , getStudio()!.ui.containerShadow, ) } diff --git a/theatre/studio/src/uiComponents/Popover/PopoverArrow.tsx b/theatre/studio/src/uiComponents/Popover/PopoverArrow.tsx new file mode 100644 index 0000000..9fa10f5 --- /dev/null +++ b/theatre/studio/src/uiComponents/Popover/PopoverArrow.tsx @@ -0,0 +1,38 @@ +import React, {forwardRef} from 'react' +import {GoTriangleUp} from 'react-icons/all' +import styled from 'styled-components' +import {popoverBackgroundColor} from './Popover' + +const Container = styled.div` + font-size: 18px; + position: absolute; + width: 0; + height: 0; + + color: ${() => popoverBackgroundColor}; +` + +const Adjust = styled.div` + width: 20px; + height: 18px; + position: absolute; + left: -10px; + top: -11px; + text-align: center; +` + +type Props = { + className?: string +} + +const PopoverArrow = forwardRef(({className}, ref) => { + return ( + + + + + + ) +}) + +export default PopoverArrow