/** * Framework-agnostic widget command execution. * * Adapters supply an element lookup, host callbacks, a focus/scroll scheduler * and an announcer; this module turns the headless controller's * {@link MdyUiCommand}s into real DOM/overlay side effects. */ import type { MdyUiCommand, MdyElementTarget } from "./commands.js"; import type { MdyWidgetRuntimeCapabilities } from "./runtime.js"; /** Looks up a widget part element, optionally by item key. */ export type MdyElementLookup = (part: string, key?: string) => HTMLElement | undefined; /** Host callbacks for command side effects that need framework cooperation. */ export interface MdyWidgetCommandHandlers { /** Called for open-overlay / close-overlay. */ setOpen(open: boolean): void; /** Called for emit-change. */ onChange?(): void; /** Called for mark-touched. */ onTouched?(): void; /** Called for mark-dirty. */ onDirty?(): void; } /** Context supplied by each adapter when processing commands. */ export interface MdyWidgetCommandContext { readonly lookup: MdyElementLookup; readonly handlers: MdyWidgetCommandHandlers; /** * Both the element as it is now and the target that named it. * * A caller that acts immediately uses the element. One that defers until the host has rendered * needs the target, because the render is what may replace the node — resolving before it and * focusing after it is how focus lands on something no longer in the document. */ scheduleFocus(el: HTMLElement, target: MdyElementTarget): void; scheduleScroll(el: HTMLElement, target: MdyElementTarget): void; announce(message: string): void; /** * What the runtime can do, from {@link browserRuntimeCapabilities}. * * Optional, and omitting it keeps every command executing as before. Supplying it is what makes * the capability report mean something: with no DOM the commands that need one are dropped here * rather than attempted. `runtime.ts` has always said a controller consults the report "to avoid * emitting commands that cannot be executed" — nothing did, so the report was a description of * behaviour that existed nowhere. */ readonly capabilities?: MdyWidgetRuntimeCapabilities; } /** * Walks a list of UI commands and invokes framework-specific side effects * through the provided context. Focus/scroll operations are only scheduled; * the adapter flushes them with its own DOM timing. * * Commands that need a DOM are skipped where the runtime says there is none. The state-changing * ones — opening, closing, marking touched — still run: they are the controller's own bookkeeping * and mean the same thing on a server as in a browser. */ export declare function processWidgetCommands(commands: readonly MdyUiCommand[] | null | undefined, context: MdyWidgetCommandContext): void; /** Visually hidden live region used by all adapters for screen-reader announcements. */ export interface MdyAnnouncer { announce(message: string): void; } /** What marks a live region as shared by the whole page rather than owned by one widget. */ export declare const MDY_SHARED_REGION_ATTRIBUTE = "data-mdy-shared-region"; /** * The one live region on the page, named by the contract rather than by each renderer. * * Eight adapters each spelled their own — `mdy-plain-announcer`, `mdy-lit-announcer`, six more — so a * page carrying two of them carried two polite regions. Two regions speaking in the same instant are * read in an order nothing specifies: every screen reader has its own policy, and one announcement * cuts the other off partway. The loss is the same with one region, but with one there is somewhere * to put the queue that prevents it. * * Identity belongs in the message, not in the region: a reader speaks the text, never who wrote it. * Two regions saying "open" leave a person hearing "open" twice with nothing to attach it to, which * is why "Città: elenco aperto" is the fix and a second region is not. */ export declare const MDY_SHARED_REGION_ID = "mdy-live-region"; /** * Creates the page's live region if it is not there yet, and announces through it. * * The id is the contract's. It is still accepted so a renderer outside this repository keeps * working, but passing one means keeping a second region on the page, with everything above. */ export declare function createMdyAnnouncer(elementId?: string): MdyAnnouncer; /** * How a host defers work until after it has rendered. * * The only thing that genuinely differs between adapters. A microtask for the ones whose scheduler * has already flushed by then, a frame for the ones that batch into one, a host promise for the ones * that publish "I have finished rendering" — and focus is the reason it matters: focusing an element * the host is about to replace moves focus to a node that will not be there. */ export type MdyCommandDefer = (run: () => void) => void; export interface MdyCommandRuntimeOptions { /** * A live region of this host's own, where it must not share the page's. * * Omitted, the host announces through {@link MDY_SHARED_REGION_ID} — one region for the page, * which is what a person listening to two renderers at once needs. Naming one here gives this host * a second region, and two regions speaking at once are read in an order nothing specifies. */ readonly announcerId?: string; readonly defer: MdyCommandDefer; } export interface MdyCommandRuntime { /** * Runs what a controller answered with. * * `commands` may be nothing: a controller handed an intent its kind does not have answers with * `undefined`, and the documented headless path feeds `dispatch` straight into this call. */ execute(commands: readonly MdyUiCommand[] | null | undefined, lookup: MdyElementLookup, handlers: MdyWidgetCommandHandlers): void; } /** * Executing widget commands, written once. * * Seven adapters had this function and it was the same function seven times: collect focus and * scroll into a queue, run everything else now, then drain the queue after the host has rendered. * What differed was the id of the live region and one call — `queueMicrotask`, `requestAnimationFrame`, * `afterNextRender`, `host.updateComplete.then`. A copy per adapter is how one of them comes to defer * with the wrong primitive and focus lands on a node the host has already replaced. */ export declare function createCommandRuntime(options: MdyCommandRuntimeOptions): MdyCommandRuntime;