mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-23 19:05:16 +02:00
refactor: implement CitationHoverPopover component to enhance inline citation functionality and improve user interaction
This commit is contained in:
parent
56239548c8
commit
ea087d1d23
3 changed files with 293 additions and 210 deletions
|
|
@ -0,0 +1,142 @@
|
|||
"use client";
|
||||
|
||||
import type { ComponentProps, HTMLAttributes, ReactElement, ReactNode } from "react";
|
||||
import { useCallback, useEffect, useRef, useSyncExternalStore } from "react";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "./_adapter";
|
||||
|
||||
type PopoverContentProps = ComponentProps<typeof PopoverContent>;
|
||||
|
||||
export type CitationHoverTriggerProps = Pick<
|
||||
HTMLAttributes<HTMLElement>,
|
||||
"onBlur" | "onFocus" | "onPointerEnter" | "onPointerLeave"
|
||||
>;
|
||||
|
||||
interface CitationHoverPopoverProps {
|
||||
id: string;
|
||||
trigger: (props: CitationHoverTriggerProps) => ReactElement;
|
||||
children: ReactNode;
|
||||
contentClassName?: string;
|
||||
side?: PopoverContentProps["side"];
|
||||
align?: PopoverContentProps["align"];
|
||||
sideOffset?: PopoverContentProps["sideOffset"];
|
||||
onContentClick?: PopoverContentProps["onClick"];
|
||||
}
|
||||
|
||||
const OPEN_DELAY_MS = 80;
|
||||
const CLOSE_DELAY_MS = 120;
|
||||
|
||||
let activeCitationId: string | null = null;
|
||||
const listeners = new Set<() => void>();
|
||||
|
||||
function subscribe(listener: () => void) {
|
||||
listeners.add(listener);
|
||||
return () => {
|
||||
listeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
function getSnapshot() {
|
||||
return activeCitationId;
|
||||
}
|
||||
|
||||
function setActiveCitationId(id: string | null) {
|
||||
if (activeCitationId === id) return;
|
||||
activeCitationId = id;
|
||||
for (const listener of listeners) {
|
||||
listener();
|
||||
}
|
||||
}
|
||||
|
||||
function useCitationHoverState(id: string) {
|
||||
const activeId = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
||||
const openTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const closeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const clearTimers = useCallback(() => {
|
||||
if (openTimerRef.current) {
|
||||
clearTimeout(openTimerRef.current);
|
||||
openTimerRef.current = null;
|
||||
}
|
||||
if (closeTimerRef.current) {
|
||||
clearTimeout(closeTimerRef.current);
|
||||
closeTimerRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const open = activeId === id;
|
||||
|
||||
const scheduleOpen = useCallback(() => {
|
||||
clearTimers();
|
||||
openTimerRef.current = setTimeout(() => {
|
||||
setActiveCitationId(id);
|
||||
openTimerRef.current = null;
|
||||
}, OPEN_DELAY_MS);
|
||||
}, [clearTimers, id]);
|
||||
|
||||
const scheduleClose = useCallback(() => {
|
||||
clearTimers();
|
||||
closeTimerRef.current = setTimeout(() => {
|
||||
if (activeCitationId === id) {
|
||||
setActiveCitationId(null);
|
||||
}
|
||||
closeTimerRef.current = null;
|
||||
}, CLOSE_DELAY_MS);
|
||||
}, [clearTimers, id]);
|
||||
|
||||
const handleOpenChange = useCallback(
|
||||
(nextOpen: boolean) => {
|
||||
clearTimers();
|
||||
setActiveCitationId(nextOpen ? id : null);
|
||||
},
|
||||
[clearTimers, id]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearTimers();
|
||||
if (activeCitationId === id) {
|
||||
setActiveCitationId(null);
|
||||
}
|
||||
};
|
||||
}, [clearTimers, id]);
|
||||
|
||||
return { open, scheduleOpen, scheduleClose, handleOpenChange };
|
||||
}
|
||||
|
||||
export function CitationHoverPopover({
|
||||
id,
|
||||
trigger,
|
||||
children,
|
||||
contentClassName,
|
||||
side = "top",
|
||||
align = "start",
|
||||
sideOffset = 6,
|
||||
onContentClick,
|
||||
}: CitationHoverPopoverProps) {
|
||||
const { open, scheduleOpen, scheduleClose, handleOpenChange } = useCitationHoverState(id);
|
||||
const hoverProps = {
|
||||
onPointerEnter: scheduleOpen,
|
||||
onPointerLeave: scheduleClose,
|
||||
onFocus: scheduleOpen,
|
||||
onBlur: scheduleClose,
|
||||
} satisfies CitationHoverTriggerProps;
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={handleOpenChange}>
|
||||
<PopoverTrigger asChild>{trigger(hoverProps)}</PopoverTrigger>
|
||||
<PopoverContent
|
||||
side={side}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={contentClassName}
|
||||
onPointerEnter={scheduleOpen}
|
||||
onPointerLeave={scheduleClose}
|
||||
onOpenAutoFocus={(event) => event.preventDefault()}
|
||||
onCloseAutoFocus={(event) => event.preventDefault()}
|
||||
onClick={onContentClick}
|
||||
>
|
||||
{children}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue