Route Soma presence scheduling through ActiveDom

active-uix
dev 5 months ago
parent 986b86aa1d
commit 45c607df90

@ -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;

@ -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)

@ -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
});

@ -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: {

@ -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

@ -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
});

@ -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)

@ -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
});

@ -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)

@ -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)
});

@ -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) => {

@ -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)

@ -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<TransitionStatus>(undefined);
private runId = 0;
private frameIds = new Set<number>();
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();
}
}

@ -1,3 +1,5 @@
import type { ActiveDom } from '$adom';
type Getter<T> = () => T;
/**
@ -5,20 +7,20 @@ type Getter<T> = () => T;
* Debounces via requestAnimationFrame.
*/
export class ResizeObserver$ {
constructor(node: Getter<HTMLElement | null>, onResize: () => void) {
constructor(dom: ActiveDom, node: Getter<HTMLElement | null>, 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);
};
});

Loading…
Cancel
Save

Powered by TurnKey Linux.