From 0ae921975a54e1d6fb6e35086cf4342c52c3df7c Mon Sep 17 00:00:00 2001 From: Zubair286 Date: Fri, 25 Oct 2024 12:48:15 +0500 Subject: [PATCH] fix hover highlight and arrow render when canDrop is false --- src/components/LayoutTree.tsx | 28 ++++++++++++---------- src/components/Thought.tsx | 15 ++++++------ src/durations.config.ts | 1 + src/hooks/useDragAndDropThought.tsx | 14 +++++++++-- src/hooks/useDragLeave.ts | 36 ++++++++++++++++++++--------- 5 files changed, 62 insertions(+), 32 deletions(-) diff --git a/src/components/LayoutTree.tsx b/src/components/LayoutTree.tsx index ddf0c6cb09..2fa29fd93e 100644 --- a/src/components/LayoutTree.tsx +++ b/src/components/LayoutTree.tsx @@ -372,20 +372,23 @@ const LayoutTree = () => { ) const scrollTop = scrollTopStore.useState() - const [bulletWidth, setBulletWidth] = useState(0) - const toolbarHeightRef = useRef(0) - const distanceFromTop = useRef(0) + // Width of thought bullet + const [bulletWidth, setBulletWidth] = useState(0) + // Height of toolbar element + const [toolbarHeight, setToolbarHeight] = useState(0) + // Distance from toolbar to the first visible thought + const [distanceFromTop, setDistanceFromTop] = useState(0) // set the bullet width only during drag or when simulateDrop is true useLayoutEffect(() => { if (dragInProgress || testFlags.simulateDrop) { const bullet = ref.current?.querySelector('[aria-label=bullet]') - setBulletWidth(bullet?.getBoundingClientRect().width) + if (bullet) setBulletWidth(bullet?.getBoundingClientRect().width) const toolbar = document.querySelector('#toolbar') + if (toolbar) setToolbarHeight(toolbar.getBoundingClientRect().height) - if (toolbar) toolbarHeightRef.current = toolbar?.getBoundingClientRect().height - distanceFromTop.current = (ref.current?.getBoundingClientRect().top ?? 0) + scrollTop + setDistanceFromTop((ref.current?.getBoundingClientRect().top ?? 0) + scrollTop) } }, [dragInProgress, scrollTop]) @@ -471,7 +474,7 @@ const LayoutTree = () => { const { footerHeight, navbarHeight } = useNavAndFooterHeight() const navAndFooterHeight = navbarHeight + footerHeight - const maxVisibleY = viewportHeight + scrollTop - ((distanceFromTop.current ?? 0) + navbarHeight) + const maxVisibleY = viewportHeight + scrollTop - (distanceFromTop + navbarHeight) const { isSortedContext, newRank, hoveringOnDropEnd } = useSortedContext() @@ -504,7 +507,7 @@ const LayoutTree = () => { let yaccum = 0 let indentCursorAncestorTables = 0 - // Arrow visibility based on insertionY + // Arrow visibility based on the rank of drop target in sorted context. let hoverArrowVisibility: 'above' | 'below' | null = null // The rank of the first and last thoughts in sorted context. @@ -626,15 +629,15 @@ const LayoutTree = () => { if (isInSortedContext) { const currentThought = getThoughtById(state, head(node.path)) - // Update first and last thought ranks in sorted context + // Get first and last thought ranks in sorted context if (!firstThoughtRank) { firstThoughtRank = currentThought.rank } lastThoughtRank = currentThought.rank // Check if the current thought is visible - if (y < maxVisibleY && y > scrollTop - (toolbarHeightRef.current ?? 0)) { - // Update first and last visible thought ranks in sorted context + if (y < maxVisibleY && y > scrollTop - toolbarHeight) { + // Get first and last visible thought ranks in sorted context if (!firstVisibleThoughtRank) { firstVisibleThoughtRank = currentThought.rank } @@ -655,7 +658,7 @@ const LayoutTree = () => { }) // Determine hoverArrowVisibility based on newRank and the visible thoughts - if (isSortedContext || hoveringOnDropEnd) { + if (newRank > 0 && (isSortedContext || hoveringOnDropEnd)) { if (newRank > lastVisibleThoughtRank && lastVisibleThoughtRank !== lastThoughtRank) { hoverArrowVisibility = 'below' } else if (newRank < firstVisibleThoughtRank && firstVisibleThoughtRank !== firstThoughtRank) { @@ -675,6 +678,7 @@ const LayoutTree = () => { sizes, maxVisibleY, scrollTop, + toolbarHeight, newRank, ]) diff --git a/src/components/Thought.tsx b/src/components/Thought.tsx index c850ec6834..db6ddaf53a 100644 --- a/src/components/Thought.tsx +++ b/src/components/Thought.tsx @@ -162,15 +162,16 @@ const ThoughtContainer = ({ equalPath(cursorParent, path) }) - const { isDragging, dragSource, isHovering, isBeingHoveredOver, dropTarget, isDeepHovering } = useDragAndDropThought({ - path, - simplePath, - isVisible, - isCursorParent, - }) + const { isDragging, dragSource, isHovering, isBeingHoveredOver, dropTarget, canDropThought, isDeepHovering } = + useDragAndDropThought({ + path, + simplePath, + isVisible, + isCursorParent, + }) useHoveringPath(path, isBeingHoveredOver, DropThoughtZone.ThoughtDrop) - useDragLeave({ isDeepHovering }) + useDragLeave({ isDeepHovering, canDropThought }) // check if the cursor is editing a thought directly const isEditing = useSelector(state => equalPath(state.cursor, path)) diff --git a/src/durations.config.ts b/src/durations.config.ts index 3908e3eb1a..cefae7d22e 100644 --- a/src/durations.config.ts +++ b/src/durations.config.ts @@ -24,6 +24,7 @@ const durationsMillis = { layoutNodeAnimationDuration: 150, /** The time it takes the trace TraceGesture to fade in/out. */ traceOpacityDuration: 150, + arrowBobbleAnimation: 1000, } as const export default durationsMillis diff --git a/src/hooks/useDragAndDropThought.tsx b/src/hooks/useDragAndDropThought.tsx index 9bd01cc8e1..baaa8231d2 100644 --- a/src/hooks/useDragAndDropThought.tsx +++ b/src/hooks/useDragAndDropThought.tsx @@ -207,6 +207,7 @@ const dropCollect = (monitor: DropTargetMonitor) => ({ // is being hovered over current thought irrespective of whether the given item is droppable isBeingHoveredOver: monitor.isOver({ shallow: true }), isDeepHovering: monitor.isOver(), + canDropThought: monitor.canDrop(), }) /** A draggable and droppable Thought hook. */ @@ -221,14 +222,23 @@ const useDragAndDropThought = (props: Partial) => { collect: dragCollect, }) - const [{ isHovering, isBeingHoveredOver, isDeepHovering }, dropTarget] = useDrop({ + const [{ isHovering, isBeingHoveredOver, isDeepHovering, canDropThought }, dropTarget] = useDrop({ accept: [DragAndDropType.Thought, NativeTypes.FILE], canDrop: (item, monitor) => canDrop(propsTypes, monitor), drop: (item, monitor) => drop(propsTypes, monitor), collect: dropCollect, }) - return { isDragging, dragSource, dragPreview, isHovering, isBeingHoveredOver, isDeepHovering, dropTarget } + return { + isDragging, + dragSource, + dragPreview, + isHovering, + isBeingHoveredOver, + isDeepHovering, + canDropThought, + dropTarget, + } } export default useDragAndDropThought diff --git a/src/hooks/useDragLeave.ts b/src/hooks/useDragLeave.ts index 77745b830b..0a175f7fb8 100644 --- a/src/hooks/useDragLeave.ts +++ b/src/hooks/useDragLeave.ts @@ -1,8 +1,7 @@ import { debounce } from 'lodash' import { useEffect, useRef } from 'react' -import { useDispatch } from 'react-redux' +import { useDispatch, useSelector } from 'react-redux' import { updateHoveringPathActionCreator } from '../actions/updateHoveringPath' -import useSortedContext from './useSortedContext' const DEBOUNCE_DELAY = 50 let hoverCount = 0 @@ -11,10 +10,11 @@ let debouncedSetHoveringPath: ReturnType | null = null /** * Hook to capture the dragleave event and dispatch the dragInProgress action. */ -const useDragLeave = ({ isDeepHovering }: { isDeepHovering: boolean }) => { +const useDragLeave = ({ isDeepHovering, canDropThought }: { isDeepHovering: boolean; canDropThought: boolean }) => { const dispatch = useDispatch() + const hoverZone = useSelector(state => state.hoverZone) const prevIsDeepHoveringRef = useRef(isDeepHovering) - const { hoveringOnDropEnd } = useSortedContext() + const prevHoverZone = useRef(hoverZone) // Initialize the debounced function if it hasn't been already if (!debouncedSetHoveringPath) { @@ -25,33 +25,47 @@ const useDragLeave = ({ isDeepHovering }: { isDeepHovering: boolean }) => { } useEffect(() => { + if (prevHoverZone.current !== hoverZone) { + // Cancel any debounce function if hovering on subthought + prevHoverZone.current = hoverZone + debouncedSetHoveringPath?.cancel() + return + } + + // If canDrop is false return + if (!canDropThought) { + dispatch(updateHoveringPathActionCreator({ path: undefined })) + return + } + if (isDeepHovering && !prevIsDeepHoveringRef.current) { - // Cursor has entered a drop target + // Cursor has entered a drop target, increase hover count hoverCount += 1 // Cancel any pending debounce since we're over a drop target debouncedSetHoveringPath?.cancel() - } else if (!isDeepHovering && prevIsDeepHoveringRef.current) { - // Cursor has left a drop target - if (hoverCount === 0 && !hoveringOnDropEnd) { + } else { + // Cursor has left a drop target, decrease hover count + hoverCount = Math.max(hoverCount - 1, 0) + if (hoverCount === 0) { // No drop targets are being hovered over; start debounce debouncedSetHoveringPath?.() } } prevIsDeepHoveringRef.current = isDeepHovering + prevHoverZone.current = hoverZone return () => { // Cleanup on unmount if (isDeepHovering) { - hoverCount = Math.max(hoverCount - 1, 0) - if (hoverCount === 0) { // Start debounce when unmounting and no more drop targets are hovered debouncedSetHoveringPath?.() } } } - }, [isDeepHovering, dispatch, hoveringOnDropEnd]) + }, [isDeepHovering, dispatch, hoverZone, canDropThought]) } + export default useDragLeave