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 { autoUpdate, flip, hide, shift, useDismiss, useFloating, useInteractions } from "@floating-ui/react";
|
||||||
import { Editor, useEditorState } from "@tiptap/react";
|
import { Editor, useEditorState } from "@tiptap/react";
|
||||||
import { FC, useCallback, useEffect, useState } from "react";
|
import { FC, useCallback, useEffect, useRef, useState } from "react";
|
||||||
// components
|
// components
|
||||||
import { LinkView, LinkViewProps } from "@/components/links";
|
import { LinkView, LinkViewProps } from "@/components/links";
|
||||||
|
|
||||||
@@ -13,6 +13,7 @@ export const LinkViewContainer: FC<LinkViewContainerProps> = ({ editor, containe
|
|||||||
const [linkViewProps, setLinkViewProps] = useState<LinkViewProps>();
|
const [linkViewProps, setLinkViewProps] = useState<LinkViewProps>();
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [virtualElement, setVirtualElement] = useState<Element | null>(null);
|
const [virtualElement, setVirtualElement] = useState<Element | null>(null);
|
||||||
|
const hoverTimeoutRef = useRef<number | null>(null);
|
||||||
|
|
||||||
const editorState = useEditorState({
|
const editorState = useEditorState({
|
||||||
editor,
|
editor,
|
||||||
@@ -44,9 +45,26 @@ export const LinkViewContainer: FC<LinkViewContainerProps> = ({ editor, containe
|
|||||||
|
|
||||||
const { getReferenceProps, getFloatingProps } = useInteractions([dismiss]);
|
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(
|
const handleLinkHover = useCallback(
|
||||||
(event: MouseEvent) => {
|
(event: MouseEvent) => {
|
||||||
if (!editor || editorState.linkExtensionStorage.isBubbleMenuOpen) return;
|
if (!editor || editorState.linkExtensionStorage?.isBubbleMenuOpen) return;
|
||||||
|
|
||||||
// Find the closest anchor tag from the event target
|
// Find the closest anchor tag from the event target
|
||||||
const target = (event.target as HTMLElement)?.closest("a");
|
const target = (event.target as HTMLElement)?.closest("a");
|
||||||
@@ -72,6 +90,9 @@ export const LinkViewContainer: FC<LinkViewContainerProps> = ({ editor, containe
|
|||||||
|
|
||||||
setVirtualElement(target);
|
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
|
// Only update if not already open or if hovering over a different link
|
||||||
if (!isOpen || (linkViewProps && (linkViewProps.from !== pos || linkViewProps.to !== pos + node.nodeSize))) {
|
if (!isOpen || (linkViewProps && (linkViewProps.from !== pos || linkViewProps.to !== pos + node.nodeSize))) {
|
||||||
setLinkViewProps({
|
setLinkViewProps({
|
||||||
@@ -92,7 +113,46 @@ export const LinkViewContainer: FC<LinkViewContainerProps> = ({ editor, containe
|
|||||||
console.error("Error handling link hover:", error);
|
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
|
// Set up event listeners
|
||||||
@@ -101,15 +161,23 @@ export const LinkViewContainer: FC<LinkViewContainerProps> = ({ editor, containe
|
|||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
container.addEventListener("mouseover", handleLinkHover);
|
container.addEventListener("mouseover", handleLinkHover);
|
||||||
|
container.addEventListener("mouseenter", handleContainerMouseEnter);
|
||||||
|
container.addEventListener("mouseleave", handleContainerMouseLeave);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
container.removeEventListener("mouseover", handleLinkHover);
|
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
|
// Close link view when bubble menu opens
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editorState.linkExtensionStorage.isBubbleMenuOpen && isOpen) {
|
if (editorState.linkExtensionStorage?.isBubbleMenuOpen && isOpen) {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
}
|
}
|
||||||
}, [editorState.linkExtensionStorage, isOpen]);
|
}, [editorState.linkExtensionStorage, isOpen]);
|
||||||
@@ -117,7 +185,13 @@ export const LinkViewContainer: FC<LinkViewContainerProps> = ({ editor, containe
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isOpen && linkViewProps && virtualElement && (
|
{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} />
|
<LinkView {...linkViewProps} style={floatingStyles} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user