/** * The chip. * * A chip is one primitive with variants, not one shape per place it appears: an option you can * take, a counter you can step, a value already taken. The foundation styles exactly that — * `.mdy-chip` carries the box, the variants carry the difference. * * Naming it here is what lets the variants stay derived. A renderer that wrote `"mdy-chip--counter"` * as a string is a renderer deciding what a counter chip is, and the next one spells it differently * or forgets the selected modifier — at which point the theme's `.mdy-chip--selected` rule quietly * styles nothing. {@link multiselectChipClasses} answers "what classes does this chip carry" once, * from the widget's mode and the option's state, and every renderer applies the answer. */ import type { MdyMultiselectMode } from "@modyra/core"; /** Canonical class vocabulary for the chip primitive. */ export declare const MDY_CHIP_CLASSES: Readonly<{ /** The chip itself. Every chip carries this, whatever it is for. */ block: "mdy-chip"; /** An option in single-select mode: it reserves the room its tick will need. */ centered: "mdy-chip--centered"; /** An option that can be taken several times: a label between two step buttons. */ counter: "mdy-chip--counter"; /** A value already taken, shown on the control rather than offered in the list. */ value: "mdy-chip--value"; /** Taken. The state a theme paints; it applies in either mode. */ selected: "mdy-chip--selected"; /** Carries a control for taking the value off again, which changes the room the label has. */ removable: "mdy-chip--removable"; /** The tick. Drawn by the theme when a renderer supplies no icon of its own. */ check: "mdy-chip__check"; /** The chip's text. */ label: "mdy-chip__label"; /** How many times this option has been taken, in counter mode. */ count: "mdy-chip__count"; /** A step button — one down, one up. */ step: "mdy-chip__btn"; /** The control that takes a chosen value off, on the chip standing for it. */ remove: "mdy-chip__remove"; /** * The controls that move a chosen value one place, on the chip standing for it. * * A pointer path that is **not** a drag. WCAG 2.5.7 asks for one independently of any keyboard * path: somebody using a pointer who cannot hold and drag — a tremor, a head pointer, a switch — * has no way to reorder otherwise, and a keyboard alternative does not discharge it. Drawn only * where the field asked to be reorderable, so a set of filters gains no furniture. */ move: "mdy-chip__move"; /** Wraps a chip a renderer did not draw itself, so a custom option still sits in the grid. */ wrapper: "mdy-chip-wrapper"; }>; export type MdyChipPart = keyof typeof MDY_CHIP_CLASSES; /** * How a multiselect treats its options: taken or not, or taken a number of times. * * The mode is a field of the Dynamic Form Contract, so the name that owns it lives in `@modyra/core` * and everything else refers to it. A chip's appearance follows the value a document carries; a * second declaration of the same two strings is a place for the two to disagree. */ export type MdyChipMode = MdyMultiselectMode; /** Where a chip appears: offered among the options, or standing for a value already taken. */ export type MdyChipRole = "option" | "value"; export interface MdyChipAppearance { readonly mode?: MdyChipMode; readonly role?: MdyChipRole; readonly selected?: boolean; /** Carries a dismiss affordance. A value chip you can take back off the control. */ readonly removable?: boolean; } /** * The classes a chip carries, in order: the primitive, then its variant, then its state. * * The variant follows the mode, because that is what the difference *is*: an option that can be * taken once shows a tick and reserves room for it (`centered`), one that can be taken repeatedly * shows a count between two steppers (`counter`). Selection is a state on top of either, never a * variant of its own — a theme that styled "selected" twice, once per mode, would drift. */ export declare function multiselectChipClasses(appearance?: MdyChipAppearance): readonly string[]; /** * What a button inside a chip is called. * * The verb alone — "Remove", "One fewer", "Rimuovi" — names the action and not its object, so a strip * of eight chips offers eight controls with one name between them. Someone reading the page one * control at a time hears "Remove" and has to leave it, find the chip beside it, and come back to * know what they would be removing; someone listing the controls hears the same word eight times. * * **Every button in the chip, not only the one that removes it.** Read from the accessibility tree, a * two-chip strip offered `Remove Alfa` and `Remove Beta` beside four steppers called `One fewer` and * `One more` — four controls that sound like two, in the same chip that already knew how to say it. * And the unnamed pair is the one that destroys: stepping down from one takes the value off. * * The words stay with the renderer, which is where the language lives. The rule that the object * belongs in the name lives here, so all of them compose it the same way. */ export declare function chipActionName(verb: string, label: string): string; /** * Where focus goes when a chip is taken off, named as the chip it should land on. * * `null` means the strip has nothing left and focus belongs on the control itself. * * The next chip, or the previous one when the last was removed. Stated rather than left to the * browser, because the browser's answer is *whatever now occupies that position in the DOM* — which * is the next chip while one exists and nothing at all at the end of the strip, so removing from the * middle looked deliberate and removing the last dropped focus to the document. Somebody clearing a * strip from the right loses their place on the first press. */ export declare function chipFocusAfterRemoval(order: readonly string[], removed: string, /** * Which way the removal was going. * * `"backward"` — `Backspace` — lands on the chip *before* the one removed, `"forward"` — * `Delete` — on the one after. That is what every text field on every platform does, and a strip * of chips is close enough to a line of text that a person brings the expectation with them. * Absent is forward, which is what a pointer on the chip's own remove control means: there was no * direction in the gesture. */ direction?: "forward" | "backward"): string | null; /** * What a live region says when a selection changes: the change, and the new total. * * **The delta, never the list.** A polite region queues rather than replaces, so a full selection * announced on every click builds a backlog of stale lists and the person hears a selection several * actions out of date. At any size, not only at twelve. The list itself is an on-demand fact and * belongs in the field's description, where a reader can ask for it. * * Empty while the popup is open: the options there carry `aria-selected` and announce natively, so * a region firing at the same time makes every toggle speak twice. The row's own removals are the * case nothing else speaks for. */ /** * What the one way back says it is putting back. * * "Undo" alone is ambiguous once a single reversal covers three acts, so the affordance names the * act. A clear has no value to name and says how many it took; a removal and a move name the value, * because that is what the person is deciding whether they meant to lose. */ export declare function wayBackSentence(way: { readonly act: "remove" | "move" | "clear"; readonly optionKey: string | null; readonly count: number; }, templates: { readonly removed: string; readonly moved: string; readonly cleared: string; }, labelOf: (key: string) => string): string; /** * What the one way back is called, for a reader who meets it as a mark rather than as a sentence. * * The offer is a control at the field's trailing edge and its mark says only that a way back exists. * The name is where the act lives: what will come back, or how many will. * * Composed from the same three templates the sentence uses rather than from three more of its own. * A second set of wordings for one fact drifts the moment either is translated again, and the act is * the half that varies — one reversal covers a removal, a move and a clear, and "restore" is wrong * for the middle one. * * With nothing to put back the name is the bare label. The control is on the page at rest, so it is * in the accessibility tree at rest, and a button with no name there is announced as "button" — * which is what a name is for. The act is the part that is missing, not the control. */ export declare function wayBackActionName(way: { readonly act: "remove" | "move" | "clear"; readonly optionKey: string | null; readonly count: number; } | null, templates: { readonly label: string; readonly removed: string; readonly moved: string; readonly cleared: string; }, labelOf: (key: string) => string): string; /** * What a live region says when a choice lands: the change, and the new total. * * The **change**, not the list — a polite region queues rather than replaces, so announcing the whole * selection builds a backlog of stale lists and a person hears a selection several acts out of date. * * Said whether or not the popup is open. It used to be suppressed while open, on the reasoning that * the options there announce themselves and a region firing too would speak twice — which holds only * for somebody choosing with the keyboard, where focus is on the option. A choice made with a pointer * moves no focus and announces nothing at all, so the suppression was silence for exactly the person * with no other confirmation. The count is not in the native announcement either way. */ export declare function multiselectAnnouncement(previous: readonly string[], next: readonly string[], words: { readonly added: string; readonly removed: string; readonly empty: string; /** What is said when the value removed was the last one; falls back to `empty` where absent. */ readonly removedLast?: string; /** * What is said when one act moved more than one value. `{moved}` is how many changed, `{count}` * how many are held afterwards — a clear taking three from three and one taking three from ten * are different facts, and one number cannot tell them apart. * * Optional, and the singular forms answer where they are absent: a caller that has not been * given them says something true about a smaller act rather than nothing at all. */ readonly addedMany?: string; readonly removedMany?: string; readonly removedManyLast?: string; }, labelOf: (key: string) => string): string; /** * What a live region says when a quantity settles. * * A quantity that changes says nothing today: the sentence a selection change produces compares the * *distinct values* held, and stepping three of something down to two changes none of them. So the * only step that spoke was the one that removed the value — a person stepping down heard nothing * until what they were counting was gone. * * **Said on arriving at the smallest quantity, not on leaving it.** Warning at the moment of deletion * is too late: the value is already gone and the person is being told rather than asked. Said on * arrival, the next step down is a known act. */ export declare function quantityAnnouncement(label: string, count: number, words: { readonly settled: string; readonly atMinimum: string; }, minimum?: number): string; /** * A voice that says the value a gesture ended on, rather than every value it passed through. * * A live region is a queue, and a held arrow key fills it: eleven presses from twelve to one leave * eleven polite sentences to be read out after the person has stopped pressing, each describing a * state several steps in the past. A `spinbutton` does not have this problem because the platform * reads a *value* and coalesces rapid changes itself — so a control that gives up the role takes on * the coalescing, which is what this is. * * `schedule` is injected so a test can settle the voice without waiting: the default is the clock. */ export declare function settledVoice(say: (sentence: string) => void, options?: { readonly delayMs?: number; readonly schedule?: (run: () => void, ms: number) => unknown; readonly cancel?: (handle: unknown) => void; }): { announce: (sentence: string) => void; stop: () => void; }; /** * What a live region says when a chosen value is moved. * * The `Alt`-plus-arrow way of reordering has no *grabbed* state — nothing is picked up and nothing * is put down — so the movement itself is the only thing there is to announce. Unannounced, a * reorder is invisible to somebody who cannot see the strip: the value changed and the control said * nothing about it. */ export declare function chipMovedAnnouncement(template: string, label: string, position: number, count: number): string; /** * Where a dragged chip would land, from where the pointer is. * * Takes the horizontal midpoints of the chips as they are drawn, in order, and answers the index the * dragged one would occupy on release. Shared rather than written per renderer for the same reason * the dial's angle arithmetic is: three implementations of "which one is the pointer over" is three * answers, and the one a person sees is whichever renderer they happen to be using. * * Reads midpoints rather than edges so the drop follows what the eye does — a chip is "passed" when * the pointer is more than halfway across it, not when it clears the far edge. * * Direction-agnostic: the midpoints arrive in drawing order, so a right-to-left strip answers with * the same arithmetic and no renderer has to know which way its own text runs. */ export declare function chipDropIndex(midpoints: readonly number[], clientX: number, from: number): number; /** * How far a wheel turn should move a horizontal strip. * * ADR 0127 lets the chip row scroll on the condition that there is a **mechanism**, not only a cue, * for reaching what has scrolled out — and many desktop mice have no horizontal axis at all, so a * strip that only answers `deltaX` is a strip a large number of people cannot move. The larger of * the two deltas is taken, so a vertical wheel drives it and a trackpad's horizontal gesture still * behaves as its owner expects. * * Answers `0` when the strip has nothing hidden, so the page keeps its own scrolling: a wheel * swallowed by a row that had nowhere to go is a page that will not move. */ /** * A wheel reaches what has scrolled out of a chip strip. * * ADR 0127 allows the row to scroll only where a mechanism reaches what leaves the viewport, and * many desktop mice have no horizontal axis at all — so a vertical wheel over the strip has to move * it sideways. The event is cancelled only when the strip actually has something hidden, or the * gesture stops scrolling the page for no result. */ export declare function scrollChipStripByWheel(event: WheelEvent): void; /** * Where a chip's tooltip sits, in the control's own coordinates. * * Above the strip rather than inside it: the strip clips its overflow, which is the whole reason the * name needed revealing. The chip's offset is taken against the strip it scrolls in, so a chip * scrolled halfway out is named where it is drawn and not where it began. */ export declare function chipTooltipOffset(chip: HTMLElement, strip: HTMLElement): number; /** * How many chips the strip is not showing. * * A chip counts as hidden when it is not wholly inside the strip's own box — clipped at either edge, * because the row scrolls both ways and a chip half off the leading edge is as unreadable as one off * the trailing edge. `0` when nothing overflows, which is the case a control must not draw an * affordance for. * * Measured rather than derived from the count: how many fit depends on the labels, the theme's * spacing and the width the host gave the field, and a renderer guessing at it would be wrong on the * first long name. * * The chips are found by their own class at any depth, not taken as the strip's immediate children. * What the strip holds directly is a layout decision — a row, a grid's row element, one day * something else — and counting those counts containers: a single row wider than its box is one * child outside the box, so the answer is `1` however many chips are out of sight. A number that is * always one is not an imprecise measurement, it is a constant wearing a measurement's clothes, and * it reads as correct exactly once. */ export declare function hiddenChipCount(strip: HTMLElement): number; /** * Brings the chip that has focus back into the strip, after something changed the strip's width. * * The browser scrolls a focused element into view once, at the moment focus lands. An affordance * that appears on the same beat — the count of what is hidden, a clear-all — takes its width out of * the scrollport *afterwards*, and the chip the browser had just brought in is outside again by * about the width of the control that appeared. Nothing scrolls a second time on its own. * * Called after the measurement that may have changed the box, so the rule is: whatever the strip * ends up as wide as, the focused chip is inside it. */ /** * The chosen values as the strip lays them out: distinct, in the order the value has them. * * The order a strip draws is the order the value holds, not the order the options were declared in — * a person who moves a chip has rearranged their own answer, and a strip reading the offer would * put it back on the next paint. Repeats collapse to one chip carrying a count, so the sequence is * of *distinct* keys. * * Derived here rather than in each renderer because the key is the contract's: it is what * `keyFor` produced, which for an object value is a structural key and not `String(value)`. A * renderer deriving its own has a second key function, and the two agree on strings and part ways * on everything else — every object collapsing to one entry, so a strip of five chips reorders as * if it held one. */ export declare function chosenKeyOrder(state: { readonly counts: ReadonlyMap; }): readonly string[]; /** * The element carrying a key, found by comparing rather than by selecting. * * A key is derived from a value, and a value is whatever the document put in it. Inside an attribute * selector it stops being data and becomes syntax: a quote closes the selector and the browser * throws `SyntaxError`, which for a structural key — the shape an object value takes — is every * time. The gesture does not misbehave, it raises, and it takes the surrounding handler with it. * * Escaping would work and is not what this does. `CSS.escape` is a browser global this package must * not require — it computes in processes with no DOM — and a hand-rolled escape for attribute values * is a second set of rules to keep correct. Comparing needs neither: the key is read back from the * dataset as the string it was written as. */ export declare function elementByDataKey(root: ParentNode, attribute: "key" | "option-key", key: string): HTMLElement | null; export declare function keepFocusedChipInView(strip: HTMLElement): void; export declare function chipStripWheelDelta(deltaX: number, deltaY: number, scrollWidth: number, clientWidth: number): number; /** * A chip dragged along its strip, from the press that starts it to the index it lands on. * * Written out identically wherever a strip is drawn, down to the six pixels and the swallowed click. * The renderer still binds the press its own way — that is the part a framework owns — and this is * everything between the press and the drop. * * **The threshold is the whole design.** A drag may start anywhere on the chip, its own buttons * included: they cover most of it, and a chip draggable only by its bare edges is a chip nobody can * drag. What separates a press from a drag is travel, so a press that stays put belongs to the button * under it and one that moves belongs to the strip. * * **The click is swallowed once, in the capture phase.** A press that began on a button and ended as * a gesture still produces a click, and nobody asked for it. Taken before it reaches the button, once, * so the next real press on that button still works. * * **Tracked on the document, not by capturing the pointer.** `setPointerCapture` follows the gesture * just as far and retargets every later pointer event — the one that becomes a `click` included — to * the capturing element, at which point the chip's own buttons stop receiving their clicks entirely: * found, pressed, nothing happens. */ export interface MdyChipReorderOptions { /** The class a chip wears while it is being dragged. */ readonly draggingClass: string; /** Horizontal travel, in pixels, that turns a press into a drag. */ readonly threshold?: number; /** The horizontal centre of each chip in the strip, in the order they are shown. */ midpoints(): readonly number[]; /** Where the dragged chip sits now. */ from(): number; /** Called only when the chip lands somewhere else. */ onDrop(to: number): void; } export declare function beginChipReorder(press: { readonly button: number; readonly clientX: number; }, chip: HTMLElement, options: MdyChipReorderOptions): void; /** * How far a press travels before it is a drag. * * Published because it is the number that decides whether a chip's own buttons still work: too small * and a steady finger reorders the strip instead of pressing what it is on, too large and a drag has * to be exaggerated before anything moves. */ export declare const MDY_CHIP_DRAG_THRESHOLD = 6;