diff --git a/src/uix/soma/components/accordion/accordion-provider.svelte.ts b/src/uix/soma/components/accordion/accordion-provider.svelte.ts index e99404099..8b44fc01d 100644 --- a/src/uix/soma/components/accordion/accordion-provider.svelte.ts +++ b/src/uix/soma/components/accordion/accordion-provider.svelte.ts @@ -195,11 +195,13 @@ export class AccordionItemProvider { }); this.contentPresence = new Presence({ + dom: this.provider.soma.dom, open: readableActive(() => this.isOpen), ref: this.contentRef }); new ResizeObserver$( + this.provider.soma.dom, () => this.contentRef.current, () => { const el = this.contentRef.current; diff --git a/src/uix/soma/components/combobox/combobox-provider.svelte.ts b/src/uix/soma/components/combobox/combobox-provider.svelte.ts index e284c4efb..081f2e7fc 100644 --- a/src/uix/soma/components/combobox/combobox-provider.svelte.ts +++ b/src/uix/soma/components/combobox/combobox-provider.svelte.ts @@ -111,6 +111,7 @@ export class ComboboxProvider { this.floatingProvider = FloatingProvider.create({ dom: this.soma.dom }); this.contentPresence = new Presence({ + dom: this.soma.dom, open: opts.open, ref: this.contentRef, onComplete: (open) => opts.onOpenChangeComplete.current(open) diff --git a/src/uix/soma/components/context-menu/context-menu-provider.svelte.ts b/src/uix/soma/components/context-menu/context-menu-provider.svelte.ts index 4631e4686..1feed6bca 100644 --- a/src/uix/soma/components/context-menu/context-menu-provider.svelte.ts +++ b/src/uix/soma/components/context-menu/context-menu-provider.svelte.ts @@ -102,6 +102,7 @@ export class ContextMenuProvider { this.floatingProvider = FloatingProvider.create({ dom: this.soma.dom }); this.contentPresence = new Presence({ + dom: this.soma.dom, open: opts.open, ref: this.contentRef, onComplete: (open) => opts.onOpenChangeComplete.current(open) @@ -781,6 +782,7 @@ export class ContextMenuSubProvider { this.floatingProvider = FloatingProvider.create({ dom: this.provider.soma.dom }); this.contentPresence = new Presence({ + dom: this.provider.soma.dom, open: opts.open, ref: this.contentRef }); diff --git a/src/uix/soma/components/dialog/dialog-provider.svelte.ts b/src/uix/soma/components/dialog/dialog-provider.svelte.ts index 079a78fab..f2b98e04b 100644 --- a/src/uix/soma/components/dialog/dialog-provider.svelte.ts +++ b/src/uix/soma/components/dialog/dialog-provider.svelte.ts @@ -94,6 +94,7 @@ export class DialogProvider { const parent = DialogProvider.get() as DialogProvider | undefined; this.opts = opts; DialogProvider.ctx.set(this); + this.soma = Soma.require(); this.parent = parent; this.depth = parent ? parent.depth + 1 : 0; @@ -113,12 +114,14 @@ export class DialogProvider { } this.contentPresence = new Presence({ + dom: this.soma.dom, open: opts.open, ref: this.contentRef, onComplete: (open) => opts.onOpenChangeComplete.current(open) }); this.overlayPresence = new Presence({ + dom: this.soma.dom, open: opts.open, ref: this.overlayRef }); @@ -129,7 +132,6 @@ export class DialogProvider { const onOpen = () => this.handleOpen(); const onClose = () => this.handleClose(); - this.soma = Soma.require(); this.runtime = this.soma.runtime(dialogMorfo, { states: { open: () => opts.open.current }, props: { diff --git a/src/uix/soma/components/drawer/drawer-provider.svelte.ts b/src/uix/soma/components/drawer/drawer-provider.svelte.ts index a6dd31d02..1be7c1476 100644 --- a/src/uix/soma/components/drawer/drawer-provider.svelte.ts +++ b/src/uix/soma/components/drawer/drawer-provider.svelte.ts @@ -160,6 +160,7 @@ export class DrawerProvider { const parent = DrawerProvider.get() as DrawerProvider | undefined; this.opts = opts; DrawerProvider.ctx.set(this); + this.soma = Soma.require(); this.parent = parent; this.depth = parent ? parent.depth + 1 : 0; @@ -178,12 +179,14 @@ export class DrawerProvider { } this.contentPresence = new Presence({ + dom: this.soma.dom, open: opts.open, ref: this.contentRef, onComplete: (open) => opts.onOpenChangeComplete.current(open) }); this.overlayPresence = new Presence({ + dom: this.soma.dom, open: opts.open, ref: this.overlayRef }); @@ -196,7 +199,6 @@ export class DrawerProvider { // `data-dragging` on Content props. const noop = () => {}; - this.soma = Soma.require(); this.runtime = this.soma.runtime(drawerMorfo, { // 'persistent' forces open=true so eidos can render the layout // drawer without the consumer needing to bind `open`. The morfo diff --git a/src/uix/soma/components/dropdown-menu/dropdown-menu-provider.svelte.ts b/src/uix/soma/components/dropdown-menu/dropdown-menu-provider.svelte.ts index 0732e5a8f..c0a758d58 100644 --- a/src/uix/soma/components/dropdown-menu/dropdown-menu-provider.svelte.ts +++ b/src/uix/soma/components/dropdown-menu/dropdown-menu-provider.svelte.ts @@ -100,6 +100,7 @@ export class MenuProvider { this.floatingProvider = FloatingProvider.create({ dom: this.soma.dom }); this.contentPresence = new Presence({ + dom: this.soma.dom, open: opts.open, ref: this.contentRef, onComplete: (open) => opts.onOpenChangeComplete.current(open) @@ -857,6 +858,7 @@ export class MenuSubProvider { this.floatingProvider = FloatingProvider.create({ dom: this.provider.soma.dom }); this.contentPresence = new Presence({ + dom: this.provider.soma.dom, open: opts.open, ref: this.contentRef }); 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 f405c3462..afa2937a1 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 @@ -77,6 +77,7 @@ export class LinkPreviewProvider { this.floatingProvider = FloatingProvider.create({ dom: this.soma.dom }); this.contentPresence = new Presence({ + dom: this.soma.dom, open: opts.open, ref: this.contentRef, onComplete: (open) => opts.onOpenChangeComplete.current(open) diff --git a/src/uix/soma/components/popover/popover-provider.svelte.ts b/src/uix/soma/components/popover/popover-provider.svelte.ts index 163e5e9da..09d214830 100644 --- a/src/uix/soma/components/popover/popover-provider.svelte.ts +++ b/src/uix/soma/components/popover/popover-provider.svelte.ts @@ -99,12 +99,14 @@ export class PopoverProvider { this.floatingProvider = FloatingProvider.create({ dom: this.soma.dom }); this.contentPresence = new Presence({ + dom: this.soma.dom, open: opts.open, ref: this.contentRef, onComplete: (open) => opts.onOpenChangeComplete.current(open) }); this.overlayPresence = new Presence({ + dom: this.soma.dom, open: opts.open, ref: this.overlayRef }); diff --git a/src/uix/soma/components/select/select-provider.svelte.ts b/src/uix/soma/components/select/select-provider.svelte.ts index b9d902ac3..fa9f0e62c 100644 --- a/src/uix/soma/components/select/select-provider.svelte.ts +++ b/src/uix/soma/components/select/select-provider.svelte.ts @@ -121,6 +121,7 @@ export class SelectProvider { this.floatingProvider = FloatingProvider.create({ dom: this.soma.dom }); this.contentPresence = new Presence({ + dom: this.soma.dom, open: opts.open, ref: this.contentRef, onComplete: (open) => opts.onOpenChangeComplete.current(open) diff --git a/src/uix/soma/components/tabs/tabs-provider.svelte.ts b/src/uix/soma/components/tabs/tabs-provider.svelte.ts index 12c06c20d..19230c972 100644 --- a/src/uix/soma/components/tabs/tabs-provider.svelte.ts +++ b/src/uix/soma/components/tabs/tabs-provider.svelte.ts @@ -1,11 +1,7 @@ import { untrack } from 'svelte'; import { createAttrs } from '$uix/morfo'; import { context, type WithRefOpts } from '../../provider'; -import { - readableActive, - type ActiveProps, - type StateProps -} from '$libs/reactive'; +import { readableActive, type ActiveProps, type StateProps } from '$libs/reactive'; import type { OnChangeFn, Orientation, @@ -374,6 +370,7 @@ export class TabsContentProvider { }); this.presence = new Presence({ + dom: this.provider.soma.dom, open: readableActive(() => this.isActive), ref: readableActive(() => this.opts.ref.current) }); diff --git a/src/uix/soma/components/toast/toast-provider.svelte.ts b/src/uix/soma/components/toast/toast-provider.svelte.ts index 0a210a961..b24ecb551 100644 --- a/src/uix/soma/components/toast/toast-provider.svelte.ts +++ b/src/uix/soma/components/toast/toast-provider.svelte.ts @@ -289,6 +289,7 @@ export class ToastItemProvider { // Presence for exit animation this.presence = new Presence({ + dom: this.provider.soma.dom, open: readableActive(() => this.isOpen), ref: opts.ref, onComplete: (open) => { diff --git a/src/uix/soma/components/tooltip/tooltip-provider.svelte.ts b/src/uix/soma/components/tooltip/tooltip-provider.svelte.ts index abe69c6c9..a256d4fa3 100644 --- a/src/uix/soma/components/tooltip/tooltip-provider.svelte.ts +++ b/src/uix/soma/components/tooltip/tooltip-provider.svelte.ts @@ -152,6 +152,7 @@ export class TooltipProvider { this.floatingProvider = FloatingProvider.create({ dom: this.soma.dom }); this.contentPresence = new Presence({ + dom: this.soma.dom, open: opts.open, ref: this.contentRef, onComplete: (open) => opts.onOpenChangeComplete.current(open) diff --git a/src/uix/soma/layers/presence.svelte.ts b/src/uix/soma/layers/presence.svelte.ts index 60adcf47a..17e3d8391 100644 --- a/src/uix/soma/layers/presence.svelte.ts +++ b/src/uix/soma/layers/presence.svelte.ts @@ -1,12 +1,11 @@ -import { - type Active, - type ActiveProps -} from '$libs/reactive'; +import { type Active, type ActiveProps } from '$libs/reactive'; import { watch } from 'runed'; +import type { ActiveDom } from '$adom'; // ── Types ──────────────────────────────────────────────────────────────────── export interface PresenceOptions extends ActiveProps<{ open: boolean; ref: HTMLElement | null }> { + dom: ActiveDom; /** Called after open/close animation completes. Receives the current open state. */ onComplete?: (open: boolean) => void; /** When false, skips animation waiting — unmounts immediately on close. @default true */ @@ -39,6 +38,7 @@ export class Presence { transitionStatus = $state(undefined); private runId = 0; + private frameIds = new Set(); constructor(opts: PresenceOptions) { this.opts = opts; @@ -76,7 +76,7 @@ export class Presence { const runId = ++this.runId; // Remove data-starting-style on next frame to trigger CSS transition - requestAnimationFrame(() => { + this.requestFrame(() => { if (runId !== this.runId) return; this.transitionStatus = undefined; @@ -117,7 +117,7 @@ export class Presence { private waitForAnimations(runId: number, onDone: () => void) { // getAnimations() needs a frame to see the active animations - requestAnimationFrame(() => { + this.requestFrame(() => { if (runId !== this.runId) return; const node = this.opts.ref.current; @@ -145,7 +145,19 @@ export class Presence { // ── Cleanup ────────────────────────────────────────────────────────────── + private requestFrame(callback: () => void) { + const frame = this.opts.dom.requestFrame(() => { + this.frameIds.delete(frame); + callback(); + }, this.opts.ref.current); + this.frameIds.add(frame); + } + private cleanup() { this.runId++; + for (const frame of this.frameIds) { + this.opts.dom.cancelFrame(frame, this.opts.ref.current); + } + this.frameIds.clear(); } } diff --git a/src/uix/soma/layers/resize-observer.svelte.ts b/src/uix/soma/layers/resize-observer.svelte.ts index 81c748db5..b56edcb47 100644 --- a/src/uix/soma/layers/resize-observer.svelte.ts +++ b/src/uix/soma/layers/resize-observer.svelte.ts @@ -1,3 +1,5 @@ +import type { ActiveDom } from '$adom'; + type Getter = () => T; /** @@ -5,20 +7,20 @@ type Getter = () => T; * Debounces via requestAnimationFrame. */ export class ResizeObserver$ { - constructor(node: Getter, onResize: () => void) { + constructor(dom: ActiveDom, node: Getter, onResize: () => void) { $effect(() => { let rAF = 0; const el = node(); if (!el) return; const observer = new ResizeObserver(() => { - cancelAnimationFrame(rAF); - rAF = requestAnimationFrame(onResize); + dom.cancelFrame(rAF, el); + rAF = dom.requestFrame(onResize, el); }); observer.observe(el); return () => { - cancelAnimationFrame(rAF); + dom.cancelFrame(rAF, el); observer.unobserve(el); }; });