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 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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
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