WIP: Popovers and tooltips
This commit is contained in:
parent
c20b065bae
commit
e63d273830
17 changed files with 447 additions and 49 deletions
|
@ -12,6 +12,7 @@ import {PortalContext} from 'reakit'
|
|||
import type {$IntentionalAny} from '@theatre/shared/utils/types'
|
||||
import useKeyboardShortcuts from './useKeyboardShortcuts'
|
||||
import PointerEventsHandler from '@theatre/studio/uiComponents/PointerEventsHandler'
|
||||
import TooltipContext from '@theatre/studio/uiComponents/Popover/TooltipContext'
|
||||
|
||||
const GlobalStyle = createGlobalStyle`
|
||||
:host {
|
||||
|
@ -74,12 +75,14 @@ export default function UIRoot() {
|
|||
<GlobalStyle />
|
||||
<ProvideTheme>
|
||||
<PortalContext.Provider value={portalLayer}>
|
||||
<TooltipContext portalTarget={portalLayer}>
|
||||
<Container>
|
||||
<PortalLayer ref={portalLayerRef} />
|
||||
{shouldShowGlobalToolbar && <GlobalToolbar />}
|
||||
{shouldShowTrigger && <TheTrigger />}
|
||||
{shouldShowPanels && <PanelsRoot />}
|
||||
</Container>
|
||||
</TooltipContext>
|
||||
</PortalContext.Provider>
|
||||
</ProvideTheme>
|
||||
</>
|
||||
|
|
|
@ -8,6 +8,9 @@ import {VscListTree} from 'react-icons/all'
|
|||
import {usePrism} from '@theatre/dataverse-react'
|
||||
import getStudio from '@theatre/studio/getStudio'
|
||||
import {val} from '@theatre/dataverse'
|
||||
import useTooltip from '@theatre/studio/uiComponents/Popover/useTooltip'
|
||||
import type {$IntentionalAny} from '@theatre/shared/utils/types'
|
||||
import BasicTooltip from '@theatre/studio/uiComponents/Popover/BasicTooltip'
|
||||
|
||||
const Container = styled.div`
|
||||
background-color: transparent;
|
||||
|
@ -29,8 +32,7 @@ const Container = styled.div`
|
|||
width: 20px;
|
||||
${pointerEventsAutoInNormalMode};
|
||||
}
|
||||
|
||||
&:hover:before {
|
||||
ب &:hover:before {
|
||||
top: -12px;
|
||||
width: 300px;
|
||||
}
|
||||
|
@ -59,6 +61,8 @@ const Content = styled.div`
|
|||
}
|
||||
`
|
||||
|
||||
const ErrorTooltip = styled(BasicTooltip)``
|
||||
|
||||
const headerHeight = `32px`
|
||||
|
||||
const TriggerButton = styled(ToolbarIconButton)`
|
||||
|
@ -128,16 +132,30 @@ const OutlinePanel: React.FC<{}> = (props) => {
|
|||
const ephemeralStateOfAllProjects = val(
|
||||
getStudio().atomP.ephemeral.coreByProject,
|
||||
)
|
||||
return Object.entries(ephemeralStateOfAllProjects).filter(
|
||||
([a, state]) =>
|
||||
return Object.entries(ephemeralStateOfAllProjects)
|
||||
.map(([projectId, state]) => ({projectId, state}))
|
||||
.filter(
|
||||
({state}) =>
|
||||
state.loadingState.type === 'browserStateIsNotBasedOnDiskState',
|
||||
)
|
||||
}, [])
|
||||
|
||||
const [errorTooltip, triggerButtonRef] = useTooltip(
|
||||
{enabled: conflicts.length > 0, delay: 0},
|
||||
() => (
|
||||
<ErrorTooltip>
|
||||
{conflicts.length === 1
|
||||
? `There is a state conflict in project "${conflicts[0].projectId}". Select the project in the outline below in order to fix it.`
|
||||
: `There are ${conflicts.length} projects that have state conflicts. They are highlighted in the outline below. `}
|
||||
</ErrorTooltip>
|
||||
),
|
||||
)
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<TriggerContainer>
|
||||
<TriggerButton title="Outline">
|
||||
{errorTooltip}
|
||||
<TriggerButton ref={triggerButtonRef as $IntentionalAny}>
|
||||
<VscListTree />
|
||||
</TriggerButton>
|
||||
{conflicts.length > 0 ? (
|
||||
|
@ -148,7 +166,6 @@ const OutlinePanel: React.FC<{}> = (props) => {
|
|||
<Title>Outline</Title>
|
||||
</TriggerContainer>
|
||||
<Content>
|
||||
{/* <Header><Title>Outline</Title></Header> */}
|
||||
<Body>
|
||||
<ProjectsList />
|
||||
</Body>
|
||||
|
|
|
@ -133,6 +133,7 @@ const LengthIndicator: React.FC<IProps> = ({layoutP}) => {
|
|||
const [nodeRef, node] = useRefAndState<HTMLDivElement | null>(null)
|
||||
const [isDraggingD] = useDragBulge(node, {layoutP})
|
||||
const [popoverNode, openPopover, closePopover, isPopoverOpen] = usePopover(
|
||||
{},
|
||||
() => {
|
||||
return (
|
||||
<LengthEditorPopover layoutP={layoutP} onRequestClose={closePopover} />
|
||||
|
|
4
theatre/studio/src/uiComponents/Popover/ArrowContext.tsx
Normal file
4
theatre/studio/src/uiComponents/Popover/ArrowContext.tsx
Normal file
|
@ -0,0 +1,4 @@
|
|||
import {createContext} from 'react'
|
||||
|
||||
const ArrowContext = createContext<Record<string, string>>({})
|
||||
export default ArrowContext
|
35
theatre/studio/src/uiComponents/Popover/BasicPopover.tsx
Normal file
35
theatre/studio/src/uiComponents/Popover/BasicPopover.tsx
Normal file
|
@ -0,0 +1,35 @@
|
|||
import type {$IntentionalAny} from '@theatre/shared/utils/types'
|
||||
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import {popoverBackgroundColor} from './Popover'
|
||||
import PopoverArrow, {popoverArrowColor} from './PopoverArrow'
|
||||
|
||||
const Container = styled.div`
|
||||
position: absolute;
|
||||
background: ${popoverBackgroundColor};
|
||||
${popoverArrowColor(popoverBackgroundColor)};
|
||||
color: white;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
cursor: default;
|
||||
${pointerEventsAutoInNormalMode};
|
||||
border-radius: 3px;
|
||||
z-index: 10000;
|
||||
border: 1px solid #505159;
|
||||
box-shadow: 0 6px 8px -4px black, 0 0 0 1px black;
|
||||
backdrop-filter: blur(8px);
|
||||
`
|
||||
|
||||
const BasicPopover: React.FC<{className?: string}> = React.forwardRef(
|
||||
({children, className}, ref) => {
|
||||
return (
|
||||
<Container className={className} ref={ref as $IntentionalAny}>
|
||||
<PopoverArrow />
|
||||
{children}
|
||||
</Container>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
export default BasicPopover
|
9
theatre/studio/src/uiComponents/Popover/BasicTooltip.tsx
Normal file
9
theatre/studio/src/uiComponents/Popover/BasicTooltip.tsx
Normal file
|
@ -0,0 +1,9 @@
|
|||
import styled from 'styled-components'
|
||||
import BasicPopover from './BasicPopover'
|
||||
|
||||
const BasicTooltip = styled(BasicPopover)`
|
||||
padding: 1em;
|
||||
max-width: 240px;
|
||||
`
|
||||
|
||||
export default BasicTooltip
|
|
@ -0,0 +1,7 @@
|
|||
import styled from 'styled-components'
|
||||
import BasicTooltip from './BasicTooltip'
|
||||
|
||||
const MinimalTooltip = styled(BasicTooltip)`
|
||||
padding: 6px;
|
||||
`
|
||||
export default MinimalTooltip
|
|
@ -6,14 +6,11 @@ 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'
|
||||
|
||||
/**
|
||||
* How far from the menu should the pointer travel to auto close the menu
|
||||
*/
|
||||
const defaultPointerDistanceThreshold = 200
|
||||
|
||||
export const popoverBackgroundColor = transparentize(0.2, '#111')
|
||||
export const popoverBackgroundColor = transparentize(0.05, `#2a2a31`)
|
||||
const minimumDistanceOfArrowToEdgeOfPopover = 8
|
||||
|
||||
const Container = styled.ul`
|
||||
|
@ -29,15 +26,12 @@ const Container = styled.ul`
|
|||
`
|
||||
|
||||
const Popover: React.FC<{
|
||||
clickPoint?: {clientX: number; clientY: number}
|
||||
target: HTMLElement
|
||||
onPointerOutOfThreshold: () => void
|
||||
target: Element
|
||||
children: () => React.ReactNode
|
||||
pointerDistanceThreshold?: number
|
||||
className?: string
|
||||
}> = (props) => {
|
||||
const pointerDistanceThreshold =
|
||||
props.pointerDistanceThreshold ?? defaultPointerDistanceThreshold
|
||||
const {pointerDistanceThreshold, onPointerOutOfThreshold} =
|
||||
usePopoverContext()
|
||||
|
||||
const [container, setContainer] = useState<HTMLElement | null>(null)
|
||||
const arrowRef = useRef<HTMLDivElement>(null)
|
||||
|
@ -102,33 +96,26 @@ const Popover: React.FC<{
|
|||
e.clientY < pos.top - pointerDistanceThreshold ||
|
||||
e.clientY > pos.top + containerRect.height + pointerDistanceThreshold
|
||||
) {
|
||||
props.onPointerOutOfThreshold()
|
||||
}
|
||||
}
|
||||
|
||||
const onMouseDown = (e: MouseEvent) => {
|
||||
if (!e.composedPath().includes(container)) {
|
||||
props.onPointerOutOfThreshold()
|
||||
onPointerOutOfThreshold()
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('mousemove', onMouseMove)
|
||||
window.addEventListener('mousedown', onMouseDown, {capture: true})
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', onMouseMove)
|
||||
window.removeEventListener('mousedown', onMouseDown, {capture: true})
|
||||
}
|
||||
}, [
|
||||
containerRect,
|
||||
container,
|
||||
props.clickPoint,
|
||||
props.target,
|
||||
targetRect,
|
||||
windowSize,
|
||||
props.onPointerOutOfThreshold,
|
||||
onPointerOutOfThreshold,
|
||||
])
|
||||
|
||||
useOnClickOutside(container, onPointerOutOfThreshold)
|
||||
|
||||
return createPortal(
|
||||
<Container ref={setContainer} className={props.className}>
|
||||
<PopoverArrow ref={arrowRef} />
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
import React, {forwardRef} from 'react'
|
||||
import React, {forwardRef, useContext} from 'react'
|
||||
import {GoTriangleUp} from 'react-icons/all'
|
||||
import styled from 'styled-components'
|
||||
import {popoverBackgroundColor} from './Popover'
|
||||
import styled, {css} from 'styled-components'
|
||||
import ArrowContext from './ArrowContext'
|
||||
|
||||
export const popoverArrowColor = (color: string) => css`
|
||||
--popover-arrow-color: ${color};
|
||||
`
|
||||
|
||||
const Container = styled.div`
|
||||
font-size: 18px;
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
|
||||
color: ${() => popoverBackgroundColor};
|
||||
color: var(--popover-arrow-color);
|
||||
`
|
||||
|
||||
const Adjust = styled.div`
|
||||
|
@ -23,11 +26,13 @@ const Adjust = styled.div`
|
|||
|
||||
type Props = {
|
||||
className?: string
|
||||
color?: string
|
||||
}
|
||||
|
||||
const PopoverArrow = forwardRef<HTMLDivElement, Props>(({className}, ref) => {
|
||||
const arrowStyle = useContext(ArrowContext)
|
||||
return (
|
||||
<Container className={className} ref={ref}>
|
||||
<Container className={className} ref={ref} style={{...arrowStyle}}>
|
||||
<Adjust>
|
||||
<GoTriangleUp size={'18px'} />
|
||||
</Adjust>
|
||||
|
|
36
theatre/studio/src/uiComponents/Popover/PopoverContext.tsx
Normal file
36
theatre/studio/src/uiComponents/Popover/PopoverContext.tsx
Normal file
|
@ -0,0 +1,36 @@
|
|||
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>
|
||||
)
|
||||
}
|
63
theatre/studio/src/uiComponents/Popover/TooltipContext.tsx
Normal file
63
theatre/studio/src/uiComponents/Popover/TooltipContext.tsx
Normal file
|
@ -0,0 +1,63 @@
|
|||
import {Box} from '@theatre/dataverse'
|
||||
import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
||||
import React, {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
|
||||
const ctx = createContext<{
|
||||
currentTooltipId: Box<number>
|
||||
portalTarget: HTMLElement
|
||||
}>(null!)
|
||||
|
||||
let lastTooltipId = 0
|
||||
|
||||
export const useTooltipOpenState = (): [
|
||||
isOpen: boolean,
|
||||
setIsOpen: (isOpen: boolean, delay: number) => void,
|
||||
] => {
|
||||
const id = useMemo(() => lastTooltipId++, [])
|
||||
const {currentTooltipId} = useContext(ctx)
|
||||
const [isOpenRef, isOpen] = useRefAndState<boolean>(false)
|
||||
|
||||
const setIsOpen = useCallback((shouldOpen: boolean, delay: number) => {
|
||||
if (shouldOpen) {
|
||||
if (currentTooltipId.get() !== id) {
|
||||
currentTooltipId.set(id)
|
||||
}
|
||||
} else {
|
||||
if (currentTooltipId.get() === id) {
|
||||
currentTooltipId.set(-1)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const {derivation} = currentTooltipId
|
||||
return derivation.changesWithoutValues().tap(() => {
|
||||
const flag = derivation.getValue() === id
|
||||
|
||||
if (isOpenRef.current !== flag) isOpenRef.current = flag
|
||||
})
|
||||
}, [currentTooltipId, id])
|
||||
|
||||
return [isOpen, setIsOpen]
|
||||
}
|
||||
|
||||
const TooltipContext: React.FC<{portalTarget: HTMLElement}> = ({
|
||||
children,
|
||||
portalTarget,
|
||||
}) => {
|
||||
const currentTooltipId = useMemo(() => new Box(-1), [])
|
||||
|
||||
return (
|
||||
<ctx.Provider value={{currentTooltipId, portalTarget}}>
|
||||
{children}
|
||||
</ctx.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default TooltipContext
|
88
theatre/studio/src/uiComponents/Popover/TooltipWrapper.tsx
Normal file
88
theatre/studio/src/uiComponents/Popover/TooltipWrapper.tsx
Normal file
|
@ -0,0 +1,88 @@
|
|||
import React from 'react'
|
||||
import {cloneElement, useLayoutEffect, useState} from 'react'
|
||||
import useWindowSize from 'react-use/esm/useWindowSize'
|
||||
import useBoundingClientRect from '@theatre/studio/uiComponents/useBoundingClientRect'
|
||||
import ArrowContext from './ArrowContext'
|
||||
import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
||||
|
||||
const minimumDistanceOfArrowToEdgeOfPopover = 8
|
||||
|
||||
const TooltipWrapper: React.FC<{
|
||||
target: HTMLElement | SVGElement
|
||||
children: () => React.ReactElement
|
||||
}> = (props) => {
|
||||
const originalElement = props.children()
|
||||
const [ref, container] = useRefAndState<HTMLElement | SVGElement | null>(null)
|
||||
const style: Record<string, string> = originalElement.props.style
|
||||
? {...originalElement.props.style}
|
||||
: {}
|
||||
style.position = 'absolute'
|
||||
|
||||
const containerRect = useBoundingClientRect(container)
|
||||
const targetRect = useBoundingClientRect(props.target)
|
||||
const windowSize = useWindowSize()
|
||||
const [arrowContextValue, setArrowContextValue] = useState<
|
||||
Record<string, string>
|
||||
>({})
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!containerRect || !container || !targetRect) return
|
||||
|
||||
const gap = 8
|
||||
const arrowStyle: Record<string, string> = {}
|
||||
|
||||
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
|
||||
arrowStyle.top = '0px'
|
||||
} else if (targetRect.top > containerRect.height + gap) {
|
||||
verticalPlacement = 'top'
|
||||
top = targetRect.top - (containerRect.height + gap)
|
||||
arrowStyle.bottom = '0px'
|
||||
arrowStyle.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
|
||||
}
|
||||
arrowStyle.left = arrowLeft + 'px'
|
||||
}
|
||||
|
||||
const pos = {left, top}
|
||||
|
||||
container.style.left = pos.left + 'px'
|
||||
container.style.top = pos.top + 'px'
|
||||
setArrowContextValue(arrowStyle)
|
||||
|
||||
return () => {}
|
||||
}, [containerRect, container, props.target, targetRect, windowSize])
|
||||
|
||||
return (
|
||||
<ArrowContext.Provider value={arrowContextValue}>
|
||||
{cloneElement(originalElement, {ref, style})}
|
||||
</ArrowContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default TooltipWrapper
|
|
@ -1,5 +1,6 @@
|
|||
import noop from '@theatre/shared/utils/noop'
|
||||
import React, {useCallback, useState} from 'react'
|
||||
import Popover from './Popover'
|
||||
import {PopoverContextProvider} from './PopoverContext'
|
||||
|
||||
type OpenFn = (e: React.MouseEvent, target: HTMLElement) => void
|
||||
type CloseFn = () => void
|
||||
|
@ -15,6 +16,10 @@ type State =
|
|||
}
|
||||
|
||||
export default function usePopover(
|
||||
opts: {
|
||||
closeWhenPointerIsDistant?: boolean
|
||||
pointerDistanceThreshold?: number
|
||||
},
|
||||
render: () => React.ReactNode,
|
||||
): [node: React.ReactNode, open: OpenFn, close: CloseFn, isOpen: boolean] {
|
||||
const [state, setState] = useState<State>({
|
||||
|
@ -34,13 +39,22 @@ export default function usePopover(
|
|||
}, [])
|
||||
|
||||
const node = state.isOpen ? (
|
||||
<Popover
|
||||
<PopoverContextProvider
|
||||
children={render}
|
||||
clickPoint={state.clickPoint}
|
||||
target={state.target}
|
||||
onPointerOutOfThreshold={close}
|
||||
triggerPoint={state.clickPoint}
|
||||
pointerDistanceThreshold={opts.pointerDistanceThreshold}
|
||||
onPointerOutOfThreshold={
|
||||
opts.closeWhenPointerIsDistant === false ? noop : close
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
// <Popover
|
||||
// children={render}
|
||||
// triggerPoint={state.clickPoint}
|
||||
// target={state.target}
|
||||
// onPointerOutOfThreshold={
|
||||
// }
|
||||
// />
|
||||
<></>
|
||||
)
|
||||
|
||||
|
|
56
theatre/studio/src/uiComponents/Popover/useTooltip.tsx
Normal file
56
theatre/studio/src/uiComponents/Popover/useTooltip.tsx
Normal file
|
@ -0,0 +1,56 @@
|
|||
import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
||||
import type {MutableRefObject} from 'react'
|
||||
import {useEffect} from 'react'
|
||||
import React from 'react'
|
||||
import TooltipWrapper from './TooltipWrapper'
|
||||
import getStudio from '@theatre/studio/getStudio'
|
||||
import {createPortal} from 'react-dom'
|
||||
import {useTooltipOpenState} from './TooltipContext'
|
||||
|
||||
export default function useTooltip(
|
||||
opts: {enabled?: boolean; delay?: number},
|
||||
render: () => React.ReactElement,
|
||||
): [
|
||||
node: React.ReactNode,
|
||||
targetRef: MutableRefObject<HTMLElement | SVGElement | null>,
|
||||
isOpen: boolean,
|
||||
] {
|
||||
const enabled = opts.enabled !== false
|
||||
const [isOpen, setIsOpen] = useTooltipOpenState()
|
||||
|
||||
const [targetRef, targetNode] = useRefAndState<
|
||||
HTMLElement | SVGElement | null
|
||||
>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) {
|
||||
return
|
||||
}
|
||||
|
||||
const target = targetRef.current
|
||||
if (!target) return
|
||||
|
||||
const onMouseEnter = () => setIsOpen(true, opts.delay ?? 200)
|
||||
const onMouseLeave = () => setIsOpen(false, opts.delay ?? 200)
|
||||
|
||||
target.addEventListener('mouseenter', onMouseEnter)
|
||||
target.addEventListener('mouseleave', onMouseLeave)
|
||||
|
||||
return () => {
|
||||
target.removeEventListener('mouseenter', onMouseEnter)
|
||||
target.removeEventListener('mouseleave', onMouseLeave)
|
||||
}
|
||||
}, [targetRef, enabled, opts.delay])
|
||||
|
||||
const node =
|
||||
enabled && isOpen && targetNode ? (
|
||||
createPortal(
|
||||
<TooltipWrapper children={render} target={targetNode} />,
|
||||
getStudio()!.ui.containerShadow,
|
||||
)
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
|
||||
return [node, targetRef, isOpen]
|
||||
}
|
31
theatre/studio/src/uiComponents/onPointerOutside.ts
Normal file
31
theatre/studio/src/uiComponents/onPointerOutside.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import {useEffect} from 'react'
|
||||
import useBoundingClientRect from './useBoundingClientRect'
|
||||
|
||||
export default function onPointerOutside(
|
||||
container: Element | null,
|
||||
threshold: number,
|
||||
onPointerOutside: () => void,
|
||||
) {
|
||||
const containerRect = useBoundingClientRect(container)
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRect) return
|
||||
|
||||
const onMouseMove = (e: MouseEvent) => {
|
||||
if (
|
||||
e.clientX < containerRect.left - threshold ||
|
||||
e.clientX > containerRect.left + containerRect.width + threshold ||
|
||||
e.clientY < containerRect.top - threshold ||
|
||||
e.clientY > containerRect.top + containerRect.height + threshold
|
||||
) {
|
||||
onPointerOutside()
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('mousemove', onMouseMove)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', onMouseMove)
|
||||
}
|
||||
}, [containerRect, threshold])
|
||||
}
|
|
@ -2,10 +2,15 @@ import styled from 'styled-components'
|
|||
import {outlinePanelTheme} from '@theatre/studio/panels/OutlinePanel/BaseItem'
|
||||
import {darken, opacify} from 'polished'
|
||||
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||
import React from 'react'
|
||||
import type {$IntentionalAny} from '@theatre/shared/utils/types'
|
||||
import useTooltip from '@theatre/studio/uiComponents/Popover/useTooltip'
|
||||
import mergeRefs from 'react-merge-refs'
|
||||
import MinimalTooltip from '@theatre/studio/uiComponents/Popover/MinimalTooltip'
|
||||
|
||||
const {baseBg, baseBorderColor, baseFontColor} = outlinePanelTheme
|
||||
|
||||
const ToolbarIconButton = styled.button`
|
||||
const Container = styled.button`
|
||||
${pointerEventsAutoInNormalMode};
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
@ -52,4 +57,20 @@ const ToolbarIconButton = styled.button`
|
|||
border: 0;
|
||||
`
|
||||
|
||||
const ToolbarIconButton: typeof Container = React.forwardRef(
|
||||
({title, ...props}, ref: $IntentionalAny) => {
|
||||
const [tooltip, localRef] = useTooltip(
|
||||
{enabled: typeof title === 'string'},
|
||||
() => <MinimalTooltip>{title}</MinimalTooltip>,
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
{tooltip}
|
||||
<Container ref={mergeRefs([localRef, ref])} {...props} />{' '}
|
||||
</>
|
||||
)
|
||||
},
|
||||
) as $IntentionalAny
|
||||
|
||||
export default ToolbarIconButton
|
||||
|
|
21
theatre/studio/src/uiComponents/useOnClickOutside.ts
Normal file
21
theatre/studio/src/uiComponents/useOnClickOutside.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import {useEffect} from 'react'
|
||||
|
||||
export default function useOnClickOutside(
|
||||
container: Element | null,
|
||||
onOutside: (e: MouseEvent) => void,
|
||||
) {
|
||||
useEffect(() => {
|
||||
if (!container) return
|
||||
|
||||
const onMouseDown = (e: MouseEvent) => {
|
||||
if (!e.composedPath().includes(container)) {
|
||||
onOutside(e)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('mousedown', onMouseDown, {capture: true})
|
||||
return () => {
|
||||
window.removeEventListener('mousedown', onMouseDown, {capture: true})
|
||||
}
|
||||
}, [container, onOutside])
|
||||
}
|
Loading…
Reference in a new issue