yukey

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);