@ariakit/react-core
Version:
Ariakit React core
283 lines (280 loc) • 8.93 kB
JavaScript
"use client";
import {
getElementPolygon,
getEventPoint,
isPointInPolygon
} from "./X7QOZUD3.js";
import {
HovercardScopedContextProvider,
useHovercardProviderContext
} from "./2WDBOH5E.js";
import {
usePopover
} from "./4Z6JSVUY.js";
import {
createDialogComponent
} from "./2S4Q4M35.js";
import {
createElement,
createHook,
forwardRef
} from "./ILRXHV7V.js";
import {
useBooleanEvent,
useEvent,
useIsMouseMoving,
useLiveRef,
useMergeRefs,
usePortalRef,
useSafeLayoutEffect,
useWrapElement
} from "./K2XTQB3X.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({
store,
...props
}) {
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 = {
autoFocusOnHide,
finalFocus: finalFocusRef,
...props,
onFocus
};
return props;
}
var NestedHovercardContext = createContext(null);
var useHovercard = createHook(
function useHovercard2({
store,
modal = false,
portal = !!modal,
hideOnEscape = true,
hideOnHoverOutside = true,
disablePointerEventsOnApproach = !!hideOnHoverOutside,
...props
}) {
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 = {
...props,
ref: useMergeRefs(ref, props.ref)
};
props = useAutoFocusOnHide({ store, ...props });
const autoFocusOnShow = store.useState(
(state) => modal || state.autoFocusOnShow
);
props = usePopover({
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
};