yk-button: member contracts move into JSDoc; the header keeps intent
commit 62e529c9f2045fab097dfb73c62947a602ee7d79 · parent 39823989c8
· browse this tree
Ximianth, 2026-10-17 12:20:48 UTC
Every documented member, public or private, now carries its doc as a /** */ block on the declaration, so the public ones reach dist/yk-button.d.ts and all of them reach editor hover. Each starts with the member's name. The per-property contract (label marking, icon, disabled, active, info, hotkey, hotkeyDirect) comes out of the file header to live there. The header keeps what belongs to no single member: the dual context and its claim table (now bullets instead of an aligned table), the single-click-event activation model, and the flash. Wording is unchanged apart from lowercasing emphasis and backticks on key names; lines break at clauses.
diff --git a/packages/yukey/src/yk-button.ts b/packages/yukey/src/yk-button.ts index 89dcd1c..2573546 100644 --- a/packages/yukey/src/yk-button.ts +++ b/packages/yukey/src/yk-button.ts @@ -1,76 +1,51 @@ // yk-button: the button that makes the guidelines' keyboard norms structural. // Use it and the norms happen; there is nothing to remember per screen. -// -// The norms it embodies (docs/precepts-we-love-the-keyboard.md, "buttons always show their hotkey" -// and the megacolumn sections): -// -// - Hotkey marking: if the hotkey letter occurs in the label, its first -// occurrence renders in --yk-hotkey-accent; otherwise the letter shows in a -// slot 70% of the way across. A complex button (more than one concept of -// activation) sets `icon` instead, which claims that slot: a single letter -// would mislead, so the icon says "more here; ask i". -// - The i norm: pressing i (forwarded in a row, or while focused standalone) -// dispatches a composed `yk-info` event carrying this button's `info` text. -// yk-infopop renders the popup; `info` is also mirrored onto data-info, -// the attribute yk-infopop's hover trigger finds carriers by -// (one authored content, two triggers). -// i answers even when disabled: it is how you ask why a thing is off. -// - Hit targets from --yk-hit-min-h/w. -// - Active (`active`): the in-effect option among sibling choices, marked with -// the selected wash and aria-current -- and STILL clickable: re-issuing an -// in-effect choice is the norm's contract (owners decide whether re-issue -// means anything). Not aria-pressed: these are not toggles; activating an -// active button never un-presses it. -// - Disabled is aria-disabled, never the native disabled attribute: the button -// stays perceivable (and standalone: focusable), activation is suppressed. +// (The norms: docs/precepts-we-love-the-keyboard.md, "buttons always show their hotkey" and the megacolumn sections. +// Each property's doc names the norm it carries.) // // Dual context, detected at connect time: // -// - Inside a megacolumn row (a [data-mc-row] ancestor in the same shadow tree): -// renders non-focusable markup -- the container must stay the column's only -// tab stop, and non-focusable content also gives clicks their native behavior -// of focusing the container. Activation arrives as megacolumn-key events, -// heard on the row element so the button answers row-scoped keys even when -// the forward targeted the row's other control. Claims (first claim wins; -// a claim preventDefaults, and a disabled button claims but declines): -// Enter/Space at this button's own position -> activate -// its hotkey, only when hotkeyDirect -> activate (via the column- -// scoped marking contract: -// data-mc-hotkey plus -// data-mc-hotkey-direct on the -// host). WITHOUT hotkeyDirect -// the button declines its own -// hotkey: that decline lets the -// controller resolve the marking -// as a focus move to this row, -// and Enter/Space then activate -// (see-before-commit). -// x/Delete from any position, aux buttons only-> activate (the removal norm) -// i at this button's own position -> yk-info -// - Standalone (no [data-mc-container] ancestor): a real native button; Tab -// reaches it, Enter/Space activate natively, the focus ring is the tokens'. -// Its hotkey and i answer only while it holds focus (a broader standalone -// hotkey scope is an app-level concern, not this component's). -// - Inside a container but outside any row: fail closed (throws at connect). -// A native tab stop loose in a container would quietly fracture the -// single-tab-stop model, and the scan cannot see into this shadow root. +// - Inside a megacolumn row (a [data-mc-row] ancestor in the same shadow tree): +// renders non-focusable markup -- the container must stay the column's only tab stop, +// and non-focusable content also gives clicks their native behavior of focusing the container. +// Activation arrives as megacolumn-key events, +// heard on the row element so the button answers row-scoped keys even when the forward targeted the row's other control. +// The button claims keys: it preventDefaults the forwarded `megacolumn-key` event, +// so later listeners and the controller treat the key as handled +// (the claim idiom is megacolumn-controller.ts's; it is defined there). +// First claim wins, and a disabled button claims but declines to act. What it claims: +// - Enter/Space at this button's own position: activate. +// - Its hotkey, only when hotkeyDirect: activate +// (via the column-scoped marking contract: data-mc-hotkey plus data-mc-hotkey-direct on the host). +// Without hotkeyDirect the button declines its own hotkey: +// that decline lets the controller resolve the marking as a focus move to this row, +// and Enter/Space then activate (see-before-commit). +// - x/Delete from any position, aux buttons only: activate (the removal norm). +// - i at this button's own position: yk-info. +// - Standalone (no [data-mc-container] ancestor): a real native button; +// Tab reaches it, Enter/Space activate natively, the focus ring is the tokens'. +// Its hotkey and i answer only while it holds focus +// (a broader standalone hotkey scope is an app-level concern, not this component's). +// - Inside a container but outside any row: fail closed (throws at connect). +// A native tab stop loose in a container would quietly fracture the single-tab-stop model, +// and the scan cannot see into this shadow root. // -// Activation is a `click` event on this element in every context and by every -// trigger (mouse, Enter/Space, hotkey): authors write ONE @click handler. -// In row mode every host click is also CLAIMED (preventDefault) as it passes: -// the megacolumn controller synthesizes an Enter-equivalent megacolumn-key from -// unclaimed clicks on row controls (its pointer-activation norm for plain rows), -// and the claim tells it this widget already owns click-activation -- without it, -// a mouse click would activate twice, once natively and once synthesized. -// Standalone clicks stay unclaimed: no controller listens, and a native button's -// click default is not this component's to consume. +// Activation is a `click` event on this element in every context and by every trigger (mouse, Enter/Space, hotkey): +// authors write one @click handler. +// In row mode every host click is also claimed (preventDefault) as it passes: +// the megacolumn controller synthesizes an Enter-equivalent megacolumn-key from unclaimed clicks on row controls +// (its pointer-activation norm for plain rows), +// and the claim tells it this widget already owns click-activation -- +// without it, a mouse click would activate twice, once natively and once synthesized. +// Standalone clicks stay unclaimed: no controller listens, +// and a native button's click default is not this component's to consume. // -// Activation feedback: every successful activation flashes the pressed look for -// --yk-press-ms (press-flash.ts), so keyboard triggers are as visible as a mouse -// press; a pointer held down shows the same look for as long as it stays down. +// Activation feedback: every successful activation flashes the pressed look for --yk-press-ms (press-flash.ts), +// so keyboard triggers are as visible as a mouse press; +// a pointer held down shows the same look for as long as it stays down. // Disabled buttons show neither: nothing happened, nothing must claim otherwise. -// The yk-pressed/yk-held classes live on the HOST element, outside classMap's -// managed set, so renders never clobber a flash in flight. +// The yk-pressed/yk-held classes live on the host element, outside classMap's managed set, +// so renders never clobber a flash in flight. import { css, html, LitElement, type TemplateResult } from "lit"; import { customElement, property } from "lit/decorators.js"; @@ -80,6 +55,15 @@ import type { MegacolumnKeyDetail } from "./megacolumn-controller.js"; import { flashPress } from "./press-flash.js"; import { dispatchYkInfo } from "./yk-info.js"; +/** + * YkButton is a button with the guidelines' keyboard norms built in: + * a marked hotkey, the `i` norm, the aux (small-x) form inside megacolumn rows, + * and one `click` event for every trigger. + * Hit targets from `--yk-hit-min-h`/`--yk-hit-min-w`. + * + * Standalone it is a native button; + * inside a megacolumn row it is a non-focusable row control (the file header lists which keys it claims in that mode). + */ @customElement("yk-button") export class YkButton extends LitElement { static styles = css` @@ -161,20 +145,56 @@ export class YkButton extends LitElement { } `; + /** + * label is the visible text. + * Hotkey marking: if the hotkey letter occurs in the label, its first occurrence renders in `--yk-hotkey-accent`; + * otherwise the letter shows in a slot 70% of the way across. + */ @property() label = ""; + + /** + * icon is a glyph for the hotkey slot. + * A complex button (more than one concept of activation) sets this instead of relying on the letter, + * and it takes that slot: + * a single letter would mislead, so the icon says "more here; ask `i`". + */ @property() icon = ""; + + /** + * disabled suppresses activation. + * Disabled is `aria-disabled`, never the native disabled attribute: + * the button stays perceivable (and standalone: focusable). + * `i` answers even when disabled: it is how you ask why a thing is off. + */ @property({ type: Boolean, reflect: true }) disabled = false; + + /** + * active marks the in-effect option among sibling choices, with the selected wash and `aria-current` -- + * and still clickable: re-issuing an in-effect choice is the norm's contract + * (owners decide whether re-issue means anything). + * Not `aria-pressed`: these are not toggles; activating an active button never un-presses it. + */ @property({ type: Boolean }) active = false; - // The info content, mirrored synchronously onto data-info: that attribute is - // the carrier contract yk-infopop's hover trigger finds elements by - // (decision-info-content: one authored content, two triggers), and mirroring - // keeps this property a typed accessor over the same single source. + /** + * #info is the info content, mirrored synchronously onto `data-info`: + * that attribute is the carrier contract yk-infopop's hover trigger finds elements by + * (decision-info-content: one authored content, two triggers), + * and mirroring keeps the `info` property a typed accessor over the same single source. + */ #info = ""; + + /** + * info is the text for the `i` norm: + * pressing `i` (forwarded in a row, or while focused standalone) + * dispatches a composed `yk-info` event carrying this text; yk-infopop renders the popup. + * Also mirrored onto `data-info` for yk-infopop's hover trigger. + * Empty means not a carrier. + */ @property() get info(): string { return this.#info; @@ -185,13 +205,22 @@ export class YkButton extends LitElement { else this.dataset.info = v; } - // The hotkey, normalized to lowercase, mirrored synchronously onto - // data-mc-hotkey so the megacolumn controller's scan sees the marking without - // waiting for this component's own render. Validation matches the scan's - // (single [a-z0-9]) so a bad hotkey fails loudly in standalone contexts too, - // where no scan exists. x and i are legal but conventionally left unmarked: - // the row norms answer them (see the megacolumn DOM contract's guideline). + /** + * #hotkey is the hotkey, normalized to lowercase, mirrored synchronously onto `data-mc-hotkey` + * so the megacolumn controller's scan sees the marking without waiting for this component's own render. + * Validation (in the setter) matches the scan's (single `[a-z0-9]`) + * so a bad hotkey fails loudly in standalone contexts too, where no scan exists. + */ #hotkey = ""; + + /** + * hotkey is a single `[a-z0-9]` character, case-insensitive; setting anything else throws. + * In a megacolumn row, pressing it moves logical focus to this row and Enter/Space then activate (see-before-commit); + * with {@link hotkeyDirect} the press activates outright. + * Standalone, it activates while the button holds focus. + * `x` and `i` are legal but conventionally left unmarked: + * the row norms answer them (see the megacolumn DOM contract's guideline). + */ @property() get hotkey(): string { return this.#hotkey; @@ -206,15 +235,22 @@ export class YkButton extends LitElement { else this.dataset.mcHotkey = norm; } - // Direct activation: the marked hotkey activates this button immediately - // (focus moving to its row as part of the act) instead of only moving focus - // there. Mirrored synchronously onto data-mc-hotkey-direct, the controller's - // half of the contract, the same way hotkey mirrors onto data-mc-hotkey. - // The deliberately long name is friction: skipping see-before-commit is - // legitimate where an undo semantic exists (the guidelines' example: a map - // move order is cheap to cancel, so just give the order). No undo? Leave - // this off and let the letter jump; Enter is close by. + /** + * #hotkeyDirect is mirrored synchronously onto `data-mc-hotkey-direct`, the controller's half of the contract, + * the same way hotkey mirrors onto `data-mc-hotkey`. + */ #hotkeyDirect = false; + + /** + * hotkeyDirect (attribute `hotkey-direct`) is direct activation: + * the marked hotkey activates this button immediately + * (focus moving to its row as part of the act) instead of only moving focus there. + * + * The deliberately long name is friction: + * skipping see-before-commit is only legitimate where an undo semantic exists + * (the guidelines' example: a map move order is cheap to cancel, so just give the order). + * No undo? Leave this off and let the letter jump; Enter is close by. + */ @property({ type: Boolean, attribute: "hotkey-direct" }) get hotkeyDirect(): boolean { return this.#hotkeyDirect; @@ -268,7 +304,7 @@ export class YkButton extends LitElement { this.#releaseHold(); } - // Row-mode claims; see the file header for the claim table. + /** #onMegacolumnKey answers forwarded keys in row mode; the file header has the table of which keys it claims. */ #onMegacolumnKey = (e: Event): void => { if (e.defaultPrevented) return; // someone earlier in the row claimed it const { key, position } = (e as CustomEvent<MegacolumnKeyDetail>).detail; @@ -288,7 +324,7 @@ export class YkButton extends LitElement { } }; - // Standalone: the hotkey and i answer while the button holds focus. + /** #onKeydown is standalone mode: the hotkey and `i` answer while the button holds focus. */ #onKeydown = (e: KeyboardEvent): void => { if (e.ctrlKey || e.altKey || e.metaKey) return; const k = e.key.toLowerCase(); @@ -305,12 +341,15 @@ export class YkButton extends LitElement { if (this.disabled) e.stopPropagation(); }; - // Every successful activation is a click on this host, whatever the trigger, - // so the flash hooks here and keyboard activations get the same visual - // acknowledgment as a mouse press. The disabled check repeats the - // suppressor's: a direct host-level click() never crosses the shadow root. - // In row mode the click is claimed even when disabled: nothing may activate, - // but the controller must not synthesize an activation from it either. + /** + * #onActivated is registered as the host's `click` listener in connectedCallback. + * Every activation path ends in a `click` event on this host (the keyboard paths call `this.click()`), + * so this one listener sees every activation and gives it the flash; + * that is how keyboard activations get the same visual acknowledgment as a mouse press. + * The disabled check repeats the suppressor's: a direct host-level click() never crosses the shadow root. + * In row mode the click is claimed (preventDefault) even when disabled: + * nothing may activate, but the controller must not synthesize an activation from it either. + */ #onActivated = (e: Event): void => { if (this.#row !== null) e.preventDefault(); if (this.disabled) return; @@ -326,8 +365,10 @@ export class YkButton extends LitElement { this.classList.remove("yk-held"); }; - // A claimed key is consumed whether or not the button acts: a disabled - // button's Space must not fall through and scroll the column. + /** + * #claimActivation consumes a claimed key whether or not the button acts: + * a disabled button's Space must not fall through and scroll the column. + */ #claimActivation(e: Event): void { e.preventDefault(); if (this.disabled) return; @@ -379,9 +420,11 @@ export class YkButton extends LitElement { >${content}</button>`; } - // Index of the letter-in-label marking: the first case-insensitive occurrence - // of the hotkey. -1 when unmarked (no hotkey, no occurrence, or an icon - // claiming the slot -- a complex button must not imply one letter is the story). + /** + * #labelMarkIndex is the index of the letter-in-label marking: the first case-insensitive occurrence of the hotkey. + * -1 when unmarked (no hotkey, no occurrence, or an icon claiming the slot -- + * a complex button must not imply one letter is the story). + */ #labelMarkIndex(): number { if (this.#hotkey === "" || this.icon !== "") return -1; return this.label.toLowerCase().indexOf(this.#hotkey);