refactor/docs: Minor identifier tweaks for RightClick/ContextMenu
This commit is contained in:
parent
f04bc3e31a
commit
91794f550d
15 changed files with 99 additions and 62 deletions
|
@ -119,7 +119,7 @@ export const SingleRowPropEditor: React.FC<{
|
||||||
useRefAndState<HTMLDivElement | null>(null)
|
useRefAndState<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
const [contextMenu] = useContextMenu(propNameContainer, {
|
const [contextMenu] = useContextMenu(propNameContainer, {
|
||||||
items: stuff.contextMenuItems,
|
menuItems: stuff.contextMenuItems,
|
||||||
})
|
})
|
||||||
|
|
||||||
const color = shadeToColor[stuff.shade]
|
const color = shadeToColor[stuff.shade]
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {val} from '@theatre/dataverse'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import KeyframeEditor from './KeyframeEditor/KeyframeEditor'
|
import KeyframeEditor from './KeyframeEditor/KeyframeEditor'
|
||||||
|
import type {IContextMenuItem} from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu'
|
||||||
import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu'
|
import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu'
|
||||||
import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
||||||
import getStudio from '@theatre/studio/getStudio'
|
import getStudio from '@theatre/studio/getStudio'
|
||||||
|
@ -85,7 +86,7 @@ function useBasicKeyframedTrackContextMenu(
|
||||||
props: BasicKeyframedTracksProps,
|
props: BasicKeyframedTracksProps,
|
||||||
) {
|
) {
|
||||||
return useContextMenu(node, {
|
return useContextMenu(node, {
|
||||||
items: () => {
|
menuItems: () => {
|
||||||
const selectionKeyframes =
|
const selectionKeyframes =
|
||||||
val(getStudio()!.atomP.ahistoric.clipboard.keyframes) || []
|
val(getStudio()!.atomP.ahistoric.clipboard.keyframes) || []
|
||||||
|
|
||||||
|
@ -101,7 +102,7 @@ function useBasicKeyframedTrackContextMenu(
|
||||||
function pasteKeyframesContextMenuItem(
|
function pasteKeyframesContextMenuItem(
|
||||||
props: BasicKeyframedTracksProps,
|
props: BasicKeyframedTracksProps,
|
||||||
keyframes: Keyframe[],
|
keyframes: Keyframe[],
|
||||||
) {
|
): IContextMenuItem {
|
||||||
return {
|
return {
|
||||||
label: 'Paste Keyframes',
|
label: 'Paste Keyframes',
|
||||||
callback: () => {
|
callback: () => {
|
||||||
|
|
|
@ -12,7 +12,7 @@ import type {
|
||||||
SequenceEditorPanelLayout,
|
SequenceEditorPanelLayout,
|
||||||
DopeSheetSelection,
|
DopeSheetSelection,
|
||||||
} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
|
} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
|
||||||
import {DOT_SIZE_PX} from './Dot'
|
import {DOT_SIZE_PX} from './KeyframeDot'
|
||||||
import type KeyframeEditor from './KeyframeEditor'
|
import type KeyframeEditor from './KeyframeEditor'
|
||||||
import type Sequence from '@theatre/core/sequences/Sequence'
|
import type Sequence from '@theatre/core/sequences/Sequence'
|
||||||
import usePopover from '@theatre/studio/uiComponents/Popover/usePopover'
|
import usePopover from '@theatre/studio/uiComponents/Popover/usePopover'
|
||||||
|
@ -250,7 +250,7 @@ function useConnectorContextMenu(
|
||||||
) {
|
) {
|
||||||
const maybeKeyframeIds = selectedKeyframeIdsIfInSingleTrack(props.selection)
|
const maybeKeyframeIds = selectedKeyframeIdsIfInSingleTrack(props.selection)
|
||||||
return useContextMenu(node, {
|
return useContextMenu(node, {
|
||||||
items: () => {
|
menuItems: () => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: maybeKeyframeIds ? 'Copy Selection' : 'Copy both Keyframes',
|
label: maybeKeyframeIds ? 'Copy Selection' : 'Copy both Keyframes',
|
||||||
|
|
|
@ -5,13 +5,14 @@ import type {
|
||||||
import getStudio from '@theatre/studio/getStudio'
|
import getStudio from '@theatre/studio/getStudio'
|
||||||
import type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore'
|
import type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore'
|
||||||
import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu'
|
import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu'
|
||||||
|
import type {IContextMenuItem} from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu'
|
||||||
import useDrag from '@theatre/studio/uiComponents/useDrag'
|
import useDrag from '@theatre/studio/uiComponents/useDrag'
|
||||||
|
import type {UseDragOpts} from '@theatre/studio/uiComponents/useDrag'
|
||||||
import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
||||||
import {val} from '@theatre/dataverse'
|
import {val} from '@theatre/dataverse'
|
||||||
import {lighten} from 'polished'
|
import {lighten} from 'polished'
|
||||||
import React, {useMemo, useRef, useState} from 'react'
|
import React, {useMemo, useRef, useState} from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import type KeyframeEditor from './KeyframeEditor'
|
|
||||||
import {useLockFrameStampPosition} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
import {useLockFrameStampPosition} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||||
import {attributeNameThatLocksFramestamp} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
import {attributeNameThatLocksFramestamp} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||||
import {
|
import {
|
||||||
|
@ -20,6 +21,7 @@ import {
|
||||||
} from '@theatre/studio/uiComponents/PointerEventsHandler'
|
} from '@theatre/studio/uiComponents/PointerEventsHandler'
|
||||||
import SnapCursor from './SnapCursor.svg'
|
import SnapCursor from './SnapCursor.svg'
|
||||||
import selectedKeyframeIdsIfInSingleTrack from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/selectedKeyframeIdsIfInSingleTrack'
|
import selectedKeyframeIdsIfInSingleTrack from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/selectedKeyframeIdsIfInSingleTrack'
|
||||||
|
import type {IKeyframeEditorProps} from './KeyframeEditor'
|
||||||
|
|
||||||
export const DOT_SIZE_PX = 6
|
export const DOT_SIZE_PX = 6
|
||||||
const HIT_ZONE_SIZE_PX = 12
|
const HIT_ZONE_SIZE_PX = 12
|
||||||
|
@ -42,6 +44,8 @@ const dotTheme = {
|
||||||
|
|
||||||
const Square = styled.div<{isSelected: boolean}>`
|
const Square = styled.div<{isSelected: boolean}>`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
${dims(DOT_SIZE_PX)}
|
||||||
|
|
||||||
background: ${(props) =>
|
background: ${(props) =>
|
||||||
props.isSelected ? dotTheme.selectedColor : dotTheme.normalColor};
|
props.isSelected ? dotTheme.selectedColor : dotTheme.normalColor};
|
||||||
transform: rotateZ(45deg);
|
transform: rotateZ(45deg);
|
||||||
|
@ -85,13 +89,14 @@ const HitZone = styled.div`
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
type IProps = Parameters<typeof KeyframeEditor>[0]
|
type IKeyframeDotProps = IKeyframeEditorProps
|
||||||
|
|
||||||
const Dot: React.FC<IProps> = (props) => {
|
/** The ◆ you can grab onto in "keyframe editor" (aka "dope sheet" in other programs) */
|
||||||
|
const KeyframeDot: React.FC<IKeyframeDotProps> = (props) => {
|
||||||
const [ref, node] = useRefAndState<HTMLDivElement | null>(null)
|
const [ref, node] = useRefAndState<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
const [contextMenu] = useKeyframeContextMenu(node, props)
|
|
||||||
const [isDragging] = useDragKeyframe(node, props)
|
const [isDragging] = useDragKeyframe(node, props)
|
||||||
|
const [contextMenu] = useKeyframeContextMenu(node, props)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -110,9 +115,12 @@ const Dot: React.FC<IProps> = (props) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Dot
|
export default KeyframeDot
|
||||||
|
|
||||||
function useKeyframeContextMenu(node: HTMLDivElement | null, props: IProps) {
|
function useKeyframeContextMenu(
|
||||||
|
target: HTMLDivElement | null,
|
||||||
|
props: IKeyframeDotProps,
|
||||||
|
) {
|
||||||
const maybeSelectedKeyframeIds = selectedKeyframeIdsIfInSingleTrack(
|
const maybeSelectedKeyframeIds = selectedKeyframeIdsIfInSingleTrack(
|
||||||
props.selection,
|
props.selection,
|
||||||
)
|
)
|
||||||
|
@ -123,8 +131,8 @@ function useKeyframeContextMenu(node: HTMLDivElement | null, props: IProps) {
|
||||||
|
|
||||||
const deleteItem = deleteSelectionOrKeyframeContextMenuItem(props)
|
const deleteItem = deleteSelectionOrKeyframeContextMenuItem(props)
|
||||||
|
|
||||||
return useContextMenu(node, {
|
return useContextMenu(target, {
|
||||||
items: () => {
|
menuItems: () => {
|
||||||
return [keyframeSelectionItem, deleteItem]
|
return [keyframeSelectionItem, deleteItem]
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -132,7 +140,7 @@ function useKeyframeContextMenu(node: HTMLDivElement | null, props: IProps) {
|
||||||
|
|
||||||
function useDragKeyframe(
|
function useDragKeyframe(
|
||||||
node: HTMLDivElement | null,
|
node: HTMLDivElement | null,
|
||||||
props: IProps,
|
props: IKeyframeDotProps,
|
||||||
): [isDragging: boolean] {
|
): [isDragging: boolean] {
|
||||||
const [isDragging, setIsDragging] = useState(false)
|
const [isDragging, setIsDragging] = useState(false)
|
||||||
useLockFrameStampPosition(isDragging, props.keyframe.position)
|
useLockFrameStampPosition(isDragging, props.keyframe.position)
|
||||||
|
@ -140,10 +148,10 @@ function useDragKeyframe(
|
||||||
const propsRef = useRef(props)
|
const propsRef = useRef(props)
|
||||||
propsRef.current = props
|
propsRef.current = props
|
||||||
|
|
||||||
const gestureHandlers = useMemo<Parameters<typeof useDrag>[1]>(() => {
|
const useDragOpts = useMemo<UseDragOpts>(() => {
|
||||||
let toUnitSpace: SequenceEditorPanelLayout['scaledSpace']['toUnitSpace']
|
let toUnitSpace: SequenceEditorPanelLayout['scaledSpace']['toUnitSpace']
|
||||||
let tempTransaction: CommitOrDiscard | undefined
|
let tempTransaction: CommitOrDiscard | undefined
|
||||||
let propsAtStartOfDrag: IProps
|
let propsAtStartOfDrag: IKeyframeDotProps
|
||||||
|
|
||||||
let selectionDragHandlers:
|
let selectionDragHandlers:
|
||||||
| ReturnType<DopeSheetSelection['getDragHandlers']>
|
| ReturnType<DopeSheetSelection['getDragHandlers']>
|
||||||
|
@ -151,6 +159,7 @@ function useDragKeyframe(
|
||||||
|
|
||||||
return {
|
return {
|
||||||
debugName: 'Dot/useDragKeyframe',
|
debugName: 'Dot/useDragKeyframe',
|
||||||
|
|
||||||
onDragStart(event) {
|
onDragStart(event) {
|
||||||
setIsDragging(true)
|
setIsDragging(true)
|
||||||
const props = propsRef.current
|
const props = propsRef.current
|
||||||
|
@ -241,14 +250,16 @@ function useDragKeyframe(
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useDrag(node, gestureHandlers)
|
useDrag(node, useDragOpts)
|
||||||
|
|
||||||
useCssCursorLock(isDragging, 'draggingPositionInSequenceEditor', 'ew-resize')
|
useCssCursorLock(isDragging, 'draggingPositionInSequenceEditor', 'ew-resize')
|
||||||
|
|
||||||
return [isDragging]
|
return [isDragging]
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteSelectionOrKeyframeContextMenuItem(props: IProps) {
|
function deleteSelectionOrKeyframeContextMenuItem(
|
||||||
|
props: IKeyframeDotProps,
|
||||||
|
): IContextMenuItem {
|
||||||
return {
|
return {
|
||||||
label: props.selection ? 'Delete Selection' : 'Delete Keyframe',
|
label: props.selection ? 'Delete Selection' : 'Delete Keyframe',
|
||||||
callback: () => {
|
callback: () => {
|
||||||
|
@ -269,7 +280,10 @@ function deleteSelectionOrKeyframeContextMenuItem(props: IProps) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyKeyFrameContextMenuItem(props: IProps, keyframeIds: string[]) {
|
function copyKeyFrameContextMenuItem(
|
||||||
|
props: IKeyframeDotProps,
|
||||||
|
keyframeIds: string[],
|
||||||
|
): IContextMenuItem {
|
||||||
return {
|
return {
|
||||||
label: keyframeIds.length > 1 ? 'Copy selection' : 'Copy keyframe',
|
label: keyframeIds.length > 1 ? 'Copy selection' : 'Copy keyframe',
|
||||||
callback: () => {
|
callback: () => {
|
|
@ -12,7 +12,7 @@ import {val} from '@theatre/dataverse'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import Connector from './Connector'
|
import Connector from './Connector'
|
||||||
import Dot from './Dot'
|
import KeyframeDot from './KeyframeDot'
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -20,14 +20,16 @@ const Container = styled.div`
|
||||||
|
|
||||||
const noConnector = <></>
|
const noConnector = <></>
|
||||||
|
|
||||||
const KeyframeEditor: React.FC<{
|
export type IKeyframeEditorProps = {
|
||||||
index: number
|
index: number
|
||||||
keyframe: Keyframe
|
keyframe: Keyframe
|
||||||
trackData: TrackData
|
trackData: TrackData
|
||||||
layoutP: Pointer<SequenceEditorPanelLayout>
|
layoutP: Pointer<SequenceEditorPanelLayout>
|
||||||
leaf: SequenceEditorTree_PrimitiveProp
|
leaf: SequenceEditorTree_PrimitiveProp
|
||||||
selection: undefined | DopeSheetSelection
|
selection: undefined | DopeSheetSelection
|
||||||
}> = (props) => {
|
}
|
||||||
|
|
||||||
|
const KeyframeEditor: React.FC<IKeyframeEditorProps> = (props) => {
|
||||||
const {index, trackData} = props
|
const {index, trackData} = props
|
||||||
const cur = trackData.keyframes[index]
|
const cur = trackData.keyframes[index]
|
||||||
const next = trackData.keyframes[index + 1]
|
const next = trackData.keyframes[index + 1]
|
||||||
|
@ -45,7 +47,7 @@ const KeyframeEditor: React.FC<{
|
||||||
}px))`,
|
}px))`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Dot {...props} />
|
<KeyframeDot {...props} />
|
||||||
{connected ? <Connector {...props} /> : noConnector}
|
{connected ? <Connector {...props} /> : noConnector}
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
|
|
|
@ -109,7 +109,7 @@ function useConnectorContextMenu(node: SVGElement | null, props: IProps) {
|
||||||
const next = trackData.keyframes[index + 1]
|
const next = trackData.keyframes[index + 1]
|
||||||
|
|
||||||
return useContextMenu(node, {
|
return useContextMenu(node, {
|
||||||
items: () => {
|
menuItems: () => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: 'Delete',
|
label: 'Delete',
|
||||||
|
|
|
@ -243,7 +243,7 @@ function useOurDrags(node: SVGCircleElement | null, props: IProps): void {
|
||||||
|
|
||||||
function useOurContextMenu(node: SVGCircleElement | null, props: IProps) {
|
function useOurContextMenu(node: SVGCircleElement | null, props: IProps) {
|
||||||
return useContextMenu(node, {
|
return useContextMenu(node, {
|
||||||
items: () => {
|
menuItems: () => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: 'Delete',
|
label: 'Delete',
|
||||||
|
|
|
@ -181,7 +181,7 @@ function useDragKeyframe(
|
||||||
|
|
||||||
function useKeyframeContextMenu(node: SVGCircleElement | null, props: IProps) {
|
function useKeyframeContextMenu(node: SVGCircleElement | null, props: IProps) {
|
||||||
return useContextMenu(node, {
|
return useContextMenu(node, {
|
||||||
items: () => {
|
menuItems: () => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: 'Delete',
|
label: 'Delete',
|
||||||
|
|
|
@ -235,7 +235,7 @@ function useDragKeyframe(
|
||||||
|
|
||||||
function useKeyframeContextMenu(node: SVGCircleElement | null, props: IProps) {
|
function useKeyframeContextMenu(node: SVGCircleElement | null, props: IProps) {
|
||||||
return useContextMenu(node, {
|
return useContextMenu(node, {
|
||||||
items: () => {
|
menuItems: () => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: 'Delete',
|
label: 'Delete',
|
||||||
|
|
|
@ -132,9 +132,8 @@ const FocusRangeStrip: React.FC<{
|
||||||
)
|
)
|
||||||
|
|
||||||
const [contextMenu] = useContextMenu(rangeStripNode, {
|
const [contextMenu] = useContextMenu(rangeStripNode, {
|
||||||
items: () => {
|
menuItems: () => {
|
||||||
const sheet = val(layoutP.sheet)
|
const sheet = val(layoutP.sheet)
|
||||||
|
|
||||||
const existingRange = existingRangeD.getValue()
|
const existingRange = existingRangeD.getValue()
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|
|
@ -21,8 +21,8 @@ const Container = styled.div`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
left: 12px;
|
left: 12px;
|
||||||
/* bottom: 8px; */
|
/* bottom: 8px; */
|
||||||
${pointerEventsAutoInNormalMode};
|
|
||||||
z-index: ${() => zIndexes.horizontalScrollbar};
|
z-index: ${() => zIndexes.horizontalScrollbar};
|
||||||
|
${pointerEventsAutoInNormalMode}
|
||||||
`
|
`
|
||||||
|
|
||||||
const TimeThread = styled.div`
|
const TimeThread = styled.div`
|
||||||
|
|
|
@ -18,7 +18,7 @@ const minWidth = 190
|
||||||
*/
|
*/
|
||||||
const pointerDistanceThreshold = 20
|
const pointerDistanceThreshold = 20
|
||||||
|
|
||||||
const Container = styled.ul`
|
const MenuContainer = styled.ul`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
min-width: ${minWidth}px;
|
min-width: ${minWidth}px;
|
||||||
z-index: 10000;
|
z-index: 10000;
|
||||||
|
@ -34,6 +34,10 @@ const Container = styled.ul`
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
`
|
`
|
||||||
|
|
||||||
|
export type IContextMenuItemCustomNodeRenderFn = (controls: {
|
||||||
|
closeMenu(): void
|
||||||
|
}) => React.ReactChild
|
||||||
|
|
||||||
export type IContextMenuItem = {
|
export type IContextMenuItem = {
|
||||||
label: string | ElementType
|
label: string | ElementType
|
||||||
callback?: (e: React.MouseEvent) => void
|
callback?: (e: React.MouseEvent) => void
|
||||||
|
@ -41,9 +45,13 @@ export type IContextMenuItem = {
|
||||||
// subs?: Item[]
|
// subs?: Item[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const RightClickMenu: React.FC<{
|
export type IContextMenuItemsValue =
|
||||||
items: IContextMenuItem[] | (() => IContextMenuItem[])
|
| IContextMenuItem[]
|
||||||
rightClickPoint: {clientX: number; clientY: number}
|
| (() => IContextMenuItem[])
|
||||||
|
|
||||||
|
const ContextMenu: React.FC<{
|
||||||
|
items: IContextMenuItemsValue
|
||||||
|
clickPoint: {clientX: number; clientY: number}
|
||||||
onRequestClose: () => void
|
onRequestClose: () => void
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
const [container, setContainer] = useState<HTMLElement | null>(null)
|
const [container, setContainer] = useState<HTMLElement | null>(null)
|
||||||
|
@ -59,8 +67,8 @@ const RightClickMenu: React.FC<{
|
||||||
}
|
}
|
||||||
|
|
||||||
const pos = {
|
const pos = {
|
||||||
left: props.rightClickPoint.clientX - preferredAnchorPoint.left,
|
left: props.clickPoint.clientX - preferredAnchorPoint.left,
|
||||||
top: props.rightClickPoint.clientY - preferredAnchorPoint.top,
|
top: props.clickPoint.clientY - preferredAnchorPoint.top,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pos.left < 0) {
|
if (pos.left < 0) {
|
||||||
|
@ -94,7 +102,7 @@ const RightClickMenu: React.FC<{
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('mousemove', onMouseMove)
|
window.removeEventListener('mousemove', onMouseMove)
|
||||||
}
|
}
|
||||||
}, [rect, container, props.rightClickPoint, windowSize, props.onRequestClose])
|
}, [rect, container, props.clickPoint, windowSize, props.onRequestClose])
|
||||||
const portalLayer = useContext(PortalContext)
|
const portalLayer = useContext(PortalContext)
|
||||||
|
|
||||||
useOnKeyDown((ev) => {
|
useOnKeyDown((ev) => {
|
||||||
|
@ -104,7 +112,7 @@ const RightClickMenu: React.FC<{
|
||||||
const items = Array.isArray(props.items) ? props.items : props.items()
|
const items = Array.isArray(props.items) ? props.items : props.items()
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<Container ref={setContainer}>
|
<MenuContainer ref={setContainer}>
|
||||||
{items.map((item, i) => (
|
{items.map((item, i) => (
|
||||||
<Item
|
<Item
|
||||||
key={`item-${i}`}
|
key={`item-${i}`}
|
||||||
|
@ -118,9 +126,9 @@ const RightClickMenu: React.FC<{
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Container>,
|
</MenuContainer>,
|
||||||
portalLayer!,
|
portalLayer!,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default RightClickMenu
|
export default ContextMenu
|
|
@ -5,7 +5,7 @@ import styled from 'styled-components'
|
||||||
|
|
||||||
export const height = 26
|
export const height = 26
|
||||||
|
|
||||||
const Container = styled.li<{enabled: boolean}>`
|
const ItemContainer = styled.li<{enabled: boolean}>`
|
||||||
height: ${height}px;
|
height: ${height}px;
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -32,7 +32,7 @@ const Container = styled.li<{enabled: boolean}>`
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const Label = styled.span``
|
const ItemLabel = styled.span``
|
||||||
|
|
||||||
const Item: React.FC<{
|
const Item: React.FC<{
|
||||||
label: string | ElementType
|
label: string | ElementType
|
||||||
|
@ -40,12 +40,12 @@ const Item: React.FC<{
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
return (
|
return (
|
||||||
<Container
|
<ItemContainer
|
||||||
onClick={props.enabled ? props.onClick : noop}
|
onClick={props.enabled ? props.onClick : noop}
|
||||||
enabled={props.enabled}
|
enabled={props.enabled}
|
||||||
>
|
>
|
||||||
<Label>{props.label}</Label>
|
<ItemLabel>{props.label}</ItemLabel>
|
||||||
</Container>
|
</ItemContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,28 +1,36 @@
|
||||||
import type {VoidFn} from '@theatre/shared/utils/types'
|
import type {VoidFn} from '@theatre/shared/utils/types'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import RightClickMenu from './RightClickMenu/RightClickMenu'
|
import ContextMenu from './ContextMenu/ContextMenu'
|
||||||
|
import type {
|
||||||
|
IContextMenuItemsValue,
|
||||||
|
IContextMenuItem,
|
||||||
|
} from './ContextMenu/ContextMenu'
|
||||||
import useRequestContextMenu from './useRequestContextMenu'
|
import useRequestContextMenu from './useRequestContextMenu'
|
||||||
export type {IContextMenuItem} from './RightClickMenu/RightClickMenu'
|
import type {IRequestContextMenuOptions} from './useRequestContextMenu'
|
||||||
|
|
||||||
|
// re-exports
|
||||||
|
export type {
|
||||||
|
IContextMenuItemsValue,
|
||||||
|
IContextMenuItem,
|
||||||
|
IRequestContextMenuOptions,
|
||||||
|
}
|
||||||
|
|
||||||
const emptyNode = <></>
|
const emptyNode = <></>
|
||||||
|
|
||||||
type IProps = Omit<
|
|
||||||
Parameters<typeof RightClickMenu>[0],
|
|
||||||
'rightClickPoint' | 'onRequestClose'
|
|
||||||
>
|
|
||||||
|
|
||||||
export default function useContextMenu(
|
export default function useContextMenu(
|
||||||
target: HTMLElement | SVGElement | null,
|
target: HTMLElement | SVGElement | null,
|
||||||
props: IProps,
|
opts: IRequestContextMenuOptions & {
|
||||||
|
menuItems: IContextMenuItemsValue
|
||||||
|
},
|
||||||
): [node: React.ReactNode, close: VoidFn, isOpen: boolean] {
|
): [node: React.ReactNode, close: VoidFn, isOpen: boolean] {
|
||||||
const [status, close] = useRequestContextMenu(target)
|
const [status, close] = useRequestContextMenu(target, opts)
|
||||||
|
|
||||||
const node = !status.isOpen ? (
|
const node = !status.isOpen ? (
|
||||||
emptyNode
|
emptyNode
|
||||||
) : (
|
) : (
|
||||||
<RightClickMenu
|
<ContextMenu
|
||||||
items={props.items}
|
items={opts.menuItems}
|
||||||
rightClickPoint={status.event}
|
clickPoint={status.event}
|
||||||
onRequestClose={close}
|
onRequestClose={close}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,28 +5,33 @@ type IState = {isOpen: true; event: MouseEvent} | {isOpen: false}
|
||||||
|
|
||||||
type CloseMenuFn = () => void
|
type CloseMenuFn = () => void
|
||||||
|
|
||||||
|
export type IRequestContextMenuOptions = {
|
||||||
|
disabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
const useRequestContextMenu = (
|
const useRequestContextMenu = (
|
||||||
target: HTMLElement | SVGElement | null,
|
target: HTMLElement | SVGElement | null,
|
||||||
|
opts: IRequestContextMenuOptions,
|
||||||
): [state: IState, close: CloseMenuFn] => {
|
): [state: IState, close: CloseMenuFn] => {
|
||||||
const [state, setState] = useState<IState>({isOpen: false})
|
const [state, setState] = useState<IState>({isOpen: false})
|
||||||
const close = useCallback<CloseMenuFn>(() => setState({isOpen: false}), [])
|
const close = useCallback<CloseMenuFn>(() => setState({isOpen: false}), [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!target) {
|
if (!target || opts.disabled === true) {
|
||||||
setState({isOpen: false})
|
setState({isOpen: false})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const onContextMenu = (event: MouseEvent) => {
|
const onTrigger = (event: MouseEvent) => {
|
||||||
setState({isOpen: true, event})
|
setState({isOpen: true, event})
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
}
|
}
|
||||||
target.addEventListener('contextmenu', onContextMenu as $FixMe)
|
target.addEventListener('contextmenu', onTrigger as $FixMe)
|
||||||
return () => {
|
return () => {
|
||||||
target.removeEventListener('contextmenu', onContextMenu as $FixMe)
|
target.removeEventListener('contextmenu', onTrigger as $FixMe)
|
||||||
}
|
}
|
||||||
}, [target])
|
}, [target, opts.disabled])
|
||||||
|
|
||||||
return [state, close]
|
return [state, close]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue