/** * The accessibility projection every field shell shares, independent of what the control is. * * Carries the state a screen reader needs — validity, requiredness, interactivity — and the * relations that tie the control to its label, description and error list. A widget that has no * controller of its own still needs all of it: without `aria-describedby` the error list is * rendered, styled, and announced to nobody. * * Deliberately narrower than a full field projection. `type`, `inputmode`, `autocomplete` and * `readonly` belong to a text control and are not projected here. Root classes are not projected * either: the shell already applies the kind's own, and restating them would create a second source * of truth. */ import type { MdyFieldConstraints, MdyFieldError } from "@modyra/core"; import type { MdyPartContract } from "../contract.js"; /** The state a shell reflects: the flags, with no value and no control-specific concerns. */ export interface MdyFieldShellFlags { readonly disabled: boolean; readonly required: boolean; /** * Whether the field refuses changes while staying in play. * * Optional because it is the newer half: a caller that omits it says nothing about read-only, * which is what every caller did when only `disabled` reached the shell. */ readonly readonly?: boolean; } export interface MdyFieldShellA11yOptions { readonly widgetId: string; /** * The id the label points at. A daterange has two inputs and can point at only one, so the caller * names it rather than this guessing which control is the primary one. */ readonly controlId?: string; /** * Whether the error list is actually in the document. * * The caller owns this because only the caller knows when it renders one. A renderer that defers * the list until the field is touched has errors long before it shows them, and deriving this * from `errors.length` would make `aria-describedby` name an element that is not in the document. * * Defaults to "there are errors", which is correct for a renderer that always shows them. */ readonly errorsVisible?: boolean; /** * The key this control sends its value under when the browser submits the form it sits in. * * The field's path, not its widget id: the id carries a per-form scope so two forms on one page do * not collide, and a scope in a payload is a key the receiving end never asked for. Two forms send * the same key and stay apart, because a payload belongs to its form. * * Absent leaves the control unserialised, which is what a native submit does with a control that * has no name: it sends nothing at all, rather than sending it empty. */ readonly submitName?: string; /** * Whether the control announces itself as failing. * * Separate from `errorsVisible`, which says what is on the page to point at. A field drawing its * refusal *inline* renders no error list, and a control that read one flag for both then said it * was valid while the field beside it was painted wrong and an icon stated the reason. * * Defaults to `errorsVisible`, so a renderer that draws its errors one way only is unaffected. */ readonly invalid?: boolean; /** * What a document wrote for this field's name, when the caller knows it. * * Supplied, the label says whether it is showing words somebody chose or words the shell composed * because nobody did — which is the difference between a heading and a name owed to a screen * reader. Omitted, the label carries no such claim and nothing changes. */ readonly nameSources?: { readonly ariaLabel?: string | null; readonly label?: string | null; }; /** * Whether the supporting-text element is in the document. * * Same reason as {@link MdyFieldShellA11yOptions.errorsVisible}: a renderer that only emits * supporting text when a host supplies some would otherwise be described by an element that does * not exist. When neither a description nor an error list is present, the control describes * itself by nothing. * * Defaults to true, for a renderer that always emits the element. */ readonly descriptionVisible?: boolean; /** * Whether the error container is on the page, whether or not it holds a message. * * Distinct from {@link MdyFieldShellA11yOptions.errorsVisible}, which is about the message. A * renderer reserving the container under every field that can fail a rule passes this and keeps one * reference that never changes — and a reference that never changes has no moment at which it can * point at an element not yet drawn, or one already gone. * * Defaults to `errorsVisible`, so a renderer that draws the container only when it has something to * say is unaffected. */ readonly errorsReserved?: boolean; /** * The kind whose control this is, so the shell knows which native constraints it can carry. */ readonly kind?: string; /** * What the field's rules state, already narrowed by anything the control asks for. * * Here as well as in {@link import("./text-field-a11y.js").projectTextFieldA11y} because a renderer takes * its control part from one or the other, and a control's attributes cannot depend on which * projection its renderer happens to use. */ readonly constraints?: MdyFieldConstraints; /** * What the field holds, where the kind draws it on a track. * * A slider's range is the only attribute that depends on the value: it has to span what the field * holds, or the thumb sits somewhere the form does not. See `sliderTrack`. */ readonly value?: number | null; } /** The ids a shell's parts carry, so a renderer can put them on its own elements. */ export declare function fieldShellPartIds(widgetId: string): { readonly labelId: string; readonly descriptionId: string; readonly errorId: string; }; /** * The classes the field's own root carries, from the states it is in. * * Every kind had this function, and every copy was the same five lines over the same table. The * states a root may carry are declared once in `MDY_FIELD_STATE_CLASSES`; deriving the classes from * them is not a per-kind decision. */ export declare function fieldShellRootClasses(state: Readonly>): readonly string[]; export declare function projectFieldShellA11y(flags: MdyFieldShellFlags, errors: ReadonlyArray, options: MdyFieldShellA11yOptions): { readonly label: MdyPartContract; /** Applied to whichever element the widget treats as its operable control. */ readonly control: MdyPartContract; readonly description: MdyPartContract; readonly error: MdyPartContract; }; /** * What describes a control: its error, then its help, both when both are there. * * Not one or the other. An error message does not take the place of the instruction that would have * prevented it — losing the help at the moment it is most useful — and a description is a list, so * both fit. The error goes first because it is the new thing, and somebody who moves on after the * first sentence has heard the one that mattered. * * An element with no text contributes nothing to the description: it is not read as a pause or as * "empty", it is as though the reference were absent, until text appears inside it. That is what * makes a permanently-present reference cheaper than a correct one. */ export declare function fieldDescribedBy(parts: { readonly errorId: string; readonly descriptionId: string; readonly errorsPresent: boolean; readonly descriptionPresent: boolean; }): string | null;