UNPKG

@ariakit/react-core

Version:

Ariakit React core

296 lines (293 loc) 9.36 kB
"use client"; import { getElementPolygon, getEventPoint, isPointInPolygon } from "./X7QOZUD3.js"; import { HovercardScopedContextProvider, useHovercardProviderContext } from "./7Z7JH52O.js"; import { usePopover } from "./C6DAL6ZN.js"; import { createDialogComponent } from "./CAGBPNDP.js"; import { createElement, createHook, forwardRef } from "./VOQWLFSQ.js"; import { useBooleanEvent, useEvent, useIsMouseMoving, useLiveRef, useMergeRefs, usePortalRef, useSafeLayoutEffect, useWrapElement } from "./5GGHRIN3.js"; import { __objRest, __spreadProps, __spreadValues } from "./3YLGPPWQ.js"; // src/hovercard/hovercard.tsx import { contains } from "@ariakit/core/utils/dom"; import { addGlobalEventListener } from "@ariakit/core/utils/events"; import { hasFocusWithin } from "@ariakit/core/utils/focus"; import { chain, invariant, isFalsyBooleanCallback } from "@ariakit/core/utils/misc"; import { sync } from "@ariakit/core/utils/store"; import { createContext, useCallback, useContext, useEffect, useRef, useState } from "react"; import { jsx } from "react/jsx-runtime"; var TagName = "div"; function isMovingOnHovercard(target, card, anchor, nested) { if (hasFocusWithin(card)) return true; if (!target) return false; if (contains(card, target)) return true; if (anchor && contains(anchor, target)) return true; if (nested == null ? void 0 : nested.some((card2) => isMovingOnHovercard(target, card2, anchor))) { return true; } return false; } function useAutoFocusOnHide(_a) { var _b = _a, { store } = _b, props = __objRest(_b, [ "store" ]); const [autoFocusOnHide, setAutoFocusOnHide] = useState(false); const mounted = store.useState("mounted"); useEffect(() => { if (!mounted) { setAutoFocusOnHide(false); } }, [mounted]); const onFocusProp = props.onFocus; const onFocus = useEvent((event) => { onFocusProp == null ? void 0 : onFocusProp(event); if (event.defaultPrevented) return; setAutoFocusOnHide(true); }); const finalFocusRef = useRef(null); useEffect(() => { return sync(store, ["anchorElement"], (state) => { finalFocusRef.current = state.anchorElement; }); }, []); props = __spreadProps(__spreadValues({ autoFocusOnHide, finalFocus: finalFocusRef }, props), { onFocus }); return props; } var NestedHovercardContext = createContext(null); var useHovercard = createHook( function useHovercard2(_a) { var _b = _a, { store, modal = false, portal = !!modal, hideOnEscape = true, hideOnHoverOutside = true, disablePointerEventsOnApproach = !!hideOnHoverOutside } = _b, props = __objRest(_b, [ "store", "modal", "portal", "hideOnEscape", "hideOnHoverOutside", "disablePointerEventsOnApproach" ]); const context = useHovercardProviderContext(); store = store || context; invariant( store, process.env.NODE_ENV !== "production" && "Hovercard must receive a `store` prop or be wrapped in a HovercardProvider component." ); const ref = useRef(null); const [nestedHovercards, setNestedHovercards] = useState([]); const hideTimeoutRef = useRef(0); const enterPointRef = useRef(null); const { portalRef, domReady } = usePortalRef(portal, props.portalRef); const isMouseMoving = useIsMouseMoving(); const mayHideOnHoverOutside = !!hideOnHoverOutside; const hideOnHoverOutsideProp = useBooleanEvent(hideOnHoverOutside); const mayDisablePointerEvents = !!disablePointerEventsOnApproach; const disablePointerEventsProp = useBooleanEvent( disablePointerEventsOnApproach ); const open = store.useState("open"); const mounted = store.useState("mounted"); useEffect(() => { if (!domReady) return; if (!mounted) return; if (!mayHideOnHoverOutside && !mayDisablePointerEvents) return; const element = ref.current; if (!element) return; const onMouseMove = (event) => { if (!store) return; if (!isMouseMoving()) return; const { anchorElement, hideTimeout, timeout } = store.getState(); const enterPoint = enterPointRef.current; const [target] = event.composedPath(); const anchor = anchorElement; if (isMovingOnHovercard(target, element, anchor, nestedHovercards)) { enterPointRef.current = target && anchor && contains(anchor, target) ? getEventPoint(event) : null; window.clearTimeout(hideTimeoutRef.current); hideTimeoutRef.current = 0; return; } if (hideTimeoutRef.current) return; if (enterPoint) { const currentPoint = getEventPoint(event); const polygon = getElementPolygon(element, enterPoint); if (isPointInPolygon(currentPoint, polygon)) { enterPointRef.current = currentPoint; if (!disablePointerEventsProp(event)) return; event.preventDefault(); event.stopPropagation(); return; } } if (!hideOnHoverOutsideProp(event)) return; hideTimeoutRef.current = window.setTimeout(() => { hideTimeoutRef.current = 0; store == null ? void 0 : store.hide(); }, hideTimeout != null ? hideTimeout : timeout); }; return chain( addGlobalEventListener("mousemove", onMouseMove, true), () => clearTimeout(hideTimeoutRef.current) ); }, [ store, isMouseMoving, domReady, mounted, mayHideOnHoverOutside, mayDisablePointerEvents, nestedHovercards, disablePointerEventsProp, hideOnHoverOutsideProp ]); useEffect(() => { if (!domReady) return; if (!mounted) return; if (!mayDisablePointerEvents) return; const disableEvent = (event) => { const element = ref.current; if (!element) return; const enterPoint = enterPointRef.current; if (!enterPoint) return; const polygon = getElementPolygon(element, enterPoint); if (isPointInPolygon(getEventPoint(event), polygon)) { if (!disablePointerEventsProp(event)) return; event.preventDefault(); event.stopPropagation(); } }; return chain( // Note: we may need to add pointer events here in the future. addGlobalEventListener("mouseenter", disableEvent, true), addGlobalEventListener("mouseover", disableEvent, true), addGlobalEventListener("mouseout", disableEvent, true), addGlobalEventListener("mouseleave", disableEvent, true) ); }, [domReady, mounted, mayDisablePointerEvents, disablePointerEventsProp]); useEffect(() => { if (!domReady) return; if (open) return; store == null ? void 0 : store.setAutoFocusOnShow(false); }, [store, domReady, open]); const openRef = useLiveRef(open); useEffect(() => { if (!domReady) return; return () => { if (!openRef.current) { store == null ? void 0 : store.setAutoFocusOnShow(false); } }; }, [store, domReady]); const registerOnParent = useContext(NestedHovercardContext); useSafeLayoutEffect(() => { if (modal) return; if (!portal) return; if (!mounted) return; if (!domReady) return; const element = ref.current; if (!element) return; return registerOnParent == null ? void 0 : registerOnParent(element); }, [modal, portal, mounted, domReady]); const registerNestedHovercard = useCallback( (element) => { setNestedHovercards((prevElements) => [...prevElements, element]); const parentUnregister = registerOnParent == null ? void 0 : registerOnParent(element); return () => { setNestedHovercards( (prevElements) => prevElements.filter((item) => item !== element) ); parentUnregister == null ? void 0 : parentUnregister(); }; }, [registerOnParent] ); props = useWrapElement( props, (element) => /* @__PURE__ */ jsx(HovercardScopedContextProvider, { value: store, children: /* @__PURE__ */ jsx(NestedHovercardContext.Provider, { value: registerNestedHovercard, children: element }) }), [store, registerNestedHovercard] ); props = __spreadProps(__spreadValues({}, props), { ref: useMergeRefs(ref, props.ref) }); props = useAutoFocusOnHide(__spreadValues({ store }, props)); const autoFocusOnShow = store.useState( (state) => modal || state.autoFocusOnShow ); props = usePopover(__spreadProps(__spreadValues({ store, modal, portal, autoFocusOnShow }, props), { portalRef, hideOnEscape(event) { if (isFalsyBooleanCallback(hideOnEscape, event)) return false; requestAnimationFrame(() => { requestAnimationFrame(() => { store == null ? void 0 : store.hide(); }); }); return true; } })); return props; } ); var Hovercard = createDialogComponent( forwardRef(function Hovercard2(props) { const htmlProps = useHovercard(props); return createElement(TagName, htmlProps); }), useHovercardProviderContext ); export { useHovercard, Hovercard };