From 9a43cce6974aa7b65fd099fe3b81f64117e92123 Mon Sep 17 00:00:00 2001 From: dev Date: Fri, 15 May 2026 13:14:54 +0200 Subject: [PATCH] Route Soma hover delays through timers --- .../link-preview-provider.svelte.ts | 68 ++++++++++++------- .../tooltip/tooltip-provider.svelte.ts | 68 +++++++++++++------ 2 files changed, 89 insertions(+), 47 deletions(-) diff --git a/src/uix/soma/components/link-preview/link-preview-provider.svelte.ts b/src/uix/soma/components/link-preview/link-preview-provider.svelte.ts index 068184fa1..bfec4d4e2 100644 --- a/src/uix/soma/components/link-preview/link-preview-provider.svelte.ts +++ b/src/uix/soma/components/link-preview/link-preview-provider.svelte.ts @@ -20,7 +20,7 @@ import { import { linkPreviewMorfo } from '../../../morfo/components/link-preview'; import { Soma } from '../../core/soma.svelte'; import type { SomaRuntime, SomaRuntimePart } from '../../runtime.svelte'; - +import type { TimerHandle } from '$timer'; // ── Root ───────────────────────────────────────────────────────────────────── @@ -55,8 +55,8 @@ export class LinkPreviewProvider { readonly floatingProvider: FloatingProvider; readonly contentPresence: Presence; - private openTimeout: ReturnType | null = null; - private closeTimeout: ReturnType | null = null; + private openTimer: TimerHandle | null = null; + private closeTimer: TimerHandle | null = null; contentId = state(''); triggerId = state(''); @@ -94,57 +94,75 @@ export class LinkPreviewProvider { }); $effect(() => { - return () => this.clearTimeouts(); + return () => this.clearTimers(); }); } handleOpen() { if (this.opts.disabled.current) return; - this.clearTimeouts(); - this.openTimeout = setTimeout(() => { - this.openTimeout = null; - this.opts.open.current = true; - }, this.opts.openDelay.current); + this.clearTimers(); + this.openTimer = this.soma.uix.timers.schedule( + this.openTimerKey, + 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). */ handleClose(bypass = false) { - this.clearTimeouts(); + this.clearTimers(); if (bypass || this.opts.closeDelay.current <= 0) { this.opts.open.current = false; return; } - this.closeTimeout = setTimeout(() => { - this.closeTimeout = null; - this.opts.open.current = false; - }, this.opts.closeDelay.current); + this.closeTimer = this.soma.uix.timers.schedule( + this.closeTimerKey, + 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). */ cancelClose() { - if (this.closeTimeout !== null) { - clearTimeout(this.closeTimeout); - this.closeTimeout = null; + if (this.closeTimer !== null) { + this.closeTimer.cancel(); + this.closeTimer = null; } } /** Immediate close bypassing delays (Escape, outside click). */ forceClose() { - this.clearTimeouts(); + this.clearTimers(); this.opts.open.current = false; } - private clearTimeouts() { - if (this.openTimeout !== null) { - clearTimeout(this.openTimeout); - this.openTimeout = null; + private clearTimers() { + if (this.openTimer !== null) { + this.openTimer.cancel(); + this.openTimer = null; } - if (this.closeTimeout !== null) { - clearTimeout(this.closeTimeout); - this.closeTimeout = null; + if (this.closeTimer !== null) { + this.closeTimer.cancel(); + 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) { this.contentRef.current = el; } diff --git a/src/uix/soma/components/tooltip/tooltip-provider.svelte.ts b/src/uix/soma/components/tooltip/tooltip-provider.svelte.ts index 49692f552..d59611859 100644 --- a/src/uix/soma/components/tooltip/tooltip-provider.svelte.ts +++ b/src/uix/soma/components/tooltip/tooltip-provider.svelte.ts @@ -31,7 +31,7 @@ import { import { tooltipMorfo } from '../../../morfo/components/tooltip'; import { Soma } from '../../core/soma.svelte'; import type { SomaRuntime, SomaRuntimePart } from '../../runtime.svelte'; - +import type { TimerHandle } from '$timer'; export type TooltipState = 'closed' | 'delayed-open' | 'instant-open'; @@ -112,8 +112,8 @@ export class TooltipProvider { readonly contentPresence: Presence; private internalTooltipState = $state('closed'); - private openTimeout: ReturnType | null = null; - private closeTimeout: ReturnType | null = null; + private openTimer: TimerHandle | null = null; + private closeTimer: TimerHandle | null = null; private contentRef = state(null); /** @@ -167,6 +167,10 @@ export class TooltipProvider { buffer: 2, transitIntentTimeout: 200 }); + + $effect(() => { + return () => this.clearTimers(); + }); } /** Effective delay (per-tooltip override → group default). */ @@ -190,28 +194,40 @@ export class TooltipProvider { handleOpen(instant = false) { if (this.isDisabled) return; - this.clearTimeouts(); + this.clearTimers(); if (instant || this.group?.isWithinSkipDelay()) { this.internalTooltipState = 'instant-open'; this.opts.open.current = true; } else { this.internalTooltipState = 'delayed-open'; - this.openTimeout = setTimeout(() => { - this.internalTooltipState = 'instant-open'; - this.opts.open.current = true; - }, this.delay); + this.openTimer = this.soma.uix.timers.schedule( + this.openTimerKey, + this.delay, + () => { + this.openTimer = null; + this.internalTooltipState = 'instant-open'; + this.opts.open.current = true; + }, + { replace: true, meta: { component: 'tooltip', action: 'open-delay' } } + ); } } handleClose() { - this.clearTimeouts(); + this.clearTimers(); const closeDelay = this.opts.closeDelay.current; if (closeDelay > 0) { - this.closeTimeout = setTimeout(() => { - this.doClose(); - }, closeDelay); + this.closeTimer = this.soma.uix.timers.schedule( + this.closeTimerKey, + closeDelay, + () => { + this.closeTimer = null; + this.doClose(); + }, + { replace: true, meta: { component: 'tooltip', action: 'close-delay' } } + ); } else { this.doClose(); } @@ -225,23 +241,31 @@ export class TooltipProvider { /** Cancel pending close (when pointer enters content). */ cancelClose() { - if (this.closeTimeout !== null) { - clearTimeout(this.closeTimeout); - this.closeTimeout = null; + if (this.closeTimer !== null) { + this.closeTimer.cancel(); + this.closeTimer = null; } } - private clearTimeouts() { - if (this.openTimeout !== null) { - clearTimeout(this.openTimeout); - this.openTimeout = null; + private clearTimers() { + if (this.openTimer !== null) { + this.openTimer.cancel(); + this.openTimer = null; } - if (this.closeTimeout !== null) { - clearTimeout(this.closeTimeout); - this.closeTimeout = null; + if (this.closeTimer !== null) { + this.closeTimer.cancel(); + 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) { this.contentRef.current = el; }