UX Improvements
* Length of the sequence has a clear indicator now * The frame stamp snaps to the time of keyframes when they're hovered * The frame stamp hides when hovering over elements over which it wouldn't be helpful
This commit is contained in:
parent
c6c37992ac
commit
0a3c699180
13 changed files with 266 additions and 59 deletions
|
@ -12,6 +12,11 @@ import {lighten} from 'polished'
|
|||
import React, {useMemo, useRef} from 'react'
|
||||
import styled from 'styled-components'
|
||||
import type KeyframeEditor from './KeyframeEditor'
|
||||
import type {FrameStampPositionLock} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||
import {
|
||||
attributeNameThatLocksFramestamp,
|
||||
useFrameStampPosition,
|
||||
} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||
|
||||
export const dotSize = 6
|
||||
const hitZoneSize = 12
|
||||
|
@ -64,8 +69,12 @@ const Dot: React.FC<IProps> = (props) => {
|
|||
<>
|
||||
<HitZone
|
||||
ref={ref}
|
||||
title={props.keyframe.position.toString()}
|
||||
title={props.keyframe.position.toFixed(8)}
|
||||
data-pos={props.keyframe.position.toFixed(3)}
|
||||
{...{
|
||||
[attributeNameThatLocksFramestamp]:
|
||||
props.keyframe.position.toFixed(3),
|
||||
}}
|
||||
/>
|
||||
<Square isSelected={!!props.selection} />
|
||||
{contextMenu}
|
||||
|
@ -103,6 +112,8 @@ function useKeyframeContextMenu(node: HTMLDivElement | null, props: IProps) {
|
|||
}
|
||||
|
||||
function useDragKeyframe(node: HTMLDivElement | null, props: IProps) {
|
||||
const {getLock} = useFrameStampPosition()
|
||||
|
||||
const propsRef = useRef(props)
|
||||
propsRef.current = props
|
||||
|
||||
|
@ -110,6 +121,8 @@ function useDragKeyframe(node: HTMLDivElement | null, props: IProps) {
|
|||
let toUnitSpace: SequenceEditorPanelLayout['scaledSpace']['toUnitSpace']
|
||||
let tempTransaction: CommitOrDiscard | undefined
|
||||
let propsAtStartOfDrag: IProps
|
||||
let frameStampPositionLock: FrameStampPositionLock
|
||||
|
||||
let selectionDragHandlers:
|
||||
| ReturnType<DopeSheetSelection['getDragHandlers']>
|
||||
| undefined
|
||||
|
@ -131,8 +144,10 @@ function useDragKeyframe(node: HTMLDivElement | null, props: IProps) {
|
|||
}
|
||||
|
||||
propsAtStartOfDrag = propsRef.current
|
||||
|
||||
toUnitSpace = val(propsAtStartOfDrag.layoutP.scaledSpace.toUnitSpace)
|
||||
|
||||
frameStampPositionLock = getLock()
|
||||
frameStampPositionLock.set(propsAtStartOfDrag.keyframe.position)
|
||||
},
|
||||
onDrag(dx, dy, event) {
|
||||
if (selectionDragHandlers) {
|
||||
|
@ -140,6 +155,10 @@ function useDragKeyframe(node: HTMLDivElement | null, props: IProps) {
|
|||
return
|
||||
}
|
||||
const delta = toUnitSpace(dx)
|
||||
const original =
|
||||
propsAtStartOfDrag.trackData.keyframes[propsAtStartOfDrag.index]
|
||||
frameStampPositionLock.set(original.position + delta)
|
||||
|
||||
if (tempTransaction) {
|
||||
tempTransaction.discard()
|
||||
tempTransaction = undefined
|
||||
|
@ -158,6 +177,8 @@ function useDragKeyframe(node: HTMLDivElement | null, props: IProps) {
|
|||
})
|
||||
},
|
||||
onDragEnd(dragHappened) {
|
||||
frameStampPositionLock.unlock()
|
||||
|
||||
if (selectionDragHandlers) {
|
||||
selectionDragHandlers.onDragEnd?.(dragHappened)
|
||||
|
||||
|
|
|
@ -12,46 +12,58 @@ import type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore'
|
|||
import useDrag from '@theatre/studio/uiComponents/useDrag'
|
||||
import getStudio from '@theatre/studio/getStudio'
|
||||
import type Sheet from '@theatre/core/sheets/Sheet'
|
||||
import usePopover from '@theatre/studio/uiComponents/Popover/usePopover'
|
||||
import {attributeNameThatLocksFramestamp} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||
|
||||
const coverWidth = 1000
|
||||
|
||||
const colors = {
|
||||
stripNormal: `#0000006c`,
|
||||
stripActive: `#000000`,
|
||||
}
|
||||
|
||||
const Strip = styled.div`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 4px;
|
||||
z-index: ${() => zIndexes.lengthIndicatorStrip};
|
||||
pointer-events: auto;
|
||||
cursor: ew-resize;
|
||||
pointer-events: none;
|
||||
|
||||
&:after {
|
||||
display: block;
|
||||
content: ' ';
|
||||
position: absolute;
|
||||
top: ${topStripHeight}px;
|
||||
/* top: ${topStripHeight}px; */
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: -1px;
|
||||
width: 1px;
|
||||
background-color: #000000a6;
|
||||
background-color: ${colors.stripNormal};
|
||||
}
|
||||
|
||||
&:hover:after,
|
||||
&.dragging:after {
|
||||
background-color: #000000;
|
||||
background-color: ${colors.stripActive};
|
||||
}
|
||||
`
|
||||
|
||||
const Info = styled.div`
|
||||
const Bulge = styled.div`
|
||||
position: absolute;
|
||||
top: ${topStripHeight + 4}px;
|
||||
font-size: 10px;
|
||||
left: 4px;
|
||||
color: #eee;
|
||||
left: 0px;
|
||||
white-space: nowrap;
|
||||
display: none;
|
||||
padding: 2px 8px 2px 8px;
|
||||
border-radius: 0 2px 2px 0;
|
||||
pointer-events: auto;
|
||||
cursor: ew-resize;
|
||||
color: #555;
|
||||
background-color: ${colors.stripNormal};
|
||||
|
||||
${Strip}:hover &, ${Strip}.dragging & {
|
||||
display: block;
|
||||
color: white;
|
||||
background-color: ${colors.stripActive};
|
||||
}
|
||||
`
|
||||
|
||||
|
@ -64,7 +76,7 @@ const Cover = styled.div`
|
|||
z-index: ${() => zIndexes.lengthIndicatorCover};
|
||||
transform-origin: left top;
|
||||
|
||||
${Strip}:hover ~ &, ${Strip}.dragging ~ & {
|
||||
${Strip}.dragging ~ & {
|
||||
background-color: rgb(23 23 23 / 60%);
|
||||
}
|
||||
`
|
||||
|
@ -74,8 +86,11 @@ type IProps = {
|
|||
}
|
||||
|
||||
const LengthIndicator: React.FC<IProps> = ({layoutP}) => {
|
||||
const [stripRef, stripNode] = useRefAndState<HTMLDivElement | null>(null)
|
||||
const [isDraggingD] = useDragStrip(stripNode, {layoutP})
|
||||
const [nodeRef, node] = useRefAndState<HTMLDivElement | null>(null)
|
||||
const [isDraggingD] = useDragBulge(node, {layoutP})
|
||||
const [popoverNode, openPopover, _, isPopoverOpen] = usePopover(() => {
|
||||
return <div>poppio</div>
|
||||
})
|
||||
|
||||
return usePrism(() => {
|
||||
const sheet = val(layoutP.sheet)
|
||||
|
@ -101,19 +116,25 @@ const LengthIndicator: React.FC<IProps> = ({layoutP}) => {
|
|||
|
||||
return (
|
||||
<>
|
||||
{popoverNode}
|
||||
<Strip
|
||||
title="Change Sequence Length"
|
||||
ref={stripRef}
|
||||
style={{
|
||||
height: height + 'px',
|
||||
transform: `translateX(${translateX === 0 ? -1000 : translateX}px)`,
|
||||
}}
|
||||
className={val(isDraggingD) ? 'dragging' : ''}
|
||||
>
|
||||
<Info>
|
||||
sequence.length:{' '}
|
||||
<Bulge
|
||||
ref={nodeRef}
|
||||
title="Length of the sequence (sequence.length)"
|
||||
onClick={(e) => {
|
||||
openPopover(e, node!)
|
||||
}}
|
||||
{...{[attributeNameThatLocksFramestamp]: 'hide'}}
|
||||
>
|
||||
{sequence.positionFormatter.formatForPlayhead(sequenceLength)}
|
||||
</Info>
|
||||
</Bulge>
|
||||
</Strip>
|
||||
<Cover
|
||||
title="Length"
|
||||
|
@ -132,10 +153,10 @@ const LengthIndicator: React.FC<IProps> = ({layoutP}) => {
|
|||
/>
|
||||
</>
|
||||
)
|
||||
}, [layoutP, stripRef, isDraggingD])
|
||||
}, [layoutP, nodeRef, isDraggingD, popoverNode])
|
||||
}
|
||||
|
||||
function useDragStrip(node: HTMLDivElement | null, props: IProps) {
|
||||
function useDragBulge(node: HTMLDivElement | null, props: IProps) {
|
||||
const propsRef = useRef(props)
|
||||
propsRef.current = props
|
||||
const isDragging = useMemo(() => new Box(false), [])
|
||||
|
|
|
@ -95,6 +95,16 @@ const FrameStampPositionProvider: React.FC<{
|
|||
|
||||
export const useFrameStampPosition = () => useContext(context)
|
||||
|
||||
/**
|
||||
* This attribute is used so that when the cursor hovers over a keyframe,
|
||||
* the framestamp snaps to the position of that keyframe.
|
||||
*
|
||||
* Elements that need this behavior must set a data attribute like so:
|
||||
* <div data-theatre-lock-framestamp-to="120.55" />
|
||||
* Setting this attribute to "hide" hides the stamp.
|
||||
*/
|
||||
export const attributeNameThatLocksFramestamp =
|
||||
'data-theatre-lock-framestamp-to'
|
||||
const pointerPositionInUnitSpace = (
|
||||
layoutP: Pointer<SequenceEditorPanelLayout>,
|
||||
): IDerivation<number> => {
|
||||
|
@ -103,7 +113,25 @@ const pointerPositionInUnitSpace = (
|
|||
const clippedSpaceToUnitSpace = val(layoutP.clippedSpace.toUnitSpace)
|
||||
const leftPadding = val(layoutP.scaledSpace.leftPadding)
|
||||
|
||||
const {clientX, clientY} = val(mousePositionD)
|
||||
const mousePos = val(mousePositionD)
|
||||
if (!mousePos) return -1
|
||||
|
||||
for (const el of mousePos.composedPath()) {
|
||||
if (!(el instanceof HTMLElement || el instanceof SVGElement)) break
|
||||
|
||||
if (el.hasAttribute(attributeNameThatLocksFramestamp)) {
|
||||
const val = el.getAttribute(attributeNameThatLocksFramestamp)
|
||||
if (typeof val !== 'string') continue
|
||||
if (val === 'hide') return -1
|
||||
const double = parseFloat(val)
|
||||
|
||||
if (isFinite(double) && double >= 0) return double
|
||||
}
|
||||
}
|
||||
|
||||
// if (mousePos.composedPath())
|
||||
|
||||
const {clientX, clientY} = mousePos
|
||||
|
||||
const {screenX: x, screenY: y, width: rightWidth, height} = rightDims
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import styled from 'styled-components'
|
|||
import type KeyframeEditor from './KeyframeEditor'
|
||||
import type {Keyframe} from '@theatre/core/projects/store/types/SheetState_Historic'
|
||||
import type {FrameStampPositionLock} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||
import {attributeNameThatLocksFramestamp} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||
import {useFrameStampPosition} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||
|
||||
export const dotSize = 6
|
||||
|
@ -60,6 +61,9 @@ const Dot: React.FC<IProps> = (props) => {
|
|||
cx: `calc(var(--unitSpaceToScaledSpaceMultiplier) * ${cur.position} * 1px)`,
|
||||
cy: `calc((var(--graphEditorVerticalSpace) - var(--graphEditorVerticalSpace) * ${cyInExtremumSpace}) * 1px)`,
|
||||
}}
|
||||
{...{
|
||||
[attributeNameThatLocksFramestamp]: cur.position.toFixed(3),
|
||||
}}
|
||||
/>
|
||||
<Circle
|
||||
style={{
|
||||
|
|
|
@ -6,6 +6,7 @@ import React, {useCallback} from 'react'
|
|||
import styled from 'styled-components'
|
||||
import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
|
||||
import {VscTriangleUp} from 'react-icons/all'
|
||||
import {attributeNameThatLocksFramestamp} from './FrameStampPositionProvider'
|
||||
|
||||
const Container = styled.button`
|
||||
outline: none;
|
||||
|
@ -64,6 +65,7 @@ const GraphEditorToggle: React.FC<{
|
|||
onClick={toggle}
|
||||
title={'Toggle graph editor'}
|
||||
className={isOpen ? 'open' : ''}
|
||||
{...{[attributeNameThatLocksFramestamp]: 'hide'}}
|
||||
>
|
||||
<VscTriangleUp />
|
||||
</Container>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
|
||||
import mousePositionD from '@theatre/studio/utils/mousePositionD'
|
||||
import {usePrism, useVal} from '@theatre/dataverse-react'
|
||||
import type {Pointer} from '@theatre/dataverse'
|
||||
import {val} from '@theatre/dataverse'
|
||||
|
@ -8,7 +7,6 @@ import styled from 'styled-components'
|
|||
import {stampsGridTheme} from '@theatre/studio/panels/SequenceEditorPanel/FrameGrid/StampsGrid'
|
||||
import {zIndexes} from '@theatre/studio/panels/SequenceEditorPanel/SequenceEditorPanel'
|
||||
import {topStripTheme} from './TopStrip'
|
||||
import {inRange} from 'lodash-es'
|
||||
import {useFrameStampPosition} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||
|
||||
const Label = styled.div`
|
||||
|
@ -77,33 +75,3 @@ const FrameStamp: React.FC<{
|
|||
})
|
||||
|
||||
export default FrameStamp
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns -1 if outside, otherwise, a positive number
|
||||
*/
|
||||
const usePointerPositionInUnitSpace = (
|
||||
layoutP: Pointer<SequenceEditorPanelLayout>,
|
||||
): number => {
|
||||
return usePrism(() => {
|
||||
const rightDims = val(layoutP.rightDims)
|
||||
const clippedSpaceToUnitSpace = val(layoutP.clippedSpace.toUnitSpace)
|
||||
const leftPadding = val(layoutP.scaledSpace.leftPadding)
|
||||
|
||||
const {clientX, clientY} = val(mousePositionD)
|
||||
|
||||
const {screenX: x, screenY: y, width: rightWidth, height} = rightDims
|
||||
|
||||
if (
|
||||
inRange(clientX, x, x + rightWidth) &&
|
||||
inRange(clientY, y, y + height)
|
||||
) {
|
||||
const posInRightDims = clientX - x
|
||||
const posInUnitSpace = clippedSpaceToUnitSpace(posInRightDims)
|
||||
|
||||
return posInUnitSpace
|
||||
} else {
|
||||
return -1
|
||||
}
|
||||
}, [layoutP])
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import {position} from 'polished'
|
|||
import React, {useCallback, useMemo, useState} from 'react'
|
||||
import styled from 'styled-components'
|
||||
import {zIndexes} from '@theatre/studio/panels/SequenceEditorPanel/SequenceEditorPanel'
|
||||
import {attributeNameThatLocksFramestamp} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||
|
||||
const Container = styled.div`
|
||||
--threadHeight: 6px;
|
||||
|
@ -237,7 +238,10 @@ const HorizontalScrollbar: React.FC<{
|
|||
}, [layoutP, relevantValuesD])
|
||||
|
||||
return (
|
||||
<Container style={{bottom: bottom + 8 + 'px'}}>
|
||||
<Container
|
||||
style={{bottom: bottom + 8 + 'px'}}
|
||||
{...{[attributeNameThatLocksFramestamp]: 'hide'}}
|
||||
>
|
||||
<TimeThread>
|
||||
<DraggableArea
|
||||
onDragStart={handles.onDragStart}
|
||||
|
|
|
@ -11,6 +11,7 @@ import clamp from 'lodash-es/clamp'
|
|||
import React, {useMemo} from 'react'
|
||||
import styled from 'styled-components'
|
||||
import {zIndexes} from '@theatre/studio/panels/SequenceEditorPanel/SequenceEditorPanel'
|
||||
import {attributeNameThatLocksFramestamp} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||
|
||||
const Container = styled.div<{isVisible: boolean}>`
|
||||
--thumbColor: #00e0ff;
|
||||
|
@ -177,6 +178,7 @@ const Playhead: React.FC<{layoutP: Pointer<SequenceEditorPanelLayout>}> = ({
|
|||
isVisible={isVisible}
|
||||
style={{transform: `translate3d(${posInClippedSpace}px, 0, 0)`}}
|
||||
className={isSeeking ? 'seeking' : ''}
|
||||
{...{[attributeNameThatLocksFramestamp]: 'hide'}}
|
||||
>
|
||||
<Thumb ref={thumbRef as $IntentionalAny}>
|
||||
<RoomToClick room={8} />
|
||||
|
|
|
@ -5,6 +5,7 @@ import styled from 'styled-components'
|
|||
import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
|
||||
import StampsGrid from '@theatre/studio/panels/SequenceEditorPanel/FrameGrid/StampsGrid'
|
||||
import PanelDragZone from '@theatre/studio/panels/BasePanel/PanelDragZone'
|
||||
import {attributeNameThatLocksFramestamp} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||
|
||||
export const topStripHeight = 20
|
||||
|
||||
|
@ -30,7 +31,7 @@ const TopStrip: React.FC<{layoutP: Pointer<SequenceEditorPanelLayout>}> = ({
|
|||
}) => {
|
||||
const width = useVal(layoutP.rightDims.width)
|
||||
return (
|
||||
<Container>
|
||||
<Container {...{[attributeNameThatLocksFramestamp]: 'hide'}}>
|
||||
<StampsGrid layoutP={layoutP} width={width} height={topStripHeight} />
|
||||
</Container>
|
||||
)
|
||||
|
|
105
theatre/studio/src/uiComponents/Popover/Popover.tsx
Normal file
105
theatre/studio/src/uiComponents/Popover/Popover.tsx
Normal file
|
@ -0,0 +1,105 @@
|
|||
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 {createPortal} from 'react-dom'
|
||||
import useWindowSize from 'react-use/esm/useWindowSize'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const minWidth = 190
|
||||
|
||||
/**
|
||||
* How far from the menu should the pointer travel to auto close the menu
|
||||
*/
|
||||
const defaultPointerDistanceThreshold = 200
|
||||
|
||||
const Container = styled.ul`
|
||||
position: absolute;
|
||||
min-width: ${minWidth}px;
|
||||
z-index: 10000;
|
||||
background: ${transparentize(0.2, '#111')};
|
||||
color: white;
|
||||
list-style-type: none;
|
||||
padding: 2px 0;
|
||||
margin: 0;
|
||||
border-radius: 1px;
|
||||
cursor: default;
|
||||
pointer-events: all;
|
||||
border-radius: 3px;
|
||||
`
|
||||
|
||||
const Popover: React.FC<{
|
||||
clickPoint: {clientX: number; clientY: number}
|
||||
target: HTMLElement
|
||||
onRequestClose: () => void
|
||||
children: () => React.ReactNode
|
||||
pointerDistanceThreshold?: number
|
||||
}> = (props) => {
|
||||
const pointerDistanceThreshold =
|
||||
props.pointerDistanceThreshold ?? defaultPointerDistanceThreshold
|
||||
|
||||
const [container, setContainer] = useState<HTMLElement | null>(null)
|
||||
const rect = useBoundingClientRect(container)
|
||||
const windowSize = useWindowSize()
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!rect || !container) return
|
||||
|
||||
const preferredAnchorPoint = {
|
||||
left: rect.width / 2,
|
||||
top: 0,
|
||||
}
|
||||
|
||||
const pos = {
|
||||
left: props.clickPoint.clientX - preferredAnchorPoint.left,
|
||||
top: props.clickPoint.clientY - preferredAnchorPoint.top,
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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 + rect.width + pointerDistanceThreshold ||
|
||||
e.clientY < pos.top - pointerDistanceThreshold ||
|
||||
e.clientY > pos.top + rect.height + pointerDistanceThreshold
|
||||
) {
|
||||
props.onRequestClose()
|
||||
}
|
||||
}
|
||||
|
||||
const onMouseDown = (e: MouseEvent) => {
|
||||
if (!e.composedPath().includes(container)) {
|
||||
props.onRequestClose()
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('mousemove', onMouseMove)
|
||||
window.addEventListener('mousedown', onMouseDown, {capture: true})
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', onMouseMove)
|
||||
window.removeEventListener('mousedown', onMouseDown, {capture: true})
|
||||
}
|
||||
}, [rect, container, props.clickPoint, windowSize, props.onRequestClose])
|
||||
|
||||
return createPortal(
|
||||
<Container ref={setContainer}>{props.children()}</Container>,
|
||||
getStudio()!.ui.containerShadow,
|
||||
)
|
||||
}
|
||||
|
||||
export default Popover
|
48
theatre/studio/src/uiComponents/Popover/usePopover.tsx
Normal file
48
theatre/studio/src/uiComponents/Popover/usePopover.tsx
Normal file
|
@ -0,0 +1,48 @@
|
|||
import React, {useCallback, useState} from 'react'
|
||||
import Popover from './Popover'
|
||||
|
||||
type OpenFn = (e: React.MouseEvent, target: HTMLElement) => void
|
||||
type CloseFn = () => void
|
||||
type State =
|
||||
| {isOpen: false}
|
||||
| {
|
||||
isOpen: true
|
||||
clickPoint: {
|
||||
clientX: number
|
||||
clientY: number
|
||||
}
|
||||
target: HTMLElement
|
||||
}
|
||||
|
||||
export default function usePopover(
|
||||
render: () => React.ReactNode,
|
||||
): [node: React.ReactNode, open: OpenFn, close: CloseFn, isOpen: boolean] {
|
||||
const [state, setState] = useState<State>({
|
||||
isOpen: false,
|
||||
})
|
||||
|
||||
const open = useCallback<OpenFn>((e, target) => {
|
||||
setState({
|
||||
isOpen: true,
|
||||
clickPoint: {clientX: e.clientX, clientY: e.clientY},
|
||||
target,
|
||||
})
|
||||
}, [])
|
||||
|
||||
const close = useCallback<CloseFn>(() => {
|
||||
setState({isOpen: false})
|
||||
}, [])
|
||||
|
||||
const node = state.isOpen ? (
|
||||
<Popover
|
||||
children={render}
|
||||
clickPoint={state.clickPoint}
|
||||
target={state.target}
|
||||
onRequestClose={close}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
|
||||
return [node, open, close, state.isOpen]
|
||||
}
|
|
@ -5,7 +5,10 @@ import React from 'react'
|
|||
import {createPortal} from 'react-dom'
|
||||
|
||||
const ShowMousePosition: React.FC<{}> = (props) => {
|
||||
const pos = usePrism(() => val(mousePositionD), [])
|
||||
const pos = usePrism(
|
||||
() => val(mousePositionD) ?? {clientX: 0, clientY: 0},
|
||||
[],
|
||||
)
|
||||
return createPortal(
|
||||
<>
|
||||
<div
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import {prism} from '@theatre/dataverse'
|
||||
|
||||
const mousePositionD = prism(() => {
|
||||
const [pos, setPos] = prism.state('pos', {clientX: 0, clientY: 0})
|
||||
const [pos, setPos] = prism.state<MouseEvent | null>('pos', null)
|
||||
prism.effect(
|
||||
'setupListeners',
|
||||
() => {
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
setPos({clientX: e.clientX, clientY: e.clientY})
|
||||
setPos(e)
|
||||
}
|
||||
document.addEventListener('mousemove', handleMouseMove)
|
||||
|
||||
|
|
Loading…
Reference in a new issue