primitives icon indicating copy to clipboard operation
primitives copied to clipboard

[HoverCard] Support a custom Anchor like Popover

Open islami00 opened this issue 8 months ago • 2 comments

Feature request

Overview

I would like the HoverCard component to support a custom anchor like the Popover component.

Examples in other libraries

https://www.radix-ui.com/primitives/docs/components/popover#anchor

Who does this impact? Who is this for?

This impacts calendar users in my use case. In a timeline view with resources, event titles are sticky while scrolling. I need to position my HoverCardContent relative to the sticky element so it isn't positioned off the calendar.

Additional context

Below is a demo with my current implementation using a patch-package to match the use case.

https://github.com/user-attachments/assets/91e9fa8a-08b8-4198-b58b-03f80a24f894

This is the patch I used:

diff --git a/dist/index.d.mts b/dist/index.d.mts
index 6d931171c0caedba412acb4b714173608a61a8f5..c3f346e65f05bd56059d46d89c1c26ae3ed4e54a 100644
--- a/dist/index.d.mts
+++ b/dist/index.d.mts
@@ -29,6 +29,11 @@ type PrimitiveLinkProps = React.ComponentPropsWithoutRef<typeof Primitive.a>;
 interface HoverCardTriggerProps extends PrimitiveLinkProps {
 }
 declare const HoverCardTrigger: React.ForwardRefExoticComponent<HoverCardTriggerProps & React.RefAttributes<HTMLAnchorElement>>;
+type PopperAnchorProps = React.ComponentPropsWithoutRef<typeof PopperPrimitive.Anchor>;
+interface HoverCardAnchorProps extends PopperAnchorProps {
+}
+declare const HoverCardAnchor: React.ForwardRefExoticComponent<HoverCardAnchorProps & React.RefAttributes<HTMLDivElement>>;
+
 type PortalProps = React.ComponentPropsWithoutRef<typeof Portal$1>;
 interface HoverCardPortalProps {
     children?: React.ReactNode;
@@ -85,5 +90,6 @@ declare const Trigger: React.ForwardRefExoticComponent<HoverCardTriggerProps & R
 declare const Portal: React.FC<HoverCardPortalProps>;
 declare const Content: React.ForwardRefExoticComponent<HoverCardContentProps & React.RefAttributes<HTMLDivElement>>;
 declare const Arrow: React.ForwardRefExoticComponent<HoverCardArrowProps & React.RefAttributes<SVGSVGElement>>;
+declare const Anchor: React.ForwardRefExoticComponent<HoverCardAnchorProps & React.RefAttributes<HTMLDivElement>>;
 
-export { Arrow, Content, HoverCard, HoverCardArrow, type HoverCardArrowProps, HoverCardContent, type HoverCardContentProps, HoverCardPortal, type HoverCardPortalProps, type HoverCardProps, HoverCardTrigger, type HoverCardTriggerProps, Portal, Root, Trigger, createHoverCardScope };
+export {  Anchor,Arrow, Content, HoverCard, HoverCardArrow,HoverCardAnchor, type HoverCardAnchorProps, type HoverCardArrowProps, HoverCardContent, type HoverCardContentProps, HoverCardPortal, type HoverCardPortalProps, type HoverCardProps, HoverCardTrigger, type HoverCardTriggerProps, Portal, Root, Trigger, createHoverCardScope };
diff --git a/dist/index.mjs b/dist/index.mjs
index ac6fbb6748b755daf8127e26b93ea25d023edaec..6d28c62a3d8e1d9f4f1217d1676f08eb8b8fb694 100644
--- a/dist/index.mjs
+++ b/dist/index.mjs
@@ -35,6 +35,7 @@ var HoverCard = (props) => {
   const closeTimerRef = React.useRef(0);
   const hasSelectionRef = React.useRef(false);
   const isPointerDownOnContentRef = React.useRef(false);
+  const [hasCustomAnchor, setHasCustomAnchor] = React.useState(false);
   const [open = false, setOpen] = useControllableState({
     prop: openProp,
     defaultProp: defaultOpen,
@@ -67,6 +68,9 @@ var HoverCard = (props) => {
       onClose: handleClose,
       onDismiss: handleDismiss,
       hasSelectionRef,
+      hasCustomAnchor,
+      onCustomAnchorAdd: React.useCallback(() => setHasCustomAnchor(true), []),
+      onCustomAnchorRemove: React.useCallback(() => setHasCustomAnchor(false), []),
       isPointerDownOnContentRef,
       children: /* @__PURE__ */ jsx(PopperPrimitive.Root, { ...popperScope, children })
     }
@@ -79,7 +83,7 @@ var HoverCardTrigger = React.forwardRef(
     const { __scopeHoverCard, ...triggerProps } = props;
     const context = useHoverCardContext(TRIGGER_NAME, __scopeHoverCard);
     const popperScope = usePopperScope(__scopeHoverCard);
-    return /* @__PURE__ */ jsx(PopperPrimitive.Anchor, { asChild: true, ...popperScope, children: /* @__PURE__ */ jsx(
+    const trigger = jsx(
       Primitive.a,
       {
         "data-state": context.open ? "open" : "closed",
@@ -91,7 +95,8 @@ var HoverCardTrigger = React.forwardRef(
         onBlur: composeEventHandlers(props.onBlur, context.onClose),
         onTouchStart: composeEventHandlers(props.onTouchStart, (event) => event.preventDefault())
       }
-    ) });
+    );
+    return context.hasCustomAnchor ? trigger : /* @__PURE__ */ jsx(PopperPrimitive.Anchor, { asChild: true, ...popperScope, children: trigger });
   }
 );
 HoverCardTrigger.displayName = TRIGGER_NAME;
@@ -227,6 +232,22 @@ var HoverCardArrow = React.forwardRef(
   }
 );
 HoverCardArrow.displayName = ARROW_NAME;
+
+var ANCHOR_NAME = "HoverCardAnchor";
+var HoverCardAnchor = React.forwardRef(
+  (props, forwardedRef) => {
+    const { __scopeHoverCard, ...anchorProps } = props;
+    const context = useHoverCardContext(ANCHOR_NAME, __scopeHoverCard);
+    const popperScope = usePopperScope(__scopeHoverCard);
+    const { onCustomAnchorAdd, onCustomAnchorRemove } = context;
+    React.useEffect(() => {
+      onCustomAnchorAdd();
+      return () => onCustomAnchorRemove();
+    }, [onCustomAnchorAdd, onCustomAnchorRemove]);
+    return /* @__PURE__ */ jsx(PopperPrimitive.Anchor, { ...popperScope, ...anchorProps, ref: forwardedRef });
+  }
+);
+HoverCardAnchor.displayName = ANCHOR_NAME;
 function excludeTouch(eventHandler) {
   return (event) => event.pointerType === "touch" ? void 0 : eventHandler();
 }
@@ -245,6 +266,7 @@ var Trigger = HoverCardTrigger;
 var Portal = HoverCardPortal;
 var Content2 = HoverCardContent;
 var Arrow2 = HoverCardArrow;
+const Anchor = HoverCardAnchor;
 export {
   Arrow2 as Arrow,
   Content2 as Content,
@@ -255,6 +277,8 @@ export {
   HoverCardTrigger,
   Portal,
   Root2 as Root,
+  Anchor,
+  HoverCardAnchor,
   Trigger,
   createHoverCardScope
 };

islami00 avatar Sep 02 '25 16:09 islami00

This would be fantastic to get merged. I need this exact functionality

Reichenberg avatar Oct 16 '25 12:10 Reichenberg

This would be a great fix for me as well.

GuillaumeDesforges avatar Nov 06 '25 13:11 GuillaumeDesforges