Style and placement logic for usePopover()

This commit is contained in:
Aria Minaei 2021-08-02 23:02:03 +02:00
parent 58e9d9ff8b
commit ab37d1b21d
2 changed files with 102 additions and 25 deletions

View file

@ -1,20 +1,24 @@
import getStudio from '@theatre/studio/getStudio' import getStudio from '@theatre/studio/getStudio'
import useBoundingClientRect from '@theatre/studio/uiComponents/useBoundingClientRect' import useBoundingClientRect from '@theatre/studio/uiComponents/useBoundingClientRect'
import transparentize from 'polished/lib/color/transparentize' 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 {createPortal} from 'react-dom'
import useWindowSize from 'react-use/esm/useWindowSize' import useWindowSize from 'react-use/esm/useWindowSize'
import styled from 'styled-components' import styled from 'styled-components'
import PopoverArrow from './PopoverArrow'
/** /**
* How far from the menu should the pointer travel to auto close the menu * How far from the menu should the pointer travel to auto close the menu
*/ */
const defaultPointerDistanceThreshold = 200 const defaultPointerDistanceThreshold = 200
export const popoverBackgroundColor = transparentize(0.2, '#111')
const minimumDistanceOfArrowToEdgeOfPopover = 8
const Container = styled.ul` const Container = styled.ul`
position: absolute; position: absolute;
z-index: 10000; z-index: 10000;
background: ${transparentize(0.2, '#111')}; background: ${popoverBackgroundColor};
color: white; color: white;
padding: 0; padding: 0;
margin: 0; margin: 0;
@ -34,33 +38,57 @@ const Popover: React.FC<{
props.pointerDistanceThreshold ?? defaultPointerDistanceThreshold props.pointerDistanceThreshold ?? defaultPointerDistanceThreshold
const [container, setContainer] = useState<HTMLElement | null>(null) const [container, setContainer] = useState<HTMLElement | null>(null)
const rect = useBoundingClientRect(container) const arrowRef = useRef<HTMLDivElement>(null)
const containerRect = useBoundingClientRect(container)
const targetRect = useBoundingClientRect(props.target)
const windowSize = useWindowSize() const windowSize = useWindowSize()
useLayoutEffect(() => { useLayoutEffect(() => {
if (!rect || !container) return if (!containerRect || !container || !targetRect) return
const preferredAnchorPoint = { const gap = 8
left: rect.width / 2, const arrow = arrowRef.current!
top: 0,
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 = { let arrowLeft = 0
left: props.clickPoint.clientX - preferredAnchorPoint.left, if (verticalPlacement !== 'overlay') {
top: props.clickPoint.clientY - preferredAnchorPoint.top, 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) { const pos = {left, top}
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
}
container.style.left = pos.left + 'px' container.style.left = pos.left + 'px'
container.style.top = pos.top + 'px' container.style.top = pos.top + 'px'
@ -68,9 +96,9 @@ const Popover: React.FC<{
const onMouseMove = (e: MouseEvent) => { const onMouseMove = (e: MouseEvent) => {
if ( if (
e.clientX < pos.left - pointerDistanceThreshold || 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 - pointerDistanceThreshold ||
e.clientY > pos.top + rect.height + pointerDistanceThreshold e.clientY > pos.top + containerRect.height + pointerDistanceThreshold
) { ) {
props.onRequestClose() props.onRequestClose()
} }
@ -89,10 +117,21 @@ const Popover: React.FC<{
window.removeEventListener('mousemove', onMouseMove) window.removeEventListener('mousemove', onMouseMove)
window.removeEventListener('mousedown', onMouseDown, {capture: true}) 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( return createPortal(
<Container ref={setContainer}>{props.children()}</Container>, <Container ref={setContainer}>
<PopoverArrow ref={arrowRef} />
{props.children()}
</Container>,
getStudio()!.ui.containerShadow, getStudio()!.ui.containerShadow,
) )
} }

View file

@ -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<HTMLDivElement, Props>(({className}, ref) => {
return (
<Container className={className} ref={ref}>
<Adjust>
<GoTriangleUp size={'18px'} />
</Adjust>
</Container>
)
})
export default PopoverArrow