Simplified the popovers

This commit is contained in:
Aria Minaei 2021-08-19 21:41:41 +02:00
parent 69f787d5cf
commit af2ff59d5e
8 changed files with 33 additions and 196 deletions

View file

@ -57,6 +57,10 @@ export default function UIRoot() {
const [portalLayerRef, portalLayer] = useRefAndState<HTMLDivElement>( const [portalLayerRef, portalLayer] = useRefAndState<HTMLDivElement>(
undefined as $IntentionalAny, undefined as $IntentionalAny,
) )
const [containerRef, container] = useRefAndState<HTMLDivElement>(
undefined as $IntentionalAny,
)
useKeyboardShortcuts() useKeyboardShortcuts()
const inside = usePrism(() => { const inside = usePrism(() => {
const visiblityState = val(studio.atomP.ahistoric.visibilityState) const visiblityState = val(studio.atomP.ahistoric.visibilityState)
@ -75,7 +79,7 @@ export default function UIRoot() {
<GlobalStyle /> <GlobalStyle />
<ProvideTheme> <ProvideTheme>
<PortalContext.Provider value={portalLayer}> <PortalContext.Provider value={portalLayer}>
<TooltipContext portalTarget={portalLayer}> <TooltipContext>
<Container> <Container>
<PortalLayer ref={portalLayerRef} /> <PortalLayer ref={portalLayerRef} />
{shouldShowGlobalToolbar && <GlobalToolbar />} {shouldShowGlobalToolbar && <GlobalToolbar />}

View file

@ -19,6 +19,7 @@ import {
import {GoChevronLeft, GoChevronRight} from 'react-icons/all' import {GoChevronLeft, GoChevronRight} from 'react-icons/all'
import LengthEditorPopover from './LengthEditorPopover' import LengthEditorPopover from './LengthEditorPopover'
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css' import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
import BasicPopover from '@theatre/studio/uiComponents/Popover/BasicPopover'
const coverWidth = 1000 const coverWidth = 1000
@ -136,7 +137,12 @@ const LengthIndicator: React.FC<IProps> = ({layoutP}) => {
{}, {},
() => { () => {
return ( return (
<LengthEditorPopover layoutP={layoutP} onRequestClose={closePopover} /> <BasicPopover>
<LengthEditorPopover
layoutP={layoutP}
onRequestClose={closePopover}
/>
</BasicPopover>
) )
}, },
) )

View file

@ -1,10 +1,12 @@
import type {$IntentionalAny} from '@theatre/shared/utils/types' import type {$IntentionalAny} from '@theatre/shared/utils/types'
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css' import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
import {transparentize} from 'polished'
import React from 'react' import React from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import {popoverBackgroundColor} from './Popover'
import PopoverArrow, {popoverArrowColor} from './PopoverArrow' import PopoverArrow, {popoverArrowColor} from './PopoverArrow'
export const popoverBackgroundColor = transparentize(0.05, `#2a2a31`)
const Container = styled.div` const Container = styled.div`
position: absolute; position: absolute;
background: ${popoverBackgroundColor}; background: ${popoverBackgroundColor};

View file

@ -1,128 +0,0 @@
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
import getStudio from '@theatre/studio/getStudio'
import useBoundingClientRect from '@theatre/studio/uiComponents/useBoundingClientRect'
import transparentize from 'polished/lib/color/transparentize'
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 useOnClickOutside from '@theatre/studio/uiComponents/useOnClickOutside'
import PopoverArrow from './PopoverArrow'
import {usePopoverContext} from './PopoverContext'
export const popoverBackgroundColor = transparentize(0.05, `#2a2a31`)
const minimumDistanceOfArrowToEdgeOfPopover = 8
const Container = styled.ul`
position: absolute;
z-index: 10000;
background: ${popoverBackgroundColor};
color: white;
padding: 0;
margin: 0;
cursor: default;
${pointerEventsAutoInNormalMode};
border-radius: 3px;
`
const Popover: React.FC<{
target: Element
children: () => React.ReactNode
className?: string
}> = (props) => {
const {pointerDistanceThreshold, onPointerOutOfThreshold} =
usePopoverContext()
const [container, setContainer] = useState<HTMLElement | null>(null)
const arrowRef = useRef<HTMLDivElement>(null)
const containerRect = useBoundingClientRect(container)
const targetRect = useBoundingClientRect(props.target)
const windowSize = useWindowSize()
useLayoutEffect(() => {
if (!containerRect || !container || !targetRect) return
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'
}
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'
}
const pos = {left, top}
container.style.left = pos.left + 'px'
container.style.top = pos.top + 'px'
const onMouseMove = (e: MouseEvent) => {
if (
e.clientX < pos.left - pointerDistanceThreshold ||
e.clientX > pos.left + containerRect.width + pointerDistanceThreshold ||
e.clientY < pos.top - pointerDistanceThreshold ||
e.clientY > pos.top + containerRect.height + pointerDistanceThreshold
) {
onPointerOutOfThreshold()
}
}
window.addEventListener('mousemove', onMouseMove)
return () => {
window.removeEventListener('mousemove', onMouseMove)
}
}, [
containerRect,
container,
props.target,
targetRect,
windowSize,
onPointerOutOfThreshold,
])
useOnClickOutside(container, onPointerOutOfThreshold)
return createPortal(
<Container ref={setContainer} className={props.className}>
<PopoverArrow ref={arrowRef} />
{props.children()}
</Container>,
getStudio()!.ui.containerShadow,
)
}
export default Popover

View file

@ -1,36 +0,0 @@
import type {$IntentionalAny} from '@theatre/shared/utils/types'
import {createContext, useContext, default as React} from 'react'
export type PopoverContext = {
triggerPoint?: {clientX: number; clientY: number}
onPointerOutOfThreshold: () => void
/**
* How far from the menu should the pointer travel to auto close the menu
*/
pointerDistanceThreshold: number
}
const defaultPointerDistanceThreshold = 200
const ctx = createContext<PopoverContext>(null as $IntentionalAny)
export const usePopoverContext = () => useContext(ctx)
export const PopoverContextProvider: React.FC<{
triggerPoint: PopoverContext['triggerPoint']
onPointerOutOfThreshold: PopoverContext['onPointerOutOfThreshold']
pointerDistanceThreshold?: number
}> = ({
children,
triggerPoint,
pointerDistanceThreshold = defaultPointerDistanceThreshold,
onPointerOutOfThreshold,
}) => {
return (
<ctx.Provider
value={{triggerPoint, pointerDistanceThreshold, onPointerOutOfThreshold}}
>
{children}
</ctx.Provider>
)
}

View file

@ -10,7 +10,6 @@ import React, {
} from 'react' } from 'react'
const ctx = createContext<{ const ctx = createContext<{
portalTarget: HTMLElement
cur: IDerivation<number> cur: IDerivation<number>
set: (id: number, delay: number) => void set: (id: number, delay: number) => void
}>(null!) }>(null!)
@ -40,10 +39,7 @@ export const useTooltipOpenState = (): [
return [isOpen, setIsOpen] return [isOpen, setIsOpen]
} }
const TooltipContext: React.FC<{portalTarget: HTMLElement}> = ({ const TooltipContext: React.FC<{}> = ({children}) => {
children,
portalTarget,
}) => {
const currentTooltipId = useMemo(() => new Box(-1), []) const currentTooltipId = useMemo(() => new Box(-1), [])
const cur = currentTooltipId.derivation const cur = currentTooltipId.derivation
@ -65,9 +61,7 @@ const TooltipContext: React.FC<{portalTarget: HTMLElement}> = ({
} }
}, []) }, [])
return ( return <ctx.Provider value={{cur, set}}>{children}</ctx.Provider>
<ctx.Provider value={{cur, set, portalTarget}}>{children}</ctx.Provider>
)
} }
export default TooltipContext export default TooltipContext

View file

@ -1,6 +1,7 @@
import noop from '@theatre/shared/utils/noop' import React, {useCallback, useContext, useState} from 'react'
import React, {useCallback, useState} from 'react' import {createPortal} from 'react-dom'
import {PopoverContextProvider} from './PopoverContext' import {PortalContext} from 'reakit'
import TooltipWrapper from './TooltipWrapper'
type OpenFn = (e: React.MouseEvent, target: HTMLElement) => void type OpenFn = (e: React.MouseEvent, target: HTMLElement) => void
type CloseFn = () => void type CloseFn = () => void
@ -20,7 +21,7 @@ export default function usePopover(
closeWhenPointerIsDistant?: boolean closeWhenPointerIsDistant?: boolean
pointerDistanceThreshold?: number pointerDistanceThreshold?: number
}, },
render: () => React.ReactNode, render: () => React.ReactElement,
): [node: React.ReactNode, open: OpenFn, close: CloseFn, isOpen: boolean] { ): [node: React.ReactNode, open: OpenFn, close: CloseFn, isOpen: boolean] {
const [state, setState] = useState<State>({ const [state, setState] = useState<State>({
isOpen: false, isOpen: false,
@ -38,23 +39,14 @@ export default function usePopover(
setState({isOpen: false}) setState({isOpen: false})
}, []) }, [])
const portalLayer = useContext(PortalContext)
const node = state.isOpen ? ( const node = state.isOpen ? (
<PopoverContextProvider createPortal(
children={render} <TooltipWrapper children={render} target={state.target} />,
triggerPoint={state.clickPoint} portalLayer!,
pointerDistanceThreshold={opts.pointerDistanceThreshold} )
onPointerOutOfThreshold={
opts.closeWhenPointerIsDistant === false ? noop : close
}
/>
) : ( ) : (
// <Popover
// children={render}
// triggerPoint={state.clickPoint}
// target={state.target}
// onPointerOutOfThreshold={
// }
// />
<></> <></>
) )

View file

@ -1,11 +1,12 @@
import useRefAndState from '@theatre/studio/utils/useRefAndState' import useRefAndState from '@theatre/studio/utils/useRefAndState'
import type {MutableRefObject} from 'react' import type {MutableRefObject} from 'react'
import {useContext} from 'react'
import {useEffect} from 'react' import {useEffect} from 'react'
import React from 'react' import React from 'react'
import TooltipWrapper from './TooltipWrapper' import TooltipWrapper from './TooltipWrapper'
import getStudio from '@theatre/studio/getStudio'
import {createPortal} from 'react-dom' import {createPortal} from 'react-dom'
import {useTooltipOpenState} from './TooltipContext' import {useTooltipOpenState} from './TooltipContext'
import {PortalContext} from 'reakit'
export default function useTooltip( export default function useTooltip(
opts: {enabled?: boolean; delay?: number}, opts: {enabled?: boolean; delay?: number},
@ -42,11 +43,13 @@ export default function useTooltip(
} }
}, [targetRef, enabled, opts.delay]) }, [targetRef, enabled, opts.delay])
const portalLayer = useContext(PortalContext)
const node = const node =
enabled && isOpen && targetNode ? ( enabled && isOpen && targetNode ? (
createPortal( createPortal(
<TooltipWrapper children={render} target={targetNode} />, <TooltipWrapper children={render} target={targetNode} />,
getStudio()!.ui.containerShadow, portalLayer!,
) )
) : ( ) : (
<></> <></>