Route Soma announce timers through scheduler

active-uix
dev 5 months ago
parent 88be88f210
commit 89911c36c2

@ -6,7 +6,7 @@ import type { AnnouncePriority, AnnounceRole } from './types';
import { announceMorfo } from '../../../morfo/components/announce';
import type { SomaRuntime, SomaRuntimePart } from '../../runtime.svelte';
import type { TimerHandle } from '$timer';
/** Visually-hidden style kept in-sync with the WAI "sr-only" recipe. */
const VISUALLY_HIDDEN_STYLE = {
@ -58,8 +58,8 @@ export class AnnounceProvider {
private politeToggle = false;
private assertiveToggle = false;
private politeTimer: ReturnType<typeof setTimeout> | null = null;
private assertiveTimer: ReturnType<typeof setTimeout> | null = null;
private politeTimer: TimerHandle | null = null;
private assertiveTimer: TimerHandle | null = null;
private constructor(opts: AnnounceOpts) {
this.opts = opts;
@ -73,10 +73,7 @@ export class AnnounceProvider {
});
$effect(() => {
return () => {
if (this.politeTimer) clearTimeout(this.politeTimer);
if (this.assertiveTimer) clearTimeout(this.assertiveTimer);
};
return () => this.clearTimers();
});
}
@ -96,12 +93,17 @@ export class AnnounceProvider {
this.assertiveA = '';
this.assertiveB = message;
}
if (this.assertiveTimer) clearTimeout(this.assertiveTimer);
this.assertiveTimer = setTimeout(() => {
this.assertiveA = '';
this.assertiveB = '';
this.assertiveTimer = null;
}, clearAfter);
this.clearAssertiveTimer();
this.assertiveTimer = this.soma.uix.timers.schedule(
this.assertiveTimerKey,
clearAfter,
() => {
this.assertiveA = '';
this.assertiveB = '';
this.assertiveTimer = null;
},
{ replace: true, meta: { component: 'announce', action: 'clear-assertive' } }
);
} else {
this.politeToggle = !this.politeToggle;
if (this.politeToggle) {
@ -111,12 +113,17 @@ export class AnnounceProvider {
this.politeA = '';
this.politeB = message;
}
if (this.politeTimer) clearTimeout(this.politeTimer);
this.politeTimer = setTimeout(() => {
this.politeA = '';
this.politeB = '';
this.politeTimer = null;
}, clearAfter);
this.clearPoliteTimer();
this.politeTimer = this.soma.uix.timers.schedule(
this.politeTimerKey,
clearAfter,
() => {
this.politeA = '';
this.politeB = '';
this.politeTimer = null;
},
{ replace: true, meta: { component: 'announce', action: 'clear-polite' } }
);
}
};
@ -125,12 +132,36 @@ export class AnnounceProvider {
this.politeB = '';
this.assertiveA = '';
this.assertiveB = '';
if (this.politeTimer) clearTimeout(this.politeTimer);
if (this.assertiveTimer) clearTimeout(this.assertiveTimer);
this.politeTimer = null;
this.assertiveTimer = null;
this.clearTimers();
};
private clearPoliteTimer() {
if (this.politeTimer) {
this.politeTimer.cancel();
this.politeTimer = null;
}
}
private clearAssertiveTimer() {
if (this.assertiveTimer) {
this.assertiveTimer.cancel();
this.assertiveTimer = null;
}
}
private clearTimers() {
this.clearPoliteTimer();
this.clearAssertiveTimer();
}
private get politeTimerKey() {
return `soma:announce:${this.opts.id.current}:polite`;
}
private get assertiveTimerKey() {
return `soma:announce:${this.opts.id.current}:assertive`;
}
readonly snippetProps = $derived.by(() => ({
announce: this.announce,
clear: this.clear

@ -1,4 +1,5 @@
import type { ActiveDom } from '$adom';
import type { TimerHandle, TimerScheduler } from '$timer';
import type { AnnouncePriority, AnnounceApi } from './types';
/**
@ -32,10 +33,11 @@ type RegionPair = {
a: HTMLElement;
b: HTMLElement;
toggle: boolean;
timer: ReturnType<typeof setTimeout> | null;
timer: AnnounceTimer | null;
};
type AnnouncerCache = WeakMap<Document, Map<string, RegionPair>>;
type AnnounceTimer = Pick<TimerHandle, 'cancel'>;
function ensurePair(
dom: ActiveDom,
@ -91,7 +93,35 @@ function ensurePair(
return pair;
}
function write(dom: ActiveDom, pair: RegionPair, text: string, timeout: number) {
function scheduleClear(
key: string,
timeout: number,
task: () => void,
timers?: TimerScheduler
): AnnounceTimer {
if (timers) {
return timers.schedule(key, timeout, task, {
replace: true,
meta: { component: 'announce', action: 'global-clear' }
});
}
const native = setTimeout(task, timeout);
return {
cancel() {
clearTimeout(native);
return true;
}
};
}
function write(
dom: ActiveDom,
pair: RegionPair,
text: string,
timeout: number,
timers?: TimerScheduler
) {
pair.toggle = !pair.toggle;
if (pair.toggle) {
dom.writeText(pair.a, text);
@ -100,12 +130,17 @@ function write(dom: ActiveDom, pair: RegionPair, text: string, timeout: number)
dom.writeText(pair.a, '');
dom.writeText(pair.b, text);
}
if (pair.timer) clearTimeout(pair.timer);
pair.timer = setTimeout(() => {
dom.writeText(pair.a, '');
dom.writeText(pair.b, '');
pair.timer = null;
}, timeout);
if (pair.timer) pair.timer.cancel();
pair.timer = scheduleClear(
`soma:announce:global:${pair.a.id}`,
timeout,
() => {
dom.writeText(pair.a, '');
dom.writeText(pair.b, '');
pair.timer = null;
},
timers
);
}
/**
@ -122,13 +157,19 @@ function write(dom: ActiveDom, pair: RegionPair, text: string, timeout: number)
* ```
*
* @param defaultTimeout Ms before messages are cleared. @default 1000
* @param timers Optional UIX/App scheduler. When omitted, the global fallback
* uses native timers because this API is intentionally usable outside UIX.
*/
export function createAnnouncer(dom: ActiveDom, defaultTimeout = 1000): AnnounceApi {
export function createAnnouncer(
dom: ActiveDom,
defaultTimeout = 1000,
timers?: TimerScheduler
): AnnounceApi {
const announce: AnnounceApi['announce'] = (message, priority = 'polite', timeout) => {
const baseId = priority === 'assertive' ? ASSERTIVE_ID : POLITE_ID;
const pair = ensurePair(dom, priority, baseId);
if (!pair) return;
write(dom, pair, message, timeout ?? defaultTimeout);
write(dom, pair, message, timeout ?? defaultTimeout, timers);
};
const clear: AnnounceApi['clear'] = () => {
@ -147,7 +188,7 @@ export function createAnnouncer(dom: ActiveDom, defaultTimeout = 1000): Announce
for (const pair of cache.values()) {
dom.writeText(pair.a, '');
dom.writeText(pair.b, '');
if (pair.timer) clearTimeout(pair.timer);
if (pair.timer) pair.timer.cancel();
pair.timer = null;
}
};

@ -60,7 +60,7 @@ export class DragDropProvider {
private constructor(opts: DragDropOpts) {
this.opts = opts;
this.globalAnnounce = createAnnouncer(this.soma.dom, 1500);
this.globalAnnounce = createAnnouncer(this.soma.dom, 1500, this.soma.uix.timers);
this.runtime = this.soma.runtime(dragDropMorfo, {});
this.runtimePart = this.runtime.part('provider', {
id: opts.id,

Loading…
Cancel
Save

Powered by TurnKey Linux.