diff --git a/src/components/hover-card/base.tsx b/src/components/hover-card/base.tsx index 863474f..8be26fd 100644 --- a/src/components/hover-card/base.tsx +++ b/src/components/hover-card/base.tsx @@ -1,4 +1,4 @@ -import { createEffect, createSignal, onCleanup, Show } from "solid-js"; +import { createContext, createEffect, createSignal, onCleanup, Show, useContext } from "solid-js"; import type { Accessor, JSX } from "solid-js"; import { Portal } from "solid-js/web"; @@ -11,6 +11,13 @@ interface HoverTriggerState { export type HoverTriggerRenderer = (state: HoverTriggerState) => JSX.Element; +/** + * Lets a nested hover card suppress its ancestor's preview while the descendant + * is being interacted with, so nested previews don't overlap. A card calls the + * function on mouseenter and receives an unsubscribe to invoke on mouseleave. + */ +const HoverCardContext = createContext<() => () => void>(); + export const HoverCardError = (props: { message?: string }) => (
{props.message} @@ -36,6 +43,9 @@ interface HoverCardProps { const HoverCard = (props: HoverCardProps) => { const [show, setShow] = createSignal(false); + const [childActive, setChildActive] = createSignal(false); + + const active = () => show() && !childActive(); const [previewSize, setPreviewSize] = createSignal({ width: 0, height: 0 }); const [anchorRect, setAnchorRect] = createSignal(null); @@ -43,6 +53,13 @@ const HoverCard = (props: HoverCardProps) => { let previewRef!: HTMLDivElement; let resizeObserver: ResizeObserver | null = null; let hoverTimeout: number | null = null; + let unsuppressParent: (() => void) | null = null; + + const parentSuppress = useContext(HoverCardContext); + const suppressAncestor = () => { + setChildActive(true); + return () => setChildActive(false); + }; const updateAnchorRect = () => { if (anchorRef) setAnchorRect(anchorRef.getBoundingClientRect()); @@ -75,6 +92,8 @@ const HoverCard = (props: HoverCardProps) => { if (hoverTimeout !== null) { clearTimeout(hoverTimeout); } + unsuppressParent?.(); + unsuppressParent = null; }); const getPreviewStyle = () => { @@ -88,6 +107,11 @@ const HoverCard = (props: HoverCardProps) => { const delay = props.hoverDelay ?? 0; updateAnchorRect(); + // Suppress any ancestor hover card while this one is (or will be) open, so + // nested previews don't overlap. Done immediately, before the hover delay, + // so the ancestor's own delayed preview never flashes underneath us. + unsuppressParent = parentSuppress?.() ?? null; + if (delay > 0) { hoverTimeout = window.setTimeout(() => { props.onHover?.(); @@ -106,28 +130,32 @@ const HoverCard = (props: HoverCardProps) => { hoverTimeout = null; } setShow(false); + unsuppressParent?.(); + unsuppressParent = null; }; return ( - - {props.trigger} - - -
- {props.children} -
-
-
-
+ + + {props.trigger} + + +
+ {props.children} +
+
+
+
+
); };