Route Soma hover delays through timers

active-uix
dev 5 months ago
parent 6578cefdaf
commit 9a43cce697

@ -20,7 +20,7 @@ import {
import { linkPreviewMorfo } from '../../../morfo/components/link-preview'; import { linkPreviewMorfo } from '../../../morfo/components/link-preview';
import { Soma } from '../../core/soma.svelte'; import { Soma } from '../../core/soma.svelte';
import type { SomaRuntime, SomaRuntimePart } from '../../runtime.svelte'; import type { SomaRuntime, SomaRuntimePart } from '../../runtime.svelte';
import type { TimerHandle } from '$timer';
// ── Root ───────────────────────────────────────────────────────────────────── // ── Root ─────────────────────────────────────────────────────────────────────
@ -55,8 +55,8 @@ export class LinkPreviewProvider {
readonly floatingProvider: FloatingProvider; readonly floatingProvider: FloatingProvider;
readonly contentPresence: Presence; readonly contentPresence: Presence;
private openTimeout: ReturnType<typeof setTimeout> | null = null; private openTimer: TimerHandle | null = null;
private closeTimeout: ReturnType<typeof setTimeout> | null = null; private closeTimer: TimerHandle | null = null;
contentId = state(''); contentId = state('');
triggerId = state(''); triggerId = state('');
@ -94,57 +94,75 @@ export class LinkPreviewProvider {
}); });
$effect(() => { $effect(() => {
return () => this.clearTimeouts(); return () => this.clearTimers();
}); });
} }
handleOpen() { handleOpen() {
if (this.opts.disabled.current) return; if (this.opts.disabled.current) return;
this.clearTimeouts(); this.clearTimers();
this.openTimeout = setTimeout(() => { this.openTimer = this.soma.uix.timers.schedule(
this.openTimeout = null; this.openTimerKey,
this.opts.open.current = true; this.opts.openDelay.current,
}, this.opts.openDelay.current); () => {
this.openTimer = null;
this.opts.open.current = true;
},
{ replace: true, meta: { component: 'link-preview', action: 'open-delay' } }
);
} }
/** Close after closeDelay (or immediately when bypass=true, e.g. SafePolygon exit). */ /** Close after closeDelay (or immediately when bypass=true, e.g. SafePolygon exit). */
handleClose(bypass = false) { handleClose(bypass = false) {
this.clearTimeouts(); this.clearTimers();
if (bypass || this.opts.closeDelay.current <= 0) { if (bypass || this.opts.closeDelay.current <= 0) {
this.opts.open.current = false; this.opts.open.current = false;
return; return;
} }
this.closeTimeout = setTimeout(() => { this.closeTimer = this.soma.uix.timers.schedule(
this.closeTimeout = null; this.closeTimerKey,
this.opts.open.current = false; this.opts.closeDelay.current,
}, this.opts.closeDelay.current); () => {
this.closeTimer = null;
this.opts.open.current = false;
},
{ replace: true, meta: { component: 'link-preview', action: 'close-delay' } }
);
} }
/** Cancel a pending close (when pointer enters the content). */ /** Cancel a pending close (when pointer enters the content). */
cancelClose() { cancelClose() {
if (this.closeTimeout !== null) { if (this.closeTimer !== null) {
clearTimeout(this.closeTimeout); this.closeTimer.cancel();
this.closeTimeout = null; this.closeTimer = null;
} }
} }
/** Immediate close bypassing delays (Escape, outside click). */ /** Immediate close bypassing delays (Escape, outside click). */
forceClose() { forceClose() {
this.clearTimeouts(); this.clearTimers();
this.opts.open.current = false; this.opts.open.current = false;
} }
private clearTimeouts() { private clearTimers() {
if (this.openTimeout !== null) { if (this.openTimer !== null) {
clearTimeout(this.openTimeout); this.openTimer.cancel();
this.openTimeout = null; this.openTimer = null;
} }
if (this.closeTimeout !== null) { if (this.closeTimer !== null) {
clearTimeout(this.closeTimeout); this.closeTimer.cancel();
this.closeTimeout = null; this.closeTimer = null;
} }
} }
private get openTimerKey() {
return `soma:link-preview:${this.opts.id.current}:open`;
}
private get closeTimerKey() {
return `soma:link-preview:${this.opts.id.current}:close`;
}
setContentRef(el: HTMLElement | null) { setContentRef(el: HTMLElement | null) {
this.contentRef.current = el; this.contentRef.current = el;
} }

@ -31,7 +31,7 @@ import {
import { tooltipMorfo } from '../../../morfo/components/tooltip'; import { tooltipMorfo } from '../../../morfo/components/tooltip';
import { Soma } from '../../core/soma.svelte'; import { Soma } from '../../core/soma.svelte';
import type { SomaRuntime, SomaRuntimePart } from '../../runtime.svelte'; import type { SomaRuntime, SomaRuntimePart } from '../../runtime.svelte';
import type { TimerHandle } from '$timer';
export type TooltipState = 'closed' | 'delayed-open' | 'instant-open'; export type TooltipState = 'closed' | 'delayed-open' | 'instant-open';
@ -112,8 +112,8 @@ export class TooltipProvider {
readonly contentPresence: Presence; readonly contentPresence: Presence;
private internalTooltipState = $state<TooltipState>('closed'); private internalTooltipState = $state<TooltipState>('closed');
private openTimeout: ReturnType<typeof setTimeout> | null = null; private openTimer: TimerHandle | null = null;
private closeTimeout: ReturnType<typeof setTimeout> | null = null; private closeTimer: TimerHandle | null = null;
private contentRef = state<HTMLElement | null>(null); private contentRef = state<HTMLElement | null>(null);
/** /**
@ -167,6 +167,10 @@ export class TooltipProvider {
buffer: 2, buffer: 2,
transitIntentTimeout: 200 transitIntentTimeout: 200
}); });
$effect(() => {
return () => this.clearTimers();
});
} }
/** Effective delay (per-tooltip override → group default). */ /** Effective delay (per-tooltip override → group default). */
@ -190,28 +194,40 @@ export class TooltipProvider {
handleOpen(instant = false) { handleOpen(instant = false) {
if (this.isDisabled) return; if (this.isDisabled) return;
this.clearTimeouts(); this.clearTimers();
if (instant || this.group?.isWithinSkipDelay()) { if (instant || this.group?.isWithinSkipDelay()) {
this.internalTooltipState = 'instant-open'; this.internalTooltipState = 'instant-open';
this.opts.open.current = true; this.opts.open.current = true;
} else { } else {
this.internalTooltipState = 'delayed-open'; this.internalTooltipState = 'delayed-open';
this.openTimeout = setTimeout(() => { this.openTimer = this.soma.uix.timers.schedule(
this.internalTooltipState = 'instant-open'; this.openTimerKey,
this.opts.open.current = true; this.delay,
}, this.delay); () => {
this.openTimer = null;
this.internalTooltipState = 'instant-open';
this.opts.open.current = true;
},
{ replace: true, meta: { component: 'tooltip', action: 'open-delay' } }
);
} }
} }
handleClose() { handleClose() {
this.clearTimeouts(); this.clearTimers();
const closeDelay = this.opts.closeDelay.current; const closeDelay = this.opts.closeDelay.current;
if (closeDelay > 0) { if (closeDelay > 0) {
this.closeTimeout = setTimeout(() => { this.closeTimer = this.soma.uix.timers.schedule(
this.doClose(); this.closeTimerKey,
}, closeDelay); closeDelay,
() => {
this.closeTimer = null;
this.doClose();
},
{ replace: true, meta: { component: 'tooltip', action: 'close-delay' } }
);
} else { } else {
this.doClose(); this.doClose();
} }
@ -225,23 +241,31 @@ export class TooltipProvider {
/** Cancel pending close (when pointer enters content). */ /** Cancel pending close (when pointer enters content). */
cancelClose() { cancelClose() {
if (this.closeTimeout !== null) { if (this.closeTimer !== null) {
clearTimeout(this.closeTimeout); this.closeTimer.cancel();
this.closeTimeout = null; this.closeTimer = null;
} }
} }
private clearTimeouts() { private clearTimers() {
if (this.openTimeout !== null) { if (this.openTimer !== null) {
clearTimeout(this.openTimeout); this.openTimer.cancel();
this.openTimeout = null; this.openTimer = null;
} }
if (this.closeTimeout !== null) { if (this.closeTimer !== null) {
clearTimeout(this.closeTimeout); this.closeTimer.cancel();
this.closeTimeout = null; this.closeTimer = null;
} }
} }
private get openTimerKey() {
return `soma:tooltip:${this.opts.id.current}:open`;
}
private get closeTimerKey() {
return `soma:tooltip:${this.opts.id.current}:close`;
}
setContentRef(el: HTMLElement | null) { setContentRef(el: HTMLElement | null) {
this.contentRef.current = el; this.contentRef.current = el;
} }

Loading…
Cancel
Save

Powered by TurnKey Linux.