yk-list: contracts move onto YkList, its properties, and YkListAddDetail
commit 473e406f17cd2f984c768fb0795e5906803b9be0 · parent 67070847d2
· browse this tree
Ximianth, 2026-10-18 10:34:17 UTC
The "what an owner gets" bullets (header row, live rows, add-row, list-scoped keys), the key-semantics and rendered-structure paragraphs, and the light-DOM/fail-closed paragraph move onto the YkList class doc. The per-property block moves onto the properties, each named first with its attribute where it differs; the five pass-through properties get one full doc on items and pointers from the other four. The yk-list-add event contract moves onto YkListAddDetail. Every other documented member gets its comment as a /** */ block; ykListStyles likewise. The header keeps the one-line purpose and the group-identity decision for AT, which updated()'s body comment points at. Wording is unchanged apart from lowercasing emphasis, backticks (including on `<name>-header`, which hover's markdown would otherwise eat as a tag), name-first prefixes, and "no group identity claimed" -> "taken", since claim means something specific in this kit; lines break at clauses.
diff --git a/packages/yukey/src/yk-list.ts b/packages/yukey/src/yk-list.ts index d11e310..ad977c8 100644 --- a/packages/yukey/src/yk-list.ts +++ b/packages/yukey/src/yk-list.ts @@ -1,109 +1,16 @@ // yk-list: the list region of a megacolumn, with the list norms built in. -// An owner composes it into its column ([data-mc-container]) and gets, by -// construction, the behaviors the guidelines ask every list to have: -// -// - The HEADER ROW (optional): a normal nav stop that speaks for the region. -// While it holds focus the whole list gets the quiet group wash; the focus -// ring stays on the header row itself -- deliberately no perimeter border, -// which would read as a nested focus container. Its data-info describes -// the list AS a list. Headerless lists are legal (no header row renders). -// - The LIVE ROWS: an inner yk-live-list in megacolumn mode (that file owns -// the row-lifecycle contract: churn safety, ghosts, geometry, the key pin). -// - The ADD-ROW (optional): the append affordance as a permanent ghosted nav -// stop at the tail. Its marked hotkey (default a) is DIRECT: it dives -// straight into text entry, earned under the guidelines' easy-undo clause, -// because NOTHING SYNCS UNTIL COMMIT: Enter dispatches the commit event, -// Escape abandons the draft, and the component holds no copy of the text -- -// the input's value is the draft, and the owner's data is untouched until -// the owner itself acts on the commit. -// - The LIST-SCOPED KEYS: t jumps to the list's top (its header when it has -// one, else its first row) and n jumps past the list to the next stop after -// it. Documented norms with defaults, deliberately NOT hotkey markings -// (every list wearing the same gold letters would be noise; the letters -// only mean this while focus is inside the list's own rows). Scope is DOM -// containment: this element hears the bubbling megacolumn-key dispatch from -// its own rows before the controller's column-level marking resolution runs, -// so inner scope wins by dispatch order. A claimed key with no landing is -// the clamp: consumed, focus stays -- same shape as the machine's no-wrap ends. -// -// The public surface: -// -// properties (attributes in kebab-case where marked): -// name Namespaces the structural row ids: the header row is -// "<name>-header", the add-row "<name>-add". Required -// [a-z0-9-]+; default "list". Two lists in one column need -// distinct names (the controller throws on duplicate row ids). -// header Header row text. Empty (the default) means headerless: -// no header row, no group identity claimed. -// renderHeader Optional render-prop for the header ROW's visible content -// (an owner composing its own header line -- live counts, a -// column-label grid). The `header` string stays the identity -// switch and the group's aria-label: a stable name for AT -// while the visible content varies. Ignored when headerless. -// header-info The owner's whole-list info text. The component appends its -// own norm teaching (t/n, the add hotkey) to whatever is given, -// so the norms read the same in every list. -// addable Renders the add-row. Lists that cannot append (a cargo -// manifest) just leave it off. -// add-label The add-row's label; default "add…". The hotkey letter is -// marked in the label (first occurrence, yk-button's rule), -// or shown after it when the label lacks the letter. -// add-hotkey The marked direct hotkey; default "a" (single [a-z0-9]). -// Override only when two addable lists share a column: the -// controller throws on duplicate markings. -// add-info The owner's info text for the add-row; norm teaching is -// appended, as with header-info. -// items, keyOf, renderItem, renderEmpty, resetKey -// Passed through to the inner yk-live-list untouched: the -// owner renders row CONTENT and keeps data identity; see -// yk-live-list.ts for their contracts. -// -// events: -// yk-list-add The add-row's commit (bubbling, cancelable; detail -// { text: string }, trimmed, never empty). Dispatched only on -// the input's own Enter keydown -- never on change/blur, so an -// Escape that blurs the input cannot masquerade as a commit. -// Uncancelled: the component clears the draft and the text-mode -// exit proceeds, with row focus remaining on the add-row (Enter -// re-dives). preventDefault() REFUSES the commit (owner-side -// validation): the draft and text mode both survive, so the -// user fixes the text instead of retyping it. -// -// key semantics stay the owner's: megacolumn-key listeners (what Enter, x, -// Delete MEAN on a live row) belong on the owner's container, exactly as -// without yk-list. The structural rows decline every key -- owners routing -// by rowId should skip "<name>-header" and "<name>-add" (that id scheme is -// public contract). Owners claiming letters that overlap t/n or the add -// hotkey should respect a prior claim (check defaultPrevented), though -// wanting to is already a design smell. -// -// rendered structure (public contract, since it renders into the owner's -// tree): [header row?] <yk-live-list megacolumn> [add-row?], all inside this -// element. yk-live-list's feel knobs (holdMs, keyPinMs, pointerSafety...) -// are deliberately NOT mirrored here -- mirrored twins invite drift -- and are -// set on the inner element where they live. The yk-list-prefixed classes on -// the structural rows are stable; their look is the norm's look. +// The public contract is on the YkList class doc and its properties; yk-list-add is on YkListAddDetail. // // Group identity for AT (the header-role decision, settled for flat lists): -// with a header, this element takes role="group" plus an aria-label mirroring -// the header text -- the APG listbox pattern's grouping idiom, giving AT the -// region identity the wash gives sighted users. The header row itself STAYS a -// role=option nav stop: reachable with arrows, stable stride counts, t lands on -// it. It is still announced as an option, but as the first option of a group -// named like it -- descriptive, where the bare demo markup was misleading. -// An author-set role on the host is left alone. Trees are explicitly NOT this -// component's business: a treeitem reframe (levels, expansion state) belongs to -// a distinct tree vocabulary, and nothing here forecloses one -- nesting -// yk-list was never legal (rows come from renderItem, not markup). -// -// Like yk-live-list's mega mode, this renders into the owner's tree (light -// DOM): the megacolumn scan and aria-activedescendant cannot cross shadow -// boundaries. No shadow root, no slots; styles adopt into the owner's root -// once, yk-list-prefixed. After every update it dispatches megacolumn-rescan -// (structural rows change under the controller's scan on this element's own -// schedule, not the owner's). Fail closed: connecting outside a -// [data-mc-container] throws, element children throw, a malformed name or -// add-hotkey throws. +// with a header, this element takes role="group" plus an aria-label mirroring the header text -- +// the APG listbox pattern's grouping idiom, giving AT the region identity the wash gives sighted users. +// The header row itself stays a role=option nav stop: reachable with arrows, stable stride counts, t lands on it. +// It is still announced as an option, but as the first option of a group named like it -- +// descriptive, where the bare demo markup was misleading. +// An author-set role on the host is left alone. +// Trees are explicitly not this component's business: +// a treeitem reframe (levels, expansion state) belongs to a distinct tree vocabulary, +// and nothing here forecloses one -- nesting yk-list was never legal (rows come from renderItem, not markup). import { css, html, LitElement, nothing, type TemplateResult } from "lit"; import { customElement, property } from "lit/decorators.js"; @@ -112,18 +19,30 @@ import type { MegacolumnKeyDetail } from "./megacolumn-controller.js"; import type { LiveListPhase } from "./yk-live-list.js"; import "./yk-live-list.js"; +/** + * YkListAddDetail is the detail of a `yk-list-add` event: the add-row's commit + * (bubbling, cancelable; detail { text: string }, trimmed, never empty). + * Dispatched only on the input's own Enter keydown -- + * never on change/blur, so an Escape that blurs the input cannot masquerade as a commit. + * Uncancelled: the component clears the draft and the text-mode exit proceeds, + * with row focus remaining on the add-row (Enter re-dives). + * preventDefault() refuses the commit (owner-side validation): + * the draft and text mode both survive, so the user fixes the text instead of retyping it. + */ export interface YkListAddDetail { - text: string; // trimmed, never empty + /** text is trimmed, never empty. */ + text: string; } -// The norm's look for the structural rows, adopted into the owner's root. -// The wash rule is the header-row norm made structural: focus on the header -// washes the whole region (group identity), keyed off the controller's -// mc- classes; the ring stays the kit's standard row outline on the header. -// The add-row idles ghosted (an affordance, not a form field); diving in -// raises the well and the accent border. The raised look is keyed to the -// add-row's OWN focus class, not just the container's text mode: two lists in -// one column must not both light up when one of them is being typed in. +/** + * ykListStyles is the norm's look for the structural rows, adopted into the owner's root. + * The wash rule is the header-row norm made structural: + * focus on the header washes the whole region (group identity), keyed off the controller's mc- classes; + * the ring stays the kit's standard row outline on the header. + * The add-row idles ghosted (an affordance, not a form field); diving in raises the well and the accent border. + * The raised look is keyed to the add-row's own focus class, not just the container's text mode: + * two lists in one column must not both light up when one of them is being typed in. + */ const ykListStyles = css` yk-list { display: block; @@ -170,37 +89,149 @@ const ykListStyles = css` } `; +/** + * YkList is the list region of a megacolumn, with the list norms built in. + * An owner composes it into its column (`[data-mc-container]`) and gets, by construction, + * the behaviors the guidelines ask every list to have: + * + * - The header row (optional): a normal nav stop that speaks for the region. + * While it holds focus the whole list gets the quiet group wash; + * the focus ring stays on the header row itself -- + * deliberately no perimeter border, which would read as a nested focus container. + * Its data-info describes the list as a list. + * Headerless lists are legal (no header row renders). + * - The live rows: an inner yk-live-list in megacolumn mode + * (that element owns the row-lifecycle contract: churn safety, ghosts, geometry, the key pin). + * - The add-row (optional): the append affordance as a permanent ghosted nav stop at the tail. + * Its marked hotkey (default `a`) is direct: it dives straight into text entry, + * earned under the guidelines' easy-undo clause, because nothing syncs until commit: + * Enter dispatches the commit event, Escape abandons the draft, and the component holds no copy of the text -- + * the input's value is the draft, and the owner's data is untouched until the owner itself acts on the commit. + * - The list-scoped keys: `t` jumps to the list's top (its header when it has one, else its first row) + * and `n` jumps past the list to the next stop after it. + * Documented norms with defaults, deliberately not hotkey markings + * (every list wearing the same gold letters would be noise; + * the letters only mean this while focus is inside the list's own rows). + * Scope is DOM containment: + * this element hears the bubbling megacolumn-key dispatch from its own rows + * before the controller's column-level marking resolution runs, so inner scope wins by dispatch order. + * A claimed key with no landing is the clamp: consumed, focus stays -- + * same shape as the machine's no-wrap ends. + * + * Key semantics stay the owner's: + * megacolumn-key listeners (what Enter, x, Delete mean on a live row) belong on the owner's container, + * exactly as without yk-list. + * The structural rows decline every key -- + * owners routing by rowId should skip `<name>-header` and `<name>-add` (that id scheme is public contract). + * Owners claiming letters that overlap `t`/`n` or the add hotkey should respect a prior claim (check defaultPrevented), + * though wanting to is already a design smell. + * + * Rendered structure (public contract, since it renders into the owner's tree): + * `[header row?] <yk-live-list megacolumn> [add-row?]`, all inside this element. + * yk-live-list's feel knobs (holdMs, keyPinMs, pointerSafety...) are deliberately not mirrored here -- + * mirrored twins invite drift -- and are set on the inner element where they live. + * The yk-list-prefixed classes on the structural rows are stable; their look is the norm's look. + * + * Like yk-live-list's mega mode, this renders into the owner's tree (light DOM): + * the megacolumn scan and aria-activedescendant cannot cross shadow boundaries. + * No shadow root, no slots; styles adopt into the owner's root once, yk-list-prefixed. + * After every update it dispatches megacolumn-rescan + * (structural rows change under the controller's scan on this element's own schedule, not the owner's). + * Fail closed: connecting outside a `[data-mc-container]` throws, element children throw, + * a malformed name or add-hotkey throws. + */ @customElement("yk-list") export class YkList<T = unknown> extends LitElement { + /** + * name namespaces the structural row ids: the header row is `<name>-header`, the add-row `<name>-add`. + * Required [a-z0-9-]+; default "list". + * Two lists in one column need distinct names (the controller throws on duplicate row ids). + */ @property() name = "list"; + + /** + * header is the header row text. + * Empty (the default) means headerless: no header row, no group identity taken. + */ @property() header = ""; + + /** + * headerInfo (attribute `header-info`) is the owner's whole-list info text. + * The component appends its own norm teaching (`t`/`n`, the add hotkey) to whatever is given, + * so the norms read the same in every list. + */ @property({ attribute: "header-info" }) headerInfo = ""; + + /** + * renderHeader is an optional render-prop for the header row's visible content + * (an owner composing its own header line -- live counts, a column-label grid). + * The `header` string stays the identity switch and the group's aria-label: + * a stable name for AT while the visible content varies. + * Ignored when headerless. + */ @property({ attribute: false }) renderHeader?: () => TemplateResult; + + /** + * addable renders the add-row. + * Lists that cannot append (a cargo manifest) just leave it off. + */ @property({ type: Boolean }) addable = false; + + /** + * addLabel (attribute `add-label`) is the add-row's label; default "add…". + * The hotkey letter is marked in the label (first occurrence, yk-button's rule), + * or shown after it when the label lacks the letter. + */ @property({ attribute: "add-label" }) addLabel = "add…"; + + /** + * addHotkey (attribute `add-hotkey`) is the marked direct hotkey; default `a` (single [a-z0-9]). + * Override only when two addable lists share a column: the controller throws on duplicate markings. + */ @property({ attribute: "add-hotkey" }) addHotkey = "a"; + + /** + * addInfo (attribute `add-info`) is the owner's info text for the add-row; + * norm teaching is appended, as with {@link headerInfo}. + */ @property({ attribute: "add-info" }) addInfo = ""; + + /** + * items is passed through to the inner yk-live-list untouched, + * as are {@link keyOf}, {@link renderItem}, {@link renderEmpty}, and {@link resetKey}: + * the owner renders row content and keeps data identity; see yk-live-list for their contracts. + */ @property({ attribute: false }) items: T[] = []; + + /** keyOf is passed through to the inner yk-live-list untouched; see {@link items}. */ @property({ attribute: false }) keyOf!: (item: T) => string; + + /** renderItem is passed through to the inner yk-live-list untouched; see {@link items}. */ @property({ attribute: false }) renderItem!: (item: T, phase: LiveListPhase) => TemplateResult; + + /** renderEmpty is passed through to the inner yk-live-list untouched; see {@link items}. */ @property({ attribute: false }) renderEmpty?: () => TemplateResult; + + /** resetKey is passed through to the inner yk-live-list untouched; see {@link items}. */ @property() resetKey = ""; - // The column this list belongs to; n walks its rows. Set at connect (which - // fails closed when there is none). + /** + * #container is the column this list belongs to; `n` walks its rows. + * Set at connect (which fails closed when there is none). + */ #container: HTMLElement | null = null; constructor() { @@ -212,7 +243,7 @@ export class YkList<T = unknown> extends LitElement { this.addEventListener("megacolumn-key", this.#onListKey); } - // Light DOM, no slots (see the header). Styles adopt into the owner's root. + /** createRenderRoot is light DOM, no slots (see {@link YkList}). Styles adopt into the owner's root. */ protected override createRenderRoot(): HTMLElement { if (this.children.length > 0) { throw new Error( @@ -266,12 +297,15 @@ export class YkList<T = unknown> extends LitElement { } } - // The t/n list-scoped keys (see the file header). Both resolve through - // megacolumn-focus-request, the controller's public focus-move affordance: - // asking is the filter -- the controller ignores requests for unfocusable or - // vanished rows and answers granted ones with preventDefault -- so each key - // walks its candidates until one lands. No landing = the clamp: the key - // stays claimed and focus stays put. + /** + * #onListKey is the `t`/`n` list-scoped keys (see {@link YkList}), + * registered as this element's megacolumn-key listener in the constructor. + * Both resolve through megacolumn-focus-request, the controller's public focus-move affordance: + * asking is the filter -- + * the controller ignores requests for unfocusable or vanished rows and answers granted ones with preventDefault -- + * so each key walks its candidates until one lands. + * No landing = the clamp: the key stays claimed and focus stays put. + */ #onListKey = (e: Event): void => { const { detail } = e as CustomEvent<MegacolumnKeyDetail>; if (detail.key !== "t" && detail.key !== "n") return; @@ -293,7 +327,7 @@ export class YkList<T = unknown> extends LitElement { } }; - // Ask the column to move logical focus to rowId; true when granted. + /** #requestFocus asks the column to move logical focus to rowId; true when granted. */ #requestFocus(rowId: string): boolean { const req = new CustomEvent("megacolumn-focus-request", { detail: { rowId }, @@ -304,13 +338,14 @@ export class YkList<T = unknown> extends LitElement { return req.defaultPrevented; } - // The add-row's commit/abandon mechanics, on the input's REAL keydown (text - // mode gives the input native focus, so this sees every key before the - // container's exit handling bubbles). Deliberately keydown, NOT @change: - // both text-mode exits blur the input, and change fires on - // blur-with-changed-value -- a change-driven commit would append on Escape - // too. Nothing syncs anywhere until the Enter commit; Escape abandons by - // clearing the draft. + /** + * #onAddKeydown is the add-row's commit/abandon mechanics, on the input's real keydown + * (text mode gives the input native focus, so this sees every key before the container's exit handling bubbles). + * Deliberately keydown, not @change: + * both text-mode exits blur the input, and change fires on blur-with-changed-value -- + * a change-driven commit would append on Escape too. + * Nothing syncs anywhere until the Enter commit; Escape abandons by clearing the draft. + */ #onAddKeydown = (e: KeyboardEvent): void => { const input = e.currentTarget as HTMLInputElement; if (e.key === "Escape") { @@ -374,8 +409,10 @@ export class YkList<T = unknown> extends LitElement { `; } - // The structural rows' info: the owner's text first, then the norm teaching -- - // appended by the component so the norms read the same in every list. + /** + * #composedHeaderInfo is the structural rows' info: the owner's text first, then the norm teaching -- + * appended by the component so the norms read the same in every list. + */ #composedHeaderInfo(): string { const t = this.header === "" ? "t returns to the top" : "t returns here"; const add = this.addable ? `${this.addHotkey} dives into the add-row at the tail; ` : ""; @@ -390,9 +427,11 @@ export class YkList<T = unknown> extends LitElement { return this.addInfo === "" ? norms : `${this.addInfo}; ${norms}`; } - // The add-row's hotkey marking: the letter accented in the label (first - // case-insensitive occurrence, yk-button's rule), or shown after the label - // when it lacks the letter -- a marking must be visible either way. + /** + * #markedAddLabel is the add-row's hotkey marking: + * the letter accented in the label (first case-insensitive occurrence, yk-button's rule), + * or shown after the label when it lacks the letter -- a marking must be visible either way. + */ #markedAddLabel(): TemplateResult { const idx = this.addLabel.toLowerCase().indexOf(this.addHotkey.toLowerCase()); if (idx === -1) return html`${this.addLabel} <span class="yk-list-hk">${this.addHotkey}</span>`;