export interface MdyWidgetIdFactory { /** ID for a named part of a widget instance. */ part(widgetId: string, part: string): string; /** ID for an item inside a named part (e.g. an option in a listbox). */ item(widgetId: string, part: string, key: string): string; } /** * What separates the segments of a generated id. * * Re-exported from `@modyra/core`, which owns it because the dynamic parser has to reject names * containing it and core cannot import this package. * * It matters because a widget id may not contain it: `part("a", "label")` and a field named * `a__label` both land on `a__label`, in different roles. The browser allows two elements to carry * the same id, so `getElementById`, `label[for]` and every ARIA IDREF quietly stop being * deterministic — a failure invisible until two particular fields share a page. * * The delimiter is forbidden in names rather than escaped. Escaping would encode `_`, changing the * id of every field whose name contains one, and those ids are consumer-visible. Forbidding costs * nothing: an id built from a name containing the delimiter was never deterministic in the first * place, so nothing correct is being taken away. */ export { MDY_ID_DELIMITER } from "@modyra/core"; /** * Whether a widget id can safely be a segment of a generated id. * * Whitespace is refused for the reason the delimiter is: it makes one reference into several. * `aria-labelledby` and `aria-describedby` are **space-separated lists** of ids, so a widget id of * `"my form"` produces `aria-labelledby="my form__label"`, which an assistive technology reads as * two references — `my` and `form__label` — and resolves to nothing anyone rendered. The control * then has no accessible name at all, and the markup looks correct while it says nothing. * * The HTML rule is the same one, written from the other side: an id must not contain ASCII * whitespace. */ export declare function isValidWidgetId(widgetId: string): boolean; /** * Refuses a widget id that cannot be one, where a widget's part ids are built. * * {@link isValidWidgetId} is the question a host can ask; this is the answer they get if they do not. * A predicate only protects the renderers that remember to call it, and this package is the surface * third-party renderers are built on — the one nobody has written yet is who this is for. * * Not in {@link defaultWidgetIdFactory}: that is a joining primitive a consumer may replace, it is * documented as deterministic and reversible, and something constructing ids speculatively is * entitled to use it. The per-kind builders are this contract's own front door, and a widget whose * ids cannot be referenced is not a widget anyone can render. * * Loud rather than repaired: an id is consumer-visible, so rewriting one silently would change what * a host's tests and stylesheets look for. An id containing whitespace was never a usable id, so * nothing correct is refused. */ export declare function assertUsableWidgetId(widgetId: string): void; /** Default deterministic ID factory. */ export declare const defaultWidgetIdFactory: MdyWidgetIdFactory; /** * A piece of data as a piece of an id. * * A widget id is a host's word and is refused when it cannot be one; this is for the parts that are * **data** — an option's value, a row's key, and the path a document gives a nested field. Refusing * those would refuse the document that declared them, so they are spelled instead, in the one * encoding an id may carry. * * The path is here for the same reason the others are, and it arrived by the harder road: the * library refused a host's id containing punctuation and then wrote one itself, because a field * inside a collection is named `rows.0.name` and the separator is a class selector to a browser. * `querySelector("#form-rows.0.name")` does not miss, it **throws** — a class may not begin with a * digit — so a consumer selecting a nested field by the id this contract published gets an * exception, and the only input needed is a form inside a form. * * Whitespace is why: `aria-activedescendant` and its family are space-separated lists of ids, so an * option valued `New York` produced `city__option__New York`, which an assistive technology reads as * two references and resolves to neither. The person operating the list by keyboard is pointed at * nothing, on an option that is on screen. * * Percent-encoded rather than replaced: `%` goes first so the encoding stays reversible, and the * delimiter is encoded because an id carrying it a second time cannot be taken apart again. * * **Each whitespace character carries its own code**, which is what makes the encoding injective as * well as reversible. One sequence for all five maps `a b`, `a\tb` and `a\nb` onto a single id, and * the browser accepts duplicate ids without complaint — so `getElementById`, `label[for]` and every * ARIA IDREF resolve to whichever element the document reaches first. A tab or a newline inside an * option's value is what a paste from a spreadsheet produces, so the colliding keys are the ordinary * case rather than a hostile one. * * No code emitted here contains the delimiter or whitespace, so an id still splits into exactly its * three segments — widget, part, key. */ export declare function idSafeKey(key: string): string; /** * The id a calendar's day cell carries. * * One rule in one place: the field controllers compute it for the part table, and a renderer that * cannot reach that table — a calendar drawn by a component two levels below the field — asks here * rather than rebuilding the string. Two places computing one id is the shape that drifts the day * the format changes. */ export declare function calendarDayId(widgetId: string, iso: string): string; /** * Warns when a widget publishes an id another element on the page already carries. * * Ids are the field's path (ADR 0135), so two forms built from one document claim one set of ids * unless the host scopes them. That is a *visible* failure by design — the record rejects renaming * the second form's ids, because a mount-order-dependent id is the counter's defect returned in a * corner — but silent it is the worst of both: `aria-describedby` resolves into the other form and * the page looks exactly like one whose references are right. * * So: warn, never rename. `advice` is how a renderer says which of its own doors sets the scope — the * fact belongs here and the spelling belongs to whoever is being read. * * Stateless on purpose — it asks the document rather than keeping a registry of live ids, so nothing * has to be released on teardown and a remount cannot report a collision with its own former self. */ export declare function reportIdCollision(element: Element, advice?: string, warn?: (message: string) => void): readonly string[]; /** * The scope every id of one form sits in. * * A form carries an identity of its own, and every widget bound to it derives its ids inside that * identity — so two forms built from the same document do not both claim `when__label`, whether or * not the consumer knew to ask for a scope. ADR 0146. * * The default is a function of the document, so a remount and a hydration land on the ids they had. * **Two forms built from the *same* document are the case that cannot be answered from the document** * — they are identical by construction — so the second is told apart by `taken`, a question only the * caller can answer because only the caller can see the page. A caller that cannot say passes * nothing and gets the signature, which is the single-form case and by far the common one. * * A caller that needs the scope to be a name it chose passes it instead; the renderers' own doors — * `idPrefix`, `id-scope`, `[idScope]` — still win over this. */ export declare function formScopeOf(form: object | null | undefined, taken?: (scope: string) => boolean): string; /** * The scope of the form that built `handle`. * * Separate from {@link formScopeOf} rather than one function taking either: a form and a handle are * both objects, so a single entry point cannot tell an unregistered handle from a form and would * hold a scope **per control** for the first — every field of one form under a different scope, * which is worse than the collision this exists to end. * * `undefined` for a hand-built handle no form registered, which is a shape core's registry * documents. A caller that gets it should leave the id unscoped: an id that moves because a test * double was used is worse than one that collides where nothing else exists to collide with. */ export declare function widgetScopeOf(handle: object | null | undefined, taken?: (scope: string) => boolean): string | undefined;