[WIKI-506] fix: close the link view after 300ms of hovering out #7283

This commit is contained in:
M. Palanikannan
2025-07-02 15:29:32 +05:30
committed by GitHub
parent 1fcffad7dd
commit 295eb1ef72

View File

@@ -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>
)}