UNPKG

reka-ui

Version:

Vue port for Radix UI Primitives.

224 lines (197 loc) 7.18 kB
import type { Ref } from 'vue' import type { Side } from '@/Popper/utils' import { createEventHook, refAutoReset } from '@vueuse/shared' import { ref, watchEffect } from 'vue' export function useGraceArea(triggerElement: Ref<HTMLElement | undefined>, containerElement: Ref<HTMLElement | undefined>) { // Reset the inTransit state if idle/scrolled. const isPointerInTransit = refAutoReset(false, 300) const pointerGraceArea = ref<Polygon | null>(null) const pointerExit = createEventHook<void>() function handleRemoveGraceArea() { pointerGraceArea.value = null isPointerInTransit.value = false } function handleCreateGraceArea(event: PointerEvent, hoverTarget: HTMLElement) { const currentTarget = event.currentTarget as HTMLElement 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: PointerEvent) => handleCreateGraceArea(event, containerElement.value!) const handleContentLeave = (event: PointerEvent) => 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: PointerEvent) => { 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, } } interface Point { x: number, y: number } type Polygon = Point[] function getExitSideFromRect(point: Point, rect: DOMRect): Side { 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: Point, exitSide: Side, padding = 5) { const paddedExitPoints: Point[] = [] 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: DOMRect) { const { top, right, bottom, left } = rect return [ { x: left, y: top }, { x: right, y: top }, { x: right, y: bottom }, { x: left, y: bottom }, ] } // Determine if a point is inside of a polygon. // Based on https://github.com/substack/point-in-polygon function isPointInPolygon(point: Point, polygon: 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 // prettier-ignore const intersect = ((yi > y) !== (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi) if (intersect) inside = !inside } return inside } // Returns a new array of points representing the convex hull of the given set of points. // https://www.nayuki.io/page/convex-hull-algorithm function getHull<P extends Point>(points: Readonly<Array<P>>): Array<P> { const newPoints: Array<P> = points.slice() newPoints.sort((a: Point, b: Point) => { 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) } // Returns the convex hull, assuming that each points[i] <= points[i + 1]. Runs in O(n) time. function getHullPresorted<P extends Point>(points: Readonly<Array<P>>): Array<P> { if (points.length <= 1) return points.slice() const upperHull: Array<P> = [] 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: Array<P> = [] 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) } }