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 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<HTMLElement | null>(null)
const rect = useBoundingClientRect(container)
const arrowRef = useRef<HTMLDivElement>(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(
<Container ref={setContainer}>{props.children()}</Container>,
<Container ref={setContainer}>
<PopoverArrow ref={arrowRef} />
{props.children()}
</Container>,
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