From 986b86aa1d77a0296800aa4108a17fa5c018e15e Mon Sep 17 00:00:00 2001 From: dev Date: Fri, 15 May 2026 17:31:25 +0200 Subject: [PATCH] Use ActiveDom frame scheduling in Soma --- .../combobox/combobox-provider.svelte.ts | 12 +++++------ .../drawer/drawer-provider.svelte.ts | 16 +++++++-------- .../editable/editable-provider.svelte.ts | 16 +++++++++------ .../scroll-area-provider.svelte.ts | 9 ++++++--- .../select/select-provider.svelte.ts | 8 ++++---- .../components/toast/toast-provider.svelte.ts | 4 ++-- .../soma/layers/floating/floating.svelte.ts | 20 ++++++++----------- 7 files changed, 44 insertions(+), 41 deletions(-) diff --git a/src/uix/soma/components/combobox/combobox-provider.svelte.ts b/src/uix/soma/components/combobox/combobox-provider.svelte.ts index 60b6101f4..e284c4efb 100644 --- a/src/uix/soma/components/combobox/combobox-provider.svelte.ts +++ b/src/uix/soma/components/combobox/combobox-provider.svelte.ts @@ -228,31 +228,31 @@ export class ComboboxInputProvider { this.provider.handleOpen(); } // Focus first item - requestAnimationFrame(() => { + this.provider.soma.dom.requestFrame(() => { const container = this.provider.contentRef.current; if (!container) return; const items = this.provider.getItems(container); if (items.length > 0) { - items[0].focus(); + this.provider.soma.dom.focus(items[0]); this.provider.highlightedId = items[0].id; } - }); + }, this.provider.contentRef.current ?? this.provider.inputRef.current); } else if (e.key === KEYS.ARROW_UP) { e.preventDefault(); if (!this.provider.opts.open.current) { this.provider.handleOpen(); } // Focus last item - requestAnimationFrame(() => { + this.provider.soma.dom.requestFrame(() => { const container = this.provider.contentRef.current; if (!container) return; const items = this.provider.getItems(container); if (items.length > 0) { const last = items[items.length - 1]; - last.focus(); + this.provider.soma.dom.focus(last); this.provider.highlightedId = last.id; } - }); + }, this.provider.contentRef.current ?? this.provider.inputRef.current); } else if (e.key === KEYS.ENTER) { if (this.provider.opts.open.current && this.provider.highlightedId) { e.preventDefault(); diff --git a/src/uix/soma/components/drawer/drawer-provider.svelte.ts b/src/uix/soma/components/drawer/drawer-provider.svelte.ts index 19f132b89..a6dd31d02 100644 --- a/src/uix/soma/components/drawer/drawer-provider.svelte.ts +++ b/src/uix/soma/components/drawer/drawer-provider.svelte.ts @@ -569,7 +569,7 @@ export class DrawerContentProvider { }; const queueDragProgress = () => { if (dragProgressFrame !== null) return; - dragProgressFrame = requestAnimationFrame(flushDragProgress); + dragProgressFrame = this.provider.soma.dom.requestFrame(flushDragProgress, opts.ref.current); }; let axialDragProgressFrame: number | null = null; const beginDrag = (e: PointerEvent, axisOffsetToward = 0) => { @@ -588,11 +588,11 @@ export class DrawerContentProvider { const releaseDrag = () => { if (!this.wasDragging) return; if (dragProgressFrame !== null) { - cancelAnimationFrame(dragProgressFrame); + this.provider.soma.dom.cancelFrame(dragProgressFrame, opts.ref.current); dragProgressFrame = null; } if (axialDragProgressFrame !== null) { - cancelAnimationFrame(axialDragProgressFrame); + this.provider.soma.dom.cancelFrame(axialDragProgressFrame, opts.ref.current); axialDragProgressFrame = null; } this.wasDragging = false; @@ -760,13 +760,13 @@ export class DrawerContentProvider { if (captured) return; const el = opts.ref.current; if (!el) return; - const id = requestAnimationFrame(() => { + const id = this.provider.soma.dom.requestFrame(() => { const rect = el.getBoundingClientRect(); const axis = sideToAxis(this.provider.resolvedSide); this.panelMaxSize = axis === 'x' ? rect.width : rect.height; captured = true; - }); - return () => cancelAnimationFrame(id); + }, el); + return () => this.provider.soma.dom.cancelFrame(id, el); }); // ── AxialDrag (resize-on-drag, free or snapped) ────────────────────── @@ -777,7 +777,7 @@ export class DrawerContentProvider { const axialReleaseDrag = () => releaseDrag(); const queueAxialProgress = () => { if (axialDragProgressFrame !== null) return; - axialDragProgressFrame = requestAnimationFrame(() => { + axialDragProgressFrame = this.provider.soma.dom.requestFrame(() => { axialDragProgressFrame = null; if (!this.axial.isDragging) return; void this.provider.runtime.trigger('drag-progress', { @@ -787,7 +787,7 @@ export class DrawerContentProvider { this.axial.offsetPx ) }); - }); + }, opts.ref.current); }; this.axial = new AxialDrag({ diff --git a/src/uix/soma/components/editable/editable-provider.svelte.ts b/src/uix/soma/components/editable/editable-provider.svelte.ts index 53a484904..b72cfeb65 100644 --- a/src/uix/soma/components/editable/editable-provider.svelte.ts +++ b/src/uix/soma/components/editable/editable-provider.svelte.ts @@ -12,7 +12,6 @@ import { editableMorfo } from '../../../morfo/components/editable'; const attrs = createAttrs(editableMorfo); import type { SomaRuntime, SomaRuntimePart } from '../../runtime.svelte'; - export type ActivationMode = 'focus' | 'click' | 'dblclick' | 'none'; export type SubmitMode = 'enter' | 'blur' | 'both' | 'none'; @@ -77,7 +76,10 @@ export class EditableProvider { $effect(() => { return () => { if (this.pendingFrame !== null) { - cancelAnimationFrame(this.pendingFrame); + this.soma.dom.cancelFrame( + this.pendingFrame, + this.inputRef.current ?? this.opts.ref.current + ); this.pendingFrame = null; } }; @@ -100,16 +102,18 @@ export class EditableProvider { this.opts.editing.current = true; // Focus input on next tick - if (this.pendingFrame !== null) cancelAnimationFrame(this.pendingFrame); - this.pendingFrame = requestAnimationFrame(() => { + if (this.pendingFrame !== null) { + this.soma.dom.cancelFrame(this.pendingFrame, this.inputRef.current ?? this.opts.ref.current); + } + this.pendingFrame = this.soma.dom.requestFrame(() => { this.pendingFrame = null; const input = this.inputRef.current; if (!input) return; - input.focus(); + this.soma.dom.focus(input); if (this.opts.selectOnFocus.current && 'select' in input) { (input as HTMLInputElement).select(); } - }); + }, this.inputRef.current ?? this.opts.ref.current); } submit() { diff --git a/src/uix/soma/components/scroll-area/scroll-area-provider.svelte.ts b/src/uix/soma/components/scroll-area/scroll-area-provider.svelte.ts index de65e4296..0a14b8c77 100644 --- a/src/uix/soma/components/scroll-area/scroll-area-provider.svelte.ts +++ b/src/uix/soma/components/scroll-area/scroll-area-provider.svelte.ts @@ -248,13 +248,16 @@ export class ScrollAreaScrollbarProvider { }); // Skip initial-mount flash — defer mounted flag to next frame - requestAnimationFrame(() => { + const mountedFrame = this.provider.soma.dom.requestFrame(() => { this.mounted = true; - }); + }, opts.ref.current); // Cleanup on unmount $effect(() => { - return () => this.clearHideTimer(); + return () => { + this.provider.soma.dom.cancelFrame(mountedFrame, opts.ref.current); + this.clearHideTimer(); + }; }); // Reveal the scrollbar on parent scroll for 'scroll' and 'hover' types. diff --git a/src/uix/soma/components/select/select-provider.svelte.ts b/src/uix/soma/components/select/select-provider.svelte.ts index 775b746b1..b9d902ac3 100644 --- a/src/uix/soma/components/select/select-provider.svelte.ts +++ b/src/uix/soma/components/select/select-provider.svelte.ts @@ -165,11 +165,11 @@ export class SelectProvider { this.opts.open.current = true; // Scroll selected item into view after content mounts - requestAnimationFrame(() => { - requestAnimationFrame(() => { + this.soma.dom.requestFrame(() => { + this.soma.dom.requestFrame(() => { this.scrollSelectedIntoView(); - }); - }); + }, this.contentRef.current ?? this.triggerRef.current); + }, this.triggerRef.current); } handleClose() { diff --git a/src/uix/soma/components/toast/toast-provider.svelte.ts b/src/uix/soma/components/toast/toast-provider.svelte.ts index 3762091af..0a210a961 100644 --- a/src/uix/soma/components/toast/toast-provider.svelte.ts +++ b/src/uix/soma/components/toast/toast-provider.svelte.ts @@ -462,9 +462,9 @@ export class ToastItemProvider { this.swipeDeltaX = 0; this.swipeDeltaY = 0; // Reset to idle so subsequent swipes work correctly - requestAnimationFrame(() => { + this.provider.soma.dom.requestFrame(() => { this.swipeState = 'idle'; - }); + }, this.opts.ref.current); } }; diff --git a/src/uix/soma/layers/floating/floating.svelte.ts b/src/uix/soma/layers/floating/floating.svelte.ts index a875689e4..c1ad46d35 100644 --- a/src/uix/soma/layers/floating/floating.svelte.ts +++ b/src/uix/soma/layers/floating/floating.svelte.ts @@ -12,13 +12,8 @@ import { } from '@floating-ui/dom'; import { attachRef, type RefAttachment } from '$libs/reactive'; import { cssToStyleObj, styleToString } from '$soma/css'; -import { getWindow, type ActiveDom } from '$adom'; -import { - state, - readableActive, - type Active, - type ActiveProps -} from '$libs/reactive'; +import type { ActiveDom } from '$adom'; +import { state, readableActive, type Active, type ActiveProps } from '$libs/reactive'; import { isNotNull } from '$soma/types'; import { useId } from '$soma/id'; import { context, type SomaContext } from '$soma/provider'; @@ -299,17 +294,18 @@ export class FloatingContent { () => this.contentRef.current, (contentNode) => { if (!contentNode || !this.opts.enabled.current) return; - const win = getWindow(contentNode); - const rafId = win.requestAnimationFrame(() => { + const rafId = this.provider.opts.dom.requestFrame(() => { if (this.contentRef.current !== contentNode || !this.opts.enabled.current) return; - const zIndex = win.getComputedStyle(contentNode).zIndex; + const zIndex = this.provider.opts.dom + .getWindow(contentNode) + .getComputedStyle(contentNode).zIndex; if (zIndex !== this.contentZIndex) { this.contentZIndex = zIndex; } - }); + }, contentNode); return () => { - win.cancelAnimationFrame(rafId); + this.provider.opts.dom.cancelFrame(rafId, contentNode); }; } );