@ariakit/react-core
Version:
Ariakit React core
296 lines (293 loc) • 9.36 kB
JavaScript
"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
};