|
|
|
|
@ -1,6 +1,7 @@
|
|
|
|
|
import { watch } from 'runed';
|
|
|
|
|
import type { Side } from '@floating-ui/dom';
|
|
|
|
|
import { isElement, type ActiveDom } from '$adom';
|
|
|
|
|
import type { TimerHandle, TimerScheduler } from '$timer';
|
|
|
|
|
|
|
|
|
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
@ -49,6 +50,7 @@ function getSide(triggerRect: DOMRect, contentRect: DOMRect): Side {
|
|
|
|
|
|
|
|
|
|
export type SafePolygonOptions = {
|
|
|
|
|
dom: ActiveDom;
|
|
|
|
|
timers?: TimerScheduler;
|
|
|
|
|
enabled: () => boolean;
|
|
|
|
|
triggerNode: () => HTMLElement | null;
|
|
|
|
|
contentNode: () => HTMLElement | null;
|
|
|
|
|
@ -62,12 +64,15 @@ export type SafePolygonOptions = {
|
|
|
|
|
setIsPointerInTransit?: (value: boolean) => void;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let safePolygonId = 0;
|
|
|
|
|
|
|
|
|
|
// ─── SafePolygon ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
export class SafePolygon {
|
|
|
|
|
readonly opts: SafePolygonOptions;
|
|
|
|
|
readonly buffer: number;
|
|
|
|
|
readonly transitIntentTimeout: number | null;
|
|
|
|
|
readonly timerKey = `soma:safe-polygon:${++safePolygonId}:transit-intent`;
|
|
|
|
|
|
|
|
|
|
exitPoint: Point | null = null;
|
|
|
|
|
exitTarget: 'trigger' | 'content' | null = null;
|
|
|
|
|
@ -75,6 +80,7 @@ export class SafePolygon {
|
|
|
|
|
trackedTriggerNode: HTMLElement | null = null;
|
|
|
|
|
leaveFallbackRafId: number | null = null;
|
|
|
|
|
transitIntentTimeoutId: number | null = null;
|
|
|
|
|
transitIntentTimer: TimerHandle | null = null;
|
|
|
|
|
|
|
|
|
|
cancelLeaveFallback() {
|
|
|
|
|
if (this.leaveFallbackRafId !== null) {
|
|
|
|
|
@ -94,6 +100,10 @@ export class SafePolygon {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cancelTransitIntentTimeout() {
|
|
|
|
|
if (this.transitIntentTimer !== null) {
|
|
|
|
|
this.transitIntentTimer.cancel();
|
|
|
|
|
this.transitIntentTimer = null;
|
|
|
|
|
}
|
|
|
|
|
if (this.transitIntentTimeoutId !== null) {
|
|
|
|
|
this.opts.dom.getWindow(this.trackedTriggerNode).clearTimeout(this.transitIntentTimeoutId);
|
|
|
|
|
this.transitIntentTimeoutId = null;
|
|
|
|
|
@ -103,13 +113,30 @@ export class SafePolygon {
|
|
|
|
|
scheduleTransitIntentTimeout() {
|
|
|
|
|
if (this.transitIntentTimeout === null) return;
|
|
|
|
|
this.cancelTransitIntentTimeout();
|
|
|
|
|
const leave = () => {
|
|
|
|
|
this.transitIntentTimer = null;
|
|
|
|
|
this.transitIntentTimeoutId = null;
|
|
|
|
|
if (!this.exitPoint || !this.exitTarget) return;
|
|
|
|
|
this.clearTracking();
|
|
|
|
|
this.opts.onPointerExit();
|
|
|
|
|
};
|
|
|
|
|
if (this.opts.timers) {
|
|
|
|
|
this.transitIntentTimer = this.opts.timers.schedule(
|
|
|
|
|
this.timerKey,
|
|
|
|
|
this.transitIntentTimeout,
|
|
|
|
|
leave,
|
|
|
|
|
{
|
|
|
|
|
replace: true,
|
|
|
|
|
meta: { component: 'safe-polygon', action: 'transit-intent' }
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this.transitIntentTimeoutId = this.opts.dom
|
|
|
|
|
.getWindow(this.trackedTriggerNode)
|
|
|
|
|
.setTimeout(() => {
|
|
|
|
|
this.transitIntentTimeoutId = null;
|
|
|
|
|
if (!this.exitPoint || !this.exitTarget) return;
|
|
|
|
|
this.clearTracking();
|
|
|
|
|
this.opts.onPointerExit();
|
|
|
|
|
leave();
|
|
|
|
|
}, this.transitIntentTimeout);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|