mirror of
https://github.com/makeplane/plane
synced 2025-08-07 19:59:33 +00:00
[WIKI-506] fix: close the link view after 300ms of hovering out #7283
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { autoUpdate, flip, hide, shift, useDismiss, useFloating, useInteractions } from "@floating-ui/react";
|
||||
import { Editor, useEditorState } from "@tiptap/react";
|
||||
import { FC, useCallback, useEffect, useState } from "react";
|
||||
import { FC, useCallback, useEffect, useRef, useState } from "react";
|
||||
// components
|
||||
import { LinkView, LinkViewProps } from "@/components/links";
|
||||
|
||||
@@ -13,6 +13,7 @@ export const LinkViewContainer: FC<LinkViewContainerProps> = ({ editor, containe
|
||||
const [linkViewProps, setLinkViewProps] = useState<LinkViewProps>();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [virtualElement, setVirtualElement] = useState<Element | null>(null);
|
||||
const hoverTimeoutRef = useRef<number | null>(null);
|
||||
|
||||
const editorState = useEditorState({
|
||||
editor,
|
||||
@@ -44,9 +45,26 @@ export const LinkViewContainer: FC<LinkViewContainerProps> = ({ editor, containe
|
||||
|
||||
const { getReferenceProps, getFloatingProps } = useInteractions([dismiss]);
|
||||
|
||||
// Clear any existing timeout
|
||||
const clearHoverTimeout = useCallback(() => {
|
||||
if (hoverTimeoutRef.current) {
|
||||
window.clearTimeout(hoverTimeoutRef.current);
|
||||
hoverTimeoutRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Set timeout to close link view after delay
|
||||
const setCloseTimeout = useCallback(() => {
|
||||
clearHoverTimeout();
|
||||
hoverTimeoutRef.current = window.setTimeout(() => {
|
||||
setIsOpen(false);
|
||||
editorState.linkExtensionStorage.isPreviewOpen = false;
|
||||
}, 400);
|
||||
}, [clearHoverTimeout, editorState.linkExtensionStorage]);
|
||||
|
||||
const handleLinkHover = useCallback(
|
||||
(event: MouseEvent) => {
|
||||
if (!editor || editorState.linkExtensionStorage.isBubbleMenuOpen) return;
|
||||
if (!editor || editorState.linkExtensionStorage?.isBubbleMenuOpen) return;
|
||||
|
||||
// Find the closest anchor tag from the event target
|
||||
const target = (event.target as HTMLElement)?.closest("a");
|
||||
@@ -72,6 +90,9 @@ export const LinkViewContainer: FC<LinkViewContainerProps> = ({ editor, containe
|
||||
|
||||
setVirtualElement(target);
|
||||
|
||||
// Clear any pending close timeout when hovering over a link
|
||||
clearHoverTimeout();
|
||||
|
||||
// Only update if not already open or if hovering over a different link
|
||||
if (!isOpen || (linkViewProps && (linkViewProps.from !== pos || linkViewProps.to !== pos + node.nodeSize))) {
|
||||
setLinkViewProps({
|
||||
@@ -92,7 +113,46 @@ export const LinkViewContainer: FC<LinkViewContainerProps> = ({ editor, containe
|
||||
console.error("Error handling link hover:", error);
|
||||
}
|
||||
},
|
||||
[editor, editorState.linkExtensionStorage, getReferenceProps, isOpen, linkViewProps]
|
||||
[editor, editorState.linkExtensionStorage, getReferenceProps, isOpen, linkViewProps, clearHoverTimeout]
|
||||
);
|
||||
|
||||
// Handle mouse enter on floating element (cancel close timeout)
|
||||
const handleFloatingMouseEnter = useCallback(() => {
|
||||
clearHoverTimeout();
|
||||
}, [clearHoverTimeout]);
|
||||
|
||||
// Handle mouse leave from floating element (start close timeout)
|
||||
const handleFloatingMouseLeave = useCallback(() => {
|
||||
setCloseTimeout();
|
||||
}, [setCloseTimeout]);
|
||||
|
||||
const handleContainerMouseEnter = useCallback(() => {
|
||||
// Cancel any pending close timeout when mouse enters container
|
||||
clearHoverTimeout();
|
||||
}, [clearHoverTimeout]);
|
||||
|
||||
const handleContainerMouseLeave = useCallback(
|
||||
(event: MouseEvent) => {
|
||||
if (!editor || !isOpen) return;
|
||||
|
||||
// Check if mouse is truly leaving the container area
|
||||
const relatedTarget = event.relatedTarget as HTMLElement;
|
||||
const container = containerRef.current;
|
||||
const floatingElement = refs.floating;
|
||||
|
||||
// Only start close timeout if mouse is not moving to the floating element
|
||||
// and is actually leaving the container
|
||||
if (
|
||||
container &&
|
||||
relatedTarget &&
|
||||
!container.contains(relatedTarget) &&
|
||||
(!floatingElement || !floatingElement.current?.contains(relatedTarget))
|
||||
) {
|
||||
setCloseTimeout();
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[editor, isOpen, setCloseTimeout, refs.floating]
|
||||
);
|
||||
|
||||
// Set up event listeners
|
||||
@@ -101,15 +161,23 @@ export const LinkViewContainer: FC<LinkViewContainerProps> = ({ editor, containe
|
||||
if (!container) return;
|
||||
|
||||
container.addEventListener("mouseover", handleLinkHover);
|
||||
container.addEventListener("mouseenter", handleContainerMouseEnter);
|
||||
container.addEventListener("mouseleave", handleContainerMouseLeave);
|
||||
|
||||
return () => {
|
||||
container.removeEventListener("mouseover", handleLinkHover);
|
||||
container.removeEventListener("mouseenter", handleContainerMouseEnter);
|
||||
container.removeEventListener("mouseleave", handleContainerMouseLeave);
|
||||
};
|
||||
}, [handleLinkHover]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [handleLinkHover, handleContainerMouseEnter, handleContainerMouseLeave]);
|
||||
|
||||
// Cleanup timeout on unmount
|
||||
useEffect(() => () => clearHoverTimeout(), [clearHoverTimeout]);
|
||||
|
||||
// Close link view when bubble menu opens
|
||||
useEffect(() => {
|
||||
if (editorState.linkExtensionStorage.isBubbleMenuOpen && isOpen) {
|
||||
if (editorState.linkExtensionStorage?.isBubbleMenuOpen && isOpen) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
}, [editorState.linkExtensionStorage, isOpen]);
|
||||
@@ -117,7 +185,13 @@ export const LinkViewContainer: FC<LinkViewContainerProps> = ({ editor, containe
|
||||
return (
|
||||
<>
|
||||
{isOpen && linkViewProps && virtualElement && (
|
||||
<div ref={refs.setFloating} style={{ ...floatingStyles, zIndex: 100 }} {...getFloatingProps()}>
|
||||
<div
|
||||
ref={refs.setFloating}
|
||||
style={{ ...floatingStyles, zIndex: 100 }}
|
||||
{...getFloatingProps()}
|
||||
onMouseEnter={handleFloatingMouseEnter}
|
||||
onMouseLeave={handleFloatingMouseLeave}
|
||||
>
|
||||
<LinkView {...linkViewProps} style={floatingStyles} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user