Skip to content

Commit

Permalink
Fix Hovercard when using multiple anchors (#3037)
Browse files Browse the repository at this point in the history
When multiple `HovercardAnchor` elements are used for the same
`Hovercard`, a bug occurred in the popover's position because the
`anchorElement` was reset each time the popover was mounted. This PR
resolves the problem by not depending on the `mounted` state, but
instead setting the `disclosureElement` state in a microtask following
the display of the popover.
  • Loading branch information
diegohaz committed Nov 13, 2023
1 parent b26cdee commit 62ca059
Show file tree
Hide file tree
Showing 2 changed files with 31 additions and 11 deletions.
6 changes: 6 additions & 0 deletions .changeset/3037-hovercard-anchor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@ariakit/react-core": patch
"@ariakit/react": patch
---

Fixed [Hovercard](https://ariakit.org/components/hovercard) when used with multiple [`HovercardAnchor`](https://ariakit.org/reference/hovercard-anchor) elements.
36 changes: 25 additions & 11 deletions packages/ariakit-react-core/src/hovercard/hovercard-anchor.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { MouseEvent as ReactMouseEvent } from "react";
import { useEffect, useRef } from "react";
import { useCallback, useEffect, useRef } from "react";
import { addGlobalEventListener } from "@ariakit/core/utils/events";
import { disabledFromProps, invariant } from "@ariakit/core/utils/misc";
import type { BooleanOrCallback } from "@ariakit/core/utils/types";
Expand Down Expand Up @@ -38,7 +38,6 @@ export const useHovercardAnchor = createHook<HovercardAnchorOptions>(
"HovercardAnchor must receive a `store` prop or be wrapped in a HovercardProvider component.",
);

const mounted = store.useState("mounted");
const disabled = disabledFromProps(props);
const showTimeoutRef = useRef(0);

Expand Down Expand Up @@ -67,35 +66,50 @@ export const useHovercardAnchor = createHook<HovercardAnchorOptions>(

const onMouseMove = useEvent(
(event: ReactMouseEvent<HTMLAnchorElement>) => {
store?.setAnchorElement(event.currentTarget);
onMouseMoveProp?.(event);
if (disabled) return;
if (event.defaultPrevented) return;
if (showTimeoutRef.current) return;
if (!isMouseMoving()) return;
if (!store) return;
if (!showOnHoverProp(event)) return;
const element = event.currentTarget;
store.setAnchorElement(element);
const { showTimeout, timeout } = store.getState();
showTimeoutRef.current = window.setTimeout(() => {
showTimeoutRef.current = 0;
// Let's check again if the mouse is moving. This is to avoid showing
// the hovercard on mobile clicks or after clicking on the anchor.
if (!isMouseMoving()) return;
store?.setAnchorElement(element);
store?.show();
queueMicrotask(() => {
// We need to set the anchor element as the hovercard disclosure
// element only when the hovercard is shown so it doesn't get
// assigned an arbitrary element by the dialog component.
store?.setDisclosureElement(element);
});
}, showTimeout ?? timeout);
},
);

const ref = useCallback(
(element: HTMLElement | null) => {
if (!store) return;
const { anchorElement } = store.getState();
if (anchorElement?.isConnected) return;
// We can set the anchor element only if it isn't already set or if it's
// not linked to the DOM. This helps prevent the anchor element from
// being reassigned to a different element when using multiple anchors
// and new anchors are added to the DOM.
store.setAnchorElement(element);
},
[store],
);

props = {
...props,
ref: useMergeRefs(
store.setAnchorElement,
// We need to set the anchor element as the hovercard disclosure
// disclosure element only when the hovercard is shown so it doesn't get
// assigned an arbitrary element by the dialog component.
mounted ? store.setDisclosureElement : undefined,
props.ref,
),
ref: useMergeRefs(ref, props.ref),
onMouseMove,
};

Expand Down

1 comment on commit 62ca059

@vercel
Copy link

@vercel vercel bot commented on 62ca059 Nov 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

ariakit – ./

ariakit-ariakit.vercel.app
ariakit.org
www.ariakit.org
ariakit-git-main-ariakit.vercel.app

Please sign in to comment.