Fix popover behavior when popover is open and the trigger is clicked (#211)

* Fix popover behavior when open and clicking on trigger

* Remove console log

* Resolve merge conflicts

* Remove destructuring in favor of property access

* Extract usePopover return type into an interface

* Fix merge
This commit is contained in:
Andrew Prifer 2022-09-14 20:05:09 +07:00 committed by GitHub
parent 8680f9d89e
commit 743254a6c6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 169 additions and 128 deletions

View file

@ -1,8 +1,8 @@
import React from 'react' import React from 'react'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import App from './App' import App from './App'
import type {ISheetObject} from '@theatre/core'; import type {ISheetObject} from '@theatre/core'
import { onChange, types, val} from '@theatre/core' import {onChange, types, val} from '@theatre/core'
import studio from '@theatre/studio' import studio from '@theatre/studio'
import extension from '@theatre/r3f/dist/extension' import extension from '@theatre/r3f/dist/extension'

View file

@ -62,7 +62,7 @@ const ProjectDetails: React.FC<{
}, 40000) }, 40000)
}, []) }, [])
const [tooltip, openExportTooltip] = usePopover( const exportTooltip = usePopover(
{debugName: 'ProjectDetails', pointerDistanceThreshold: 50}, {debugName: 'ProjectDetails', pointerDistanceThreshold: 50},
() => ( () => (
<ExportTooltip> <ExportTooltip>
@ -81,13 +81,13 @@ const ProjectDetails: React.FC<{
return ( return (
<> <>
{tooltip} {exportTooltip.node}
<Container> <Container>
<StateConflictRow projectId={projectId} /> <StateConflictRow projectId={projectId} />
<TheExportRow> <TheExportRow>
<DetailPanelButton <DetailPanelButton
onMouseEnter={(e) => onMouseEnter={(e) =>
openExportTooltip(e, e.target as unknown as HTMLButtonElement) exportTooltip.open(e, e.target as unknown as HTMLButtonElement)
} }
onClick={!downloaded ? exportProject : undefined} onClick={!downloaded ? exportProject : undefined}
disabled={downloaded} disabled={downloaded}

View file

@ -55,7 +55,11 @@ export const AggregateKeyframeConnector: React.VFC<IAggregateKeyframeConnectorPr
const [contextMenu] = useConnectorContextMenu(props, node) const [contextMenu] = useConnectorContextMenu(props, node)
const [isDragging] = useDragKeyframe(node, props.editorProps) const [isDragging] = useDragKeyframe(node, props.editorProps)
const [popoverNode, openPopover, closePopover] = usePopover( const {
node: popoverNode,
toggle: togglePopover,
close: closePopover,
} = usePopover(
() => { () => {
const rightDims = val(editorProps.layoutP.rightDims) const rightDims = val(editorProps.layoutP.rightDims)
@ -89,7 +93,7 @@ export const AggregateKeyframeConnector: React.VFC<IAggregateKeyframeConnectorPr
isSelected={connected ? connected.selected : false} isSelected={connected ? connected.selected : false}
isPopoverOpen={isAggregateEditingInCurvePopover} isPopoverOpen={isAggregateEditingInCurvePopover}
openPopover={(e) => { openPopover={(e) => {
if (node) openPopover(e, node) if (node) togglePopover(e, node)
}} }}
/> />
{popoverNode} {popoverNode}

View file

@ -99,14 +99,13 @@ export function AggregateKeyframeDot(
const logger = useLogger('AggregateKeyframeDot') const logger = useLogger('AggregateKeyframeDot')
const {cur} = props.utils const {cur} = props.utils
const [inlineEditorPopover, openEditor, _, isInlineEditorPopoverOpen] = const inlineEditorPopover = useKeyframeInlineEditorPopover(
useKeyframeInlineEditorPopover( props.editorProps.viewModel.type === 'sheetObject'
props.editorProps.viewModel.type === 'sheetObject' ? sheetObjectBuild(props.editorProps.viewModel, cur.keyframes)
? sheetObjectBuild(props.editorProps.viewModel, cur.keyframes) ?.children ?? null
?.children ?? null : propWithChildrenBuild(props.editorProps.viewModel, cur.keyframes)
: propWithChildrenBuild(props.editorProps.viewModel, cur.keyframes) ?.children ?? null,
?.children ?? null, )
)
const presence = usePresence(props.utils.itemKey) const presence = usePresence(props.utils.itemKey)
presence.useRelations( presence.useRelations(
@ -136,7 +135,7 @@ export function AggregateKeyframeDot(
// Need this for the dragging logic to be able to get the keyframe props // Need this for the dragging logic to be able to get the keyframe props
// based on the position. // based on the position.
{...DopeSnap.includePositionSnapAttrs(cur.position)} {...DopeSnap.includePositionSnapAttrs(cur.position)}
onClick={(e) => openEditor(e, ref.current!)} onClick={(e) => inlineEditorPopover.open(e, ref.current!)}
/> />
<AggregateKeyframeVisualDot <AggregateKeyframeVisualDot
flag={presence.flag} flag={presence.flag}
@ -144,7 +143,7 @@ export function AggregateKeyframeDot(
isSelected={cur.selected} isSelected={cur.selected}
/> />
{contextMenu} {contextMenu}
{inlineEditorPopover} {inlineEditorPopover.node}
</> </>
) )
} }

View file

@ -41,7 +41,11 @@ const BasicKeyframeConnector: React.VFC<IBasicKeyframeConnectorProps> = (
const [nodeRef, node] = useRefAndState<HTMLDivElement | null>(null) const [nodeRef, node] = useRefAndState<HTMLDivElement | null>(null)
const [popoverNode, openPopover, closePopover, isPopoverOpen] = usePopover( const {
node: popoverNode,
toggle: togglePopover,
close: closePopover,
} = usePopover(
() => { () => {
const rightDims = val(props.layoutP.rightDims) const rightDims = val(props.layoutP.rightDims)
return { return {
@ -83,7 +87,7 @@ const BasicKeyframeConnector: React.VFC<IBasicKeyframeConnectorProps> = (
connectorLengthInUnitSpace={connectorLengthInUnitSpace} connectorLengthInUnitSpace={connectorLengthInUnitSpace}
{...themeValues} {...themeValues}
openPopover={(e) => { openPopover={(e) => {
if (node) openPopover(e, node) if (node) togglePopover(e, node)
}} }}
> >
{popoverNode} {popoverNode}

View file

@ -101,20 +101,23 @@ const SingleKeyframeDot: React.VFC<ISingleKeyframeDotProps> = (props) => {
const [ref, node] = useRefAndState<HTMLDivElement | null>(null) const [ref, node] = useRefAndState<HTMLDivElement | null>(null)
const [contextMenu] = useSingleKeyframeContextMenu(node, logger, props) const [contextMenu] = useSingleKeyframeContextMenu(node, logger, props)
const [inlineEditorPopover, openEditor, _, isInlineEditorPopoverOpen] = const {
useKeyframeInlineEditorPopover([ node: inlineEditorPopover,
{ toggle: toggleEditor,
type: 'primitiveProp', isOpen: isInlineEditorPopoverOpen,
keyframe: props.keyframe, } = useKeyframeInlineEditorPopover([
pathToProp: props.leaf.pathToProp, {
propConfig: props.leaf.propConf, type: 'primitiveProp',
sheetObject: props.leaf.sheetObject, keyframe: props.keyframe,
trackId: props.leaf.trackId, pathToProp: props.leaf.pathToProp,
}, propConfig: props.leaf.propConf,
]) sheetObject: props.leaf.sheetObject,
trackId: props.leaf.trackId,
},
])
const [isDragging] = useDragForSingleKeyframeDot(node, props, { const [isDragging] = useDragForSingleKeyframeDot(node, props, {
onClickFromDrag(dragStartEvent) { onClickFromDrag(dragStartEvent) {
openEditor(dragStartEvent, ref.current!) toggleEditor(dragStartEvent, ref.current!)
}, },
}) })
@ -213,6 +216,8 @@ function useDragForSingleKeyframeDot(
const propsRef = useRef(props) const propsRef = useRef(props)
propsRef.current = props propsRef.current = props
const {onClickFromDrag} = options
const useDragOpts = useMemo<UseDragOpts>(() => { const useDragOpts = useMemo<UseDragOpts>(() => {
return { return {
debugName: 'KeyframeDot/useDragKeyframe', debugName: 'KeyframeDot/useDragKeyframe',
@ -268,7 +273,7 @@ function useDragForSingleKeyframeDot(
return ( return (
handlers && { handlers && {
...handlers, ...handlers,
onClick: options.onClickFromDrag, onClick: onClickFromDrag,
onDragEnd: (...args) => { onDragEnd: (...args) => {
handlers.onDragEnd?.(...args) handlers.onDragEnd?.(...args)
snapToNone() snapToNone()
@ -324,12 +329,12 @@ function useDragForSingleKeyframeDot(
snapToNone() snapToNone()
}, },
onClick(ev) { onClick(ev) {
options.onClickFromDrag(ev) onClickFromDrag(ev)
}, },
} }
}, },
} }
}, []) }, [onClickFromDrag])
const [isDragging] = useDrag(node, useDragOpts) const [isDragging] = useDrag(node, useDragOpts)

View file

@ -32,7 +32,7 @@ const LengthEditorPopover: React.FC<{
* Called when user hits enter/escape * Called when user hits enter/escape
*/ */
onRequestClose: (reason: string) => void onRequestClose: (reason: string) => void
}> = ({layoutP, onRequestClose}) => { }> = ({layoutP}) => {
const sheet = useVal(layoutP.sheet) const sheet = useVal(layoutP.sheet)
const fns = useMemo(() => { const fns = useMemo(() => {
@ -89,7 +89,6 @@ const LengthEditorPopover: React.FC<{
{...fns} {...fns}
isValid={greaterThanZero} isValid={greaterThanZero}
inputRef={inputRef} inputRef={inputRef}
onBlur={onRequestClose.bind(null, 'length editor number input blur')}
nudge={nudge} nudge={nudge}
/> />
</Container> </Container>

View file

@ -138,19 +138,17 @@ const RENDER_OUT_OF_VIEW_X = -10000
const LengthIndicator: React.FC<IProps> = ({layoutP}) => { const LengthIndicator: React.FC<IProps> = ({layoutP}) => {
const [nodeRef, node] = useRefAndState<HTMLDivElement | null>(null) const [nodeRef, node] = useRefAndState<HTMLDivElement | null>(null)
const [isDragging] = useDragBulge(node, {layoutP}) const [isDragging] = useDragBulge(node, {layoutP})
const [popoverNode, openPopover, closePopover, isPopoverOpen] = usePopover( const {
{debugName: 'LengthIndicator'}, node: popoverNode,
() => { toggle: togglePopover,
return ( close: closePopover,
<BasicPopover> } = usePopover({debugName: 'LengthIndicator'}, () => {
<LengthEditorPopover return (
layoutP={layoutP} <BasicPopover>
onRequestClose={closePopover} <LengthEditorPopover layoutP={layoutP} onRequestClose={closePopover} />
/> </BasicPopover>
</BasicPopover> )
) })
},
)
return usePrism(() => { return usePrism(() => {
const sheet = val(layoutP.sheet) const sheet = val(layoutP.sheet)
@ -191,7 +189,7 @@ const LengthIndicator: React.FC<IProps> = ({layoutP}) => {
ref={nodeRef} ref={nodeRef}
// title="Length of the sequence. Drag or click to change." // title="Length of the sequence. Drag or click to change."
onClick={(e) => { onClick={(e) => {
openPopover(e, node!) togglePopover(e, node!)
}} }}
{...includeLockFrameStampAttrs('hide')} {...includeLockFrameStampAttrs('hide')}
> >

View file

@ -70,24 +70,26 @@ const GraphEditorDotNonScalar: React.VFC<IProps> = (props) => {
const curValue = props.which === 'left' ? 0 : 1 const curValue = props.which === 'left' ? 0 : 1
const [inlineEditorPopover, openEditor, _, _isInlineEditorPopoverOpen] = const inlineEditorPopover = useKeyframeInlineEditorPopover([
useKeyframeInlineEditorPopover([ {
{ type: 'primitiveProp',
type: 'primitiveProp', keyframe: props.keyframe,
keyframe: props.keyframe, pathToProp: props.pathToProp,
pathToProp: props.pathToProp, propConfig: props.propConfig,
propConfig: props.propConfig, sheetObject: props.sheetObject,
sheetObject: props.sheetObject, trackId: props.trackId,
trackId: props.trackId, },
}, ])
])
const isDragging = useDragKeyframe({ const isDragging = useDragKeyframe({
node, node,
props, props,
// dragging does not work with also having a click listener // dragging does not work with also having a click listener
onDetectedClick: (event) => onDetectedClick: (event) =>
openEditor(event, event.target instanceof Element ? event.target : node!), inlineEditorPopover.toggle(
event,
event.target instanceof Element ? event.target : node!,
),
}) })
const cyInExtremumSpace = props.extremumSpace.fromValueSpace(curValue) const cyInExtremumSpace = props.extremumSpace.fromValueSpace(curValue)
@ -114,7 +116,7 @@ const GraphEditorDotNonScalar: React.VFC<IProps> = (props) => {
fill: presence.flag === PresenceFlag.Primary ? 'white' : undefined, fill: presence.flag === PresenceFlag.Primary ? 'white' : undefined,
}} }}
/> />
{inlineEditorPopover} {inlineEditorPopover.node}
{contextMenu} {contextMenu}
</> </>
) )

View file

@ -70,24 +70,26 @@ const GraphEditorDotScalar: React.VFC<IProps> = (props) => {
const curValue = cur.value as number const curValue = cur.value as number
const cyInExtremumSpace = props.extremumSpace.fromValueSpace(curValue) const cyInExtremumSpace = props.extremumSpace.fromValueSpace(curValue)
const [inlineEditorPopover, openEditor, _, _isInlineEditorPopoverOpen] = const inlineEditorPopover = useKeyframeInlineEditorPopover([
useKeyframeInlineEditorPopover([ {
{ type: 'primitiveProp',
type: 'primitiveProp', keyframe: props.keyframe,
keyframe: props.keyframe, pathToProp: props.pathToProp,
pathToProp: props.pathToProp, propConfig: props.propConfig,
propConfig: props.propConfig, sheetObject: props.sheetObject,
sheetObject: props.sheetObject, trackId: props.trackId,
trackId: props.trackId, },
}, ])
])
const isDragging = useDragKeyframe({ const isDragging = useDragKeyframe({
node, node,
props, props,
// dragging does not work with also having a click listener // dragging does not work with also having a click listener
onDetectedClick: (event) => onDetectedClick: (event) =>
openEditor(event, event.target instanceof Element ? event.target : node!), inlineEditorPopover.toggle(
event,
event.target instanceof Element ? event.target : node!,
),
}) })
return ( return (
@ -112,7 +114,7 @@ const GraphEditorDotScalar: React.VFC<IProps> = (props) => {
fill: presence.flag === PresenceFlag.Primary ? 'white' : undefined, fill: presence.flag === PresenceFlag.Primary ? 'white' : undefined,
}} }}
/> />
{inlineEditorPopover} {inlineEditorPopover.node}
{contextMenu} {contextMenu}
</> </>
) )

View file

@ -193,19 +193,20 @@ const Playhead: React.FC<{layoutP: Pointer<SequenceEditorPanelLayout>}> = ({
}) => { }) => {
const [thumbRef, thumbNode] = useRefAndState<HTMLElement | null>(null) const [thumbRef, thumbNode] = useRefAndState<HTMLElement | null>(null)
const [popoverNode, openPopover, closePopover, isPopoverOpen] = usePopover( const {
{debugName: 'Playhead'}, node: popoverNode,
() => { toggle: togglePopover,
return ( close: closePopover,
<BasicPopover> } = usePopover({debugName: 'Playhead'}, () => {
<PlayheadPositionPopover return (
layoutP={layoutP} <BasicPopover>
onRequestClose={closePopover} <PlayheadPositionPopover
/> layoutP={layoutP}
</BasicPopover> onRequestClose={closePopover}
) />
}, </BasicPopover>
) )
})
const gestureHandlers = useMemo((): Parameters<typeof useDrag>[1] => { const gestureHandlers = useMemo((): Parameters<typeof useDrag>[1] => {
return { return {
@ -236,7 +237,7 @@ const Playhead: React.FC<{layoutP: Pointer<SequenceEditorPanelLayout>}> = ({
snapToNone() snapToNone()
}, },
onClick(e) { onClick(e) {
openPopover(e, thumbRef.current!) togglePopover(e, thumbRef.current!)
}, },
} }
}, },

View file

@ -33,7 +33,7 @@ const PlayheadPositionPopover: React.FC<{
* Called when user hits enter/escape * Called when user hits enter/escape
*/ */
onRequestClose: (reason: string) => void onRequestClose: (reason: string) => void
}> = ({layoutP, onRequestClose}) => { }> = ({layoutP}) => {
const sheet = val(layoutP.sheet) const sheet = val(layoutP.sheet)
const sequence = sheet.getSequence() const sequence = sheet.getSequence()
@ -80,7 +80,6 @@ const PlayheadPositionPopover: React.FC<{
{...fns} {...fns}
isValid={greaterThanOrEqualToZero} isValid={greaterThanOrEqualToZero}
inputRef={inputRef} inputRef={inputRef}
onBlur={onRequestClose.bind(null, 'number input blur')}
nudge={nudge} nudge={nudge}
/> />
</Container> </Container>

View file

@ -75,31 +75,28 @@ function RgbaPropEditor({
[editingTools], [editingTools],
) )
const [popoverNode, openPopover] = usePopover( const popover = usePopover({debugName: 'RgbaPropEditor'}, () => (
{debugName: 'RgbaPropEditor'}, <RgbaPopover>
() => ( <RgbaColorPicker
<RgbaPopover> color={{
<RgbaColorPicker r: value.r,
color={{ g: value.g,
r: value.r, b: value.b,
g: value.g, a: value.a,
b: value.b, }}
a: value.a, temporarilySetValue={(color) => {
}} const rgba = decorateRgba(color)
temporarilySetValue={(color) => { editingTools.temporarilySetValue(rgba)
const rgba = decorateRgba(color) }}
editingTools.temporarilySetValue(rgba) permanentlySetValue={(color) => {
}} // console.log('perm')
permanentlySetValue={(color) => { const rgba = decorateRgba(color)
// console.log('perm') editingTools.permanentlySetValue(rgba)
const rgba = decorateRgba(color) }}
editingTools.permanentlySetValue(rgba) discardTemporaryValue={editingTools.discardTemporaryValue}
}} />
discardTemporaryValue={editingTools.discardTemporaryValue} </RgbaPopover>
/> ))
</RgbaPopover>
),
)
return ( return (
<> <>
@ -108,7 +105,7 @@ function RgbaPropEditor({
rgbaColor={value} rgbaColor={value}
ref={containerRef} ref={containerRef}
onClick={(e) => { onClick={(e) => {
openPopover(e, containerRef.current) popover.toggle(e, containerRef.current)
}} }}
/> />
<HexInput <HexInput
@ -120,7 +117,7 @@ function RgbaPropEditor({
autoFocus={autoFocus} autoFocus={autoFocus}
/> />
</RowContainer> </RowContainer>
{popoverNode} {popover.node}
</> </>
) )
} }

View file

@ -58,7 +58,7 @@ const HasUpdatesBadge = styled.div`
` `
const GroupDivider = styled.div` const GroupDivider = styled.div`
position: abolute; position: absolute;
height: 32px; height: 32px;
width: 1px; width: 1px;
background: #373b40; background: #373b40;
@ -98,7 +98,7 @@ const GlobalToolbar: React.FC = () => {
const hasUpdates = const hasUpdates =
useVal(getStudio().atomP.ahistoric.updateChecker.result.hasUpdates) === true useVal(getStudio().atomP.ahistoric.updateChecker.result.hasUpdates) === true
const [moreMenu, openMoreMenu] = usePopover( const moreMenu = usePopover(
() => { () => {
const triggerBounds = moreMenuTriggerRef.current!.getBoundingClientRect() const triggerBounds = moreMenuTriggerRef.current!.getBoundingClientRect()
return { return {
@ -166,11 +166,11 @@ const GlobalToolbar: React.FC = () => {
<ExtensionToolbar showLeftDivider toolbarId="global" /> <ExtensionToolbar showLeftDivider toolbarId="global" />
</SubContainer> </SubContainer>
<SubContainer> <SubContainer>
{moreMenu} {moreMenu.node}
<ToolbarIconButton <ToolbarIconButton
ref={moreMenuTriggerRef} ref={moreMenuTriggerRef}
onClick={(e) => { onClick={(e) => {
openMoreMenu(e, moreMenuTriggerRef.current!) moreMenu.toggle(e, moreMenuTriggerRef.current!)
}} }}
> >
<Ellipsis /> <Ellipsis />

View file

@ -138,7 +138,10 @@ const TooltipWrapper: React.FC<{
props.onPointerOutside, props.onPointerOutside,
]) ])
useOnClickOutside(container, props.onClickOutside ?? noop) useOnClickOutside(
[container, props.target ?? null],
props.onClickOutside ?? noop,
)
return ( return (
<ArrowContext.Provider value={arrowContextValue}> <ArrowContext.Provider value={arrowContextValue}>

View file

@ -48,10 +48,22 @@ type Opts = {
verticalGap?: number verticalGap?: number
} }
export interface IPopover {
/**
* The React node of the popover. Insert into your JSX using \{node\}. Its state
* will be managed automatically.
*/
node: React.ReactNode
open: OpenFn
close: CloseFn
toggle: OpenFn
isOpen: boolean
}
export default function usePopover( export default function usePopover(
opts: Opts | (() => Opts), opts: Opts | (() => Opts),
render: () => React.ReactElement, render: () => React.ReactElement,
): [node: React.ReactNode, open: OpenFn, close: CloseFn, isOpen: boolean] { ): IPopover {
const _debug = (...args: any) => {} const _debug = (...args: any) => {}
// want to make sure that we don't close a popover when dragging something (like a curve editor handle) // want to make sure that we don't close a popover when dragging something (like a curve editor handle)
@ -104,6 +116,14 @@ export default function usePopover(
} }
}, []) }, [])
const toggle = useCallback<OpenFn>((...args) => {
if (stateRef.current.isOpen) {
close('toggled')
} else {
open(...args)
}
}, [])
/** /**
* See doc comment on {@link useAutoCloseLockState}. * See doc comment on {@link useAutoCloseLockState}.
* Used to ensure that moving far away from a parent popover doesn't * Used to ensure that moving far away from a parent popover doesn't
@ -146,7 +166,7 @@ export default function usePopover(
<></> <></>
) )
return [node, open, close, state.isOpen] return {node, open, close, toggle, isOpen: state.isOpen}
} }
/** /**

View file

@ -2,15 +2,23 @@ import type {$IntentionalAny} from '@theatre/shared/utils/types'
import {useEffect} from 'react' import {useEffect} from 'react'
export default function useOnClickOutside( export default function useOnClickOutside(
container: Element | null, container: Element | null | (Element | null)[],
onOutside: (e: MouseEvent) => void, onOutside: (e: MouseEvent) => void,
enabled?: boolean, enabled?: boolean,
// Can be used e.g. to prevent unexpected closing-reopening when clicking on a
// popover's trigger.
) { ) {
useEffect(() => { useEffect(() => {
if (!container || enabled === false) return if (!container || enabled === false) return
const containers = Array.isArray(container)
? (container.filter((container) => container) as Element[])
: [container]
const onMouseDown = (e: MouseEvent) => { const onMouseDown = (e: MouseEvent) => {
if (!e.composedPath().includes(container)) { if (
containers.every((container) => !e.composedPath().includes(container))
) {
onOutside(e) onOutside(e)
} }
} }