Style and placement logic for usePopover()
This commit is contained in:
parent
58e9d9ff8b
commit
ab37d1b21d
2 changed files with 102 additions and 25 deletions
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
38
theatre/studio/src/uiComponents/Popover/PopoverArrow.tsx
Normal file
38
theatre/studio/src/uiComponents/Popover/PopoverArrow.tsx
Normal 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
|
Loading…
Reference in a new issue