Consolidate Soma interaction runtimes

active-uix
dev 5 months ago
parent c7527fc107
commit c499dd239a

@ -10,7 +10,7 @@ import { DRAG_DROP_LANGS } from './langs';
import type { ActiveDrag, DragData, DragEndEvent, DropEvent } from './types';
import { dragDropMorfo } from '../../../morfo/components/drag-drop';
import type { SomaRuntimePart } from '../../runtime.svelte';
import type { SomaRuntime, SomaRuntimePart } from '../../runtime.svelte';
const attrs = createAttrs(dragDropMorfo);
@ -31,6 +31,7 @@ interface DragDropOpts
export class DragDropProvider {
readonly opts: DragDropOpts;
readonly runtimePart: SomaRuntimePart;
readonly runtime: SomaRuntime;
static readonly ctx = context<DragDropProvider>('DragDrop');
static get(): DragDropProvider | undefined {
return this.ctx.getOr(undefined) as DragDropProvider | undefined;
@ -60,9 +61,13 @@ export class DragDropProvider {
private constructor(opts: DragDropOpts) {
this.opts = opts;
this.globalAnnounce = createAnnouncer(this.soma.dom, 1500);
this.runtimePart = this.soma
.runtime(dragDropMorfo, {})
.part('provider', { id: opts.id, ref: opts.ref, owner: this, context: DragDropProvider.ctx });
this.runtime = this.soma.runtime(dragDropMorfo, {});
this.runtimePart = this.runtime.part('provider', {
id: opts.id,
ref: opts.ref,
owner: this,
context: DragDropProvider.ctx
});
$effect(() => {
return () => this.teardown();
});
@ -72,8 +77,7 @@ export class DragDropProvider {
private announce(key: string, priority: 'polite' | 'assertive' = 'polite') {
if (!this.opts.announceEnabled.current) return;
const ling = this.soma?.langs;
const text = ling ? ling.ts(key) : key;
const text = this.soma.langs.ts(key);
this.globalAnnounce.announce(text, priority);
}
@ -83,8 +87,7 @@ export class DragDropProvider {
priority: 'polite' | 'assertive' = 'polite'
) {
if (!this.opts.announceEnabled.current) return;
const ling = this.soma?.langs;
const text = ling ? ling.t(key, vars) : key;
const text = this.soma.langs.t(key, vars);
this.globalAnnounce.announce(text, priority);
}
@ -375,10 +378,12 @@ export class DraggableProvider {
private constructor(opts: DraggableOpts) {
this.opts = opts;
this.runtimePart = Soma.require()
.runtime(dragDropMorfo, {})
.part('draggable', { id: opts.id, ref: opts.ref, owner: this });
this.provider = DragDropProvider.require();
this.runtimePart = this.provider.runtime.part('draggable', {
id: opts.id,
ref: opts.ref,
owner: this
});
}
readonly isActive = $derived.by(() => this.provider.active?.value === this.opts.value.current);
@ -467,7 +472,7 @@ export class DraggableProvider {
role: 'button' as const,
tabindex: this.opts.disabled.current ? -1 : 0,
'aria-disabled': this.opts.disabled.current ? true : undefined,
'aria-roledescription': this.provider.soma?.langs.ts(DRAG_DROP_LANGS.DRAGGABLE_ROLE),
'aria-roledescription': this.provider.soma.langs.ts(DRAG_DROP_LANGS.DRAGGABLE_ROLE),
'aria-grabbed': this.isActive ? true : undefined,
'data-dragging': boolToEmptyStrOrUndef(this.isActive),
'data-disabled': boolToEmptyStrOrUndef(this.opts.disabled.current),
@ -502,10 +507,12 @@ export class DroppableProvider {
private constructor(opts: DroppableOpts) {
this.opts = opts;
this.runtimePart = Soma.require()
.runtime(dragDropMorfo, {})
.part('droppable', { id: opts.id, ref: opts.ref, owner: this });
this.provider = DragDropProvider.require();
this.runtimePart = this.provider.runtime.part('droppable', {
id: opts.id,
ref: opts.ref,
owner: this
});
// Attach the accept predicate directly to the DOM element so the
// Provider can read it without a registry (which would require reactive
@ -556,7 +563,7 @@ export class DroppableProvider {
...this.runtimePart.props,
role: 'region' as const,
tabindex: this.provider.active && this.isAccepting ? 0 : -1,
'aria-roledescription': this.provider.soma?.langs.ts(DRAG_DROP_LANGS.DROPPABLE_ROLE),
'aria-roledescription': this.provider.soma.langs.ts(DRAG_DROP_LANGS.DROPPABLE_ROLE),
'aria-dropeffect':
this.provider.active && this.isAccepting ? ('move' as const) : ('none' as const),
'aria-disabled': this.opts.disabled.current ? true : undefined,
@ -584,10 +591,12 @@ export class DragPreviewProvider {
private constructor(opts: DragPreviewOpts) {
this.opts = opts;
this.runtimePart = Soma.require()
.runtime(dragDropMorfo, {})
.part('preview', { id: opts.id, ref: opts.ref, owner: this });
this.provider = DragDropProvider.require();
this.runtimePart = this.provider.runtime.part('preview', {
id: opts.id,
ref: opts.ref,
owner: this
});
}
readonly active: Active<typeof this.provider.active> = readableActive(() => this.provider.active);

@ -16,7 +16,7 @@ import { LISTBOX_LANGS } from './langs';
import type { ListboxSelectionMode } from './types';
import { listboxMorfo } from '../../../morfo/components/listbox';
import type { SomaRuntimePart } from '../../runtime.svelte';
import type { SomaRuntime, SomaRuntimePart } from '../../runtime.svelte';
const attrs = createAttrs(listboxMorfo);
@ -46,6 +46,8 @@ interface ListboxOpts
export class ListboxProvider {
readonly opts: ListboxOpts;
readonly runtimePart: SomaRuntimePart;
readonly soma: Soma;
readonly runtime: SomaRuntime;
static readonly ctx = context<ListboxProvider>('Listbox');
static get(): ListboxProvider | undefined {
return this.ctx.getOr(undefined) as ListboxProvider | undefined;
@ -58,7 +60,6 @@ export class ListboxProvider {
return new ListboxProvider(opts);
}
readonly soma = Soma.get();
readonly field = FieldProvider.get();
focused = $state(false);
@ -67,9 +68,14 @@ export class ListboxProvider {
private constructor(opts: ListboxOpts) {
this.opts = opts;
this.runtimePart = Soma.require()
.runtime(listboxMorfo, {})
.part('provider', { id: opts.id, ref: opts.ref, owner: this, context: ListboxProvider.ctx });
this.soma = Soma.require();
this.runtime = this.soma.runtime(listboxMorfo, {});
this.runtimePart = this.runtime.part('provider', {
id: opts.id,
ref: opts.ref,
owner: this,
context: ListboxProvider.ctx
});
$effect(() => {
return () => {
@ -94,13 +100,13 @@ export class ListboxProvider {
);
readonly resolvedDir: Active<Direction> = readableActive(
() => this.opts.dir.current ?? this.soma?.prefs.getDir() ?? 'ltr'
() => this.opts.dir.current ?? this.soma.prefs.getDir() ?? 'ltr'
);
readonly resolvedAriaLabel: Active<string | undefined> = readableActive(() =>
this.opts.ariaLabelledby.current
? undefined
: this.opts.ariaLabel.current || this.soma?.langs.ts(LISTBOX_LANGS.LABEL) || undefined
: this.opts.ariaLabel.current || this.soma.langs.ts(LISTBOX_LANGS.LABEL) || undefined
);
readonly isEmpty = $derived.by(() => this.opts.value.current.length === 0);
@ -360,10 +366,13 @@ export class ListboxItemProvider {
private constructor(opts: ListboxItemOpts) {
this.opts = opts;
this.runtimePart = Soma.require()
.runtime(listboxMorfo, {})
.part('item', { id: opts.id, ref: opts.ref, owner: this, context: ListboxItemProvider.ctx });
this.provider = ListboxProvider.require();
this.runtimePart = this.provider.runtime.part('item', {
id: opts.id,
ref: opts.ref,
owner: this,
context: ListboxItemProvider.ctx
});
}
readonly isSelected = $derived.by(() => this.provider.isSelected(this.opts.value.current));
@ -446,11 +455,13 @@ export class ListboxItemIndicatorProvider {
private constructor(opts: ListboxItemIndicatorOpts) {
this.opts = opts;
this.runtimePart = Soma.require()
.runtime(listboxMorfo, {})
.part('item-indicator', { id: opts.id, ref: opts.ref, owner: this });
this.provider = ListboxProvider.require();
this.item = ListboxItemProvider.require();
this.runtimePart = this.provider.runtime.part('item-indicator', {
id: opts.id,
ref: opts.ref,
owner: this
});
}
readonly isPresent = $derived.by(() => this.item.isSelected);
@ -471,6 +482,7 @@ interface ListboxGroupOpts extends WithRefOpts {}
export class ListboxGroupProvider {
readonly opts: ListboxGroupOpts;
readonly runtimePart: SomaRuntimePart;
readonly provider: ListboxProvider;
static readonly ctx = context<ListboxGroupProvider>('ListboxGroup');
static get(): ListboxGroupProvider | undefined {
return this.ctx.getOr(undefined) as ListboxGroupProvider | undefined;
@ -487,9 +499,8 @@ export class ListboxGroupProvider {
private constructor(opts: ListboxGroupOpts) {
this.opts = opts;
this.runtimePart = Soma.require()
.runtime(listboxMorfo, {})
.part('group', {
this.provider = ListboxProvider.require();
this.runtimePart = this.provider.runtime.part('group', {
id: opts.id,
ref: opts.ref,
owner: this,
@ -519,10 +530,12 @@ export class ListboxGroupLabelProvider {
private constructor(opts: ListboxGroupLabelOpts) {
this.opts = opts;
this.runtimePart = Soma.require()
.runtime(listboxMorfo, {})
.part('group-label', { id: opts.id, ref: opts.ref, owner: this });
this.group = ListboxGroupProvider.require();
this.runtimePart = this.group.provider.runtime.part('group-label', {
id: opts.id,
ref: opts.ref,
owner: this
});
this.group.labelId = opts.id.current;
}

@ -43,7 +43,7 @@ function escapeRegex(s: string): string {
}
import { numberFieldMorfo } from '../../../morfo/components/number-field';
import type { SomaRuntimePart } from '../../runtime.svelte';
import type { SomaRuntime, SomaRuntimePart } from '../../runtime.svelte';
// ── Helpers ──────────────────────────────────────────────────────────────────
@ -81,6 +81,8 @@ interface NumberFieldOpts
export class NumberFieldProvider {
readonly opts: NumberFieldOpts;
readonly runtimePart: SomaRuntimePart;
readonly soma: Soma;
readonly runtime: SomaRuntime;
static readonly ctx = context<NumberFieldProvider>('NumberField');
static get(): NumberFieldProvider | undefined {
return this.ctx.getOr(undefined) as NumberFieldProvider | undefined;
@ -93,7 +95,6 @@ export class NumberFieldProvider {
return new NumberFieldProvider(opts);
}
readonly soma = Soma.get();
/**
* Optional parent Field context. When present, NumberField inherits
* `disabled`/`readonly`/`required`/`invalid` and its Input registers into
@ -116,20 +117,18 @@ export class NumberFieldProvider {
private constructor(opts: NumberFieldOpts) {
this.opts = opts;
this.runtimePart = Soma.require()
.runtime(numberFieldMorfo, {})
.part('provider', {
this.soma = Soma.require();
this.runtime = this.soma.runtime(numberFieldMorfo, {});
this.runtimePart = this.runtime.part('provider', {
id: opts.id,
ref: opts.ref,
owner: this,
context: NumberFieldProvider.ctx
});
this.locale = readableActive(() => opts.locale.current ?? this.soma?.langs.getLocale() ?? 'en');
this.locale = readableActive(() => opts.locale.current ?? this.soma.langs.getLocale() ?? 'en');
this.resolvedDir = readableActive(
() => opts.dir.current ?? this.soma?.prefs.getDir() ?? 'ltr'
);
this.resolvedDir = readableActive(() => opts.dir.current ?? this.soma.prefs.getDir() ?? 'ltr');
// Sync inputValue with external value/locale changes.
// When focused (user is typing), only update if the value itself changed — not on locale/format changes.
@ -179,12 +178,12 @@ export class NumberFieldProvider {
/** Decimal separator for the active locale (locale-aware, override-aware). */
readonly decimalSeparator = $derived.by(
() => this.soma?.nums?.getDecimalSeparator() ?? resolveDecimalSeparator(this.locale.current)
() => this.soma.nums?.getDecimalSeparator() ?? resolveDecimalSeparator(this.locale.current)
);
/** Group (thousands) separator for the active locale. */
readonly groupSeparator = $derived.by(
() => this.soma?.nums?.getGroupSeparator() ?? resolveGroupSeparator(this.locale.current)
() => this.soma.nums?.getGroupSeparator() ?? resolveGroupSeparator(this.locale.current)
);
/** Regex of characters accepted during typing. Rebuilt on locale/format change. */
@ -223,7 +222,7 @@ export class NumberFieldProvider {
formatValue(value: number | undefined): string {
if (value === undefined || Number.isNaN(value)) return '';
const opts = this.opts.formatOptions.current;
const nums = this.soma?.nums;
const nums = this.soma.nums;
if (nums) return nums.format(value, opts);
return intlFormat(this.locale.current, opts).format(value);
}
@ -233,7 +232,7 @@ export class NumberFieldProvider {
if (value === undefined || Number.isNaN(value)) return '';
const scaled = this.toEditValue(value);
const opts = this.editFormatOptions;
const nums = this.soma?.nums;
const nums = this.soma.nums;
if (nums) return nums.format(scaled, opts);
return intlFormat(this.locale.current, opts).format(scaled);
}
@ -241,7 +240,7 @@ export class NumberFieldProvider {
/** Parse edit string and convert to internal value. Returns undefined on failure. */
parseValue(raw: string): number | undefined {
if (!raw.trim()) return undefined;
const nums = this.soma?.nums;
const nums = this.soma.nums;
let n: number | undefined;
if (nums) {
n = nums.parse(raw);
@ -451,10 +450,12 @@ export class NumberFieldInputProvider {
private constructor(opts: NumberFieldInputOpts) {
this.opts = opts;
this.runtimePart = Soma.require()
.runtime(numberFieldMorfo, {})
.part('input', { id: opts.id, ref: opts.ref, owner: this });
this.provider = NumberFieldProvider.require();
this.runtimePart = this.provider.runtime.part('input', {
id: opts.id,
ref: opts.ref,
owner: this
});
// When embedded inside Field, register this input id so Field.Label's
// `for=` attribute targets it.
if (this.provider.field) {
@ -525,10 +526,12 @@ export class NumberFieldIncrementTriggerProvider {
private constructor(opts: NumberFieldIncrementTriggerOpts) {
this.opts = opts;
this.runtimePart = Soma.require()
.runtime(numberFieldMorfo, {})
.part('increment-trigger', { id: opts.id, ref: opts.ref, owner: this });
this.provider = NumberFieldProvider.require();
this.runtimePart = this.provider.runtime.part('increment-trigger', {
id: opts.id,
ref: opts.ref,
owner: this
});
}
readonly isDisabled = $derived.by(() => this.provider.isDisabled || this.provider.atMax);
@ -543,7 +546,7 @@ export class NumberFieldIncrementTriggerProvider {
type: 'button' as const,
tabindex: -1,
disabled: this.isDisabled || undefined,
'aria-label': this.provider.soma?.langs.ts(NUMBER_FIELD_LANGS.INCREMENT),
'aria-label': this.provider.soma.langs.ts(NUMBER_FIELD_LANGS.INCREMENT),
'aria-disabled': boolToStr(this.isDisabled),
'data-disabled': boolToEmptyStrOrUndef(this.isDisabled),
onclick: this.handleClick
@ -566,10 +569,12 @@ export class NumberFieldDecrementTriggerProvider {
private constructor(opts: NumberFieldDecrementTriggerOpts) {
this.opts = opts;
this.runtimePart = Soma.require()
.runtime(numberFieldMorfo, {})
.part('decrement-trigger', { id: opts.id, ref: opts.ref, owner: this });
this.provider = NumberFieldProvider.require();
this.runtimePart = this.provider.runtime.part('decrement-trigger', {
id: opts.id,
ref: opts.ref,
owner: this
});
}
readonly isDisabled = $derived.by(() => this.provider.isDisabled || this.provider.atMin);
@ -584,7 +589,7 @@ export class NumberFieldDecrementTriggerProvider {
type: 'button' as const,
tabindex: -1,
disabled: this.isDisabled || undefined,
'aria-label': this.provider.soma?.langs.ts(NUMBER_FIELD_LANGS.DECREMENT),
'aria-label': this.provider.soma.langs.ts(NUMBER_FIELD_LANGS.DECREMENT),
'aria-disabled': boolToStr(this.isDisabled),
'data-disabled': boolToEmptyStrOrUndef(this.isDisabled),
onclick: this.handleClick
@ -619,10 +624,12 @@ export class NumberFieldScrubberProvider {
private constructor(opts: NumberFieldScrubberOpts) {
this.opts = opts;
this.runtimePart = Soma.require()
.runtime(numberFieldMorfo, {})
.part('scrubber', { id: opts.id, ref: opts.ref, owner: this });
this.provider = NumberFieldProvider.require();
this.runtimePart = this.provider.runtime.part('scrubber', {
id: opts.id,
ref: opts.ref,
owner: this
});
}
private get isDisabled(): boolean {

Loading…
Cancel
Save

Powered by TurnKey Linux.