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}
+
+
+
+
+
);
};