reka-ui
Version:
Vue port for Radix UI Primitives.
197 lines (195 loc) • 6.04 kB
JavaScript
import { ref, watchEffect } from "vue";
import { createEventHook, refAutoReset } from "@vueuse/shared";
//#region src/shared/useGraceArea.ts
function useGraceArea(triggerElement, containerElement) {
const isPointerInTransit = refAutoReset(false, 300);
const pointerGraceArea = ref(null);
const pointerExit = createEventHook();
function handleRemoveGraceArea() {
pointerGraceArea.value = null;
isPointerInTransit.value = false;
}
function handleCreateGraceArea(event, hoverTarget) {
const currentTarget = event.currentTarget;
const exitPoint = {
x: event.clientX,
y: event.clientY
};
const exitSide = getExitSideFromRect(exitPoint, currentTarget.getBoundingClientRect());
const paddedExitPoints = getPaddedExitPoints(exitPoint, exitSide);
const hoverTargetPoints = getPointsFromRect(hoverTarget.getBoundingClientRect());
const graceArea = getHull([...paddedExitPoints, ...hoverTargetPoints]);
pointerGraceArea.value = graceArea;
isPointerInTransit.value = true;
}
watchEffect((cleanupFn) => {
if (triggerElement.value && containerElement.value) {
const handleTriggerLeave = (event) => handleCreateGraceArea(event, containerElement.value);
const handleContentLeave = (event) => handleCreateGraceArea(event, triggerElement.value);
triggerElement.value.addEventListener("pointerleave", handleTriggerLeave);
containerElement.value.addEventListener("pointerleave", handleContentLeave);
cleanupFn(() => {
triggerElement.value?.removeEventListener("pointerleave", handleTriggerLeave);
containerElement.value?.removeEventListener("pointerleave", handleContentLeave);
});
}
});
watchEffect((cleanupFn) => {
if (pointerGraceArea.value) {
const handleTrackPointerGrace = (event) => {
if (!pointerGraceArea.value || !(event.target instanceof HTMLElement)) return;
const target = event.target;
const pointerPosition = {
x: event.clientX,
y: event.clientY
};
const hasEnteredTarget = triggerElement.value?.contains(target) || containerElement.value?.contains(target);
const isPointerOutsideGraceArea = !isPointInPolygon(pointerPosition, pointerGraceArea.value);
const isAnotherGraceAreaTrigger = !!target.closest("[data-grace-area-trigger]");
if (hasEnteredTarget) handleRemoveGraceArea();
else if (isPointerOutsideGraceArea || isAnotherGraceAreaTrigger) {
handleRemoveGraceArea();
pointerExit.trigger();
}
};
triggerElement.value?.ownerDocument.addEventListener("pointermove", handleTrackPointerGrace);
cleanupFn(() => triggerElement.value?.ownerDocument.removeEventListener("pointermove", handleTrackPointerGrace));
}
});
return {
isPointerInTransit,
onPointerExit: pointerExit.on
};
}
function getExitSideFromRect(point, rect) {
const top = Math.abs(rect.top - point.y);
const bottom = Math.abs(rect.bottom - point.y);
const right = Math.abs(rect.right - point.x);
const left = Math.abs(rect.left - point.x);
switch (Math.min(top, bottom, right, left)) {
case left: return "left";
case right: return "right";
case top: return "top";
case bottom: return "bottom";
default: throw new Error("unreachable");
}
}
function getPaddedExitPoints(exitPoint, exitSide, padding = 5) {
const paddedExitPoints = [];
switch (exitSide) {
case "top":
paddedExitPoints.push({
x: exitPoint.x - padding,
y: exitPoint.y + padding
}, {
x: exitPoint.x + padding,
y: exitPoint.y + padding
});
break;
case "bottom":
paddedExitPoints.push({
x: exitPoint.x - padding,
y: exitPoint.y - padding
}, {
x: exitPoint.x + padding,
y: exitPoint.y - padding
});
break;
case "left":
paddedExitPoints.push({
x: exitPoint.x + padding,
y: exitPoint.y - padding
}, {
x: exitPoint.x + padding,
y: exitPoint.y + padding
});
break;
case "right":
paddedExitPoints.push({
x: exitPoint.x - padding,
y: exitPoint.y - padding
}, {
x: exitPoint.x - padding,
y: exitPoint.y + padding
});
break;
}
return paddedExitPoints;
}
function getPointsFromRect(rect) {
const { top, right, bottom, left } = rect;
return [
{
x: left,
y: top
},
{
x: right,
y: top
},
{
x: right,
y: bottom
},
{
x: left,
y: bottom
}
];
}
function isPointInPolygon(point, polygon) {
const { x, y } = point;
let inside = false;
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
const xi = polygon[i].x;
const yi = polygon[i].y;
const xj = polygon[j].x;
const yj = polygon[j].y;
const intersect = yi > y !== yj > y && x < (xj - xi) * (y - yi) / (yj - yi) + xi;
if (intersect) inside = !inside;
}
return inside;
}
function getHull(points) {
const newPoints = points.slice();
newPoints.sort((a, b) => {
if (a.x < b.x) return -1;
else if (a.x > b.x) return 1;
else if (a.y < b.y) return -1;
else if (a.y > b.y) return 1;
else return 0;
});
return getHullPresorted(newPoints);
}
function getHullPresorted(points) {
if (points.length <= 1) return points.slice();
const upperHull = [];
for (let i = 0; i < points.length; i++) {
const p = points[i];
while (upperHull.length >= 2) {
const q = upperHull[upperHull.length - 1];
const r = upperHull[upperHull.length - 2];
if ((q.x - r.x) * (p.y - r.y) >= (q.y - r.y) * (p.x - r.x)) upperHull.pop();
else break;
}
upperHull.push(p);
}
upperHull.pop();
const lowerHull = [];
for (let i = points.length - 1; i >= 0; i--) {
const p = points[i];
while (lowerHull.length >= 2) {
const q = lowerHull[lowerHull.length - 1];
const r = lowerHull[lowerHull.length - 2];
if ((q.x - r.x) * (p.y - r.y) >= (q.y - r.y) * (p.x - r.x)) lowerHull.pop();
else break;
}
lowerHull.push(p);
}
lowerHull.pop();
if (upperHull.length === 1 && lowerHull.length === 1 && upperHull[0].x === lowerHull[0].x && upperHull[0].y === lowerHull[0].y) return upperHull;
else return upperHull.concat(lowerHull);
}
//#endregion
export { useGraceArea };
//# sourceMappingURL=useGraceArea.js.map