yukey

tab-cycle: the walk's rules move onto deepTabbables

commit 75428b734b4d48a810d81c2604de61079fe18bf1 · parent e5e792966c · browse this tree
Ximianth, 2026-10-18 10:49:22 UTC

The header's attribute-based-walk paragraph is deepTabbables' contract and
now lives on it; every other documented function and the installed set get
their comments as /** */ blocks.  The header keeps the purpose and the
install-once overview.  Wording is unchanged apart from name-first
prefixes; lines break at clauses.
diff --git a/packages/yukey/src/tab-cycle.ts b/packages/yukey/src/tab-cycle.ts
index e43c250..abbc946 100644
--- a/packages/yukey/src/tab-cycle.ts
+++ b/packages/yukey/src/tab-cycle.ts
@@ -1,18 +1,9 @@
-// Shadow-DOM-aware Tab containment for page shells: the kit's default is that
-// the Tab key never leaves the page and wanders into browser chrome (the
-// guidelines' "Focus containment and lifecycle" norm).  A shell installs it
-// once with installTabContainment(document); the helpers underneath
-// (deepTabbables, deepActiveElement, containTab) stay exported for tests and
-// for shells that need the walk itself (the gallery seats focus with it).
-//
-// The walk is attribute-based (tabIndex >= 0 on the usual focusable tags plus
-// anything with an explicit tabindex), not layout-based: it does not check
-// visibility or computed styles.  That is deliberate -- the callers' content is
-// megacolumn containers and native buttons, all visible by construction, and
-// staying layout-free keeps the walk cheap and jsdom-testable.  aria-hidden
-// subtrees are skipped: components here hide closed content that way
-// (yk-live-list's collapsing rows), and a tab stop invisible to AT must not be
-// a tab stop at all.
+// Shadow-DOM-aware Tab containment for page shells:
+// the kit's default is that the Tab key never leaves the page and wanders into browser chrome
+// (the guidelines' "Focus containment and lifecycle" norm).
+// A shell installs it once with installTabContainment(document);
+// the helpers underneath (deepTabbables, deepActiveElement, containTab) stay exported for tests
+// and for shells that need the walk itself (the gallery seats focus with it).
 
 function isTabbable(el: Element): boolean {
 	if (!(el instanceof HTMLElement)) return false;
@@ -21,7 +12,17 @@ function isTabbable(el: Element): boolean {
 	return el.matches("a[href], button, input, select, textarea, [contenteditable='true']");
 }
 
-// All tab stops under `root` in tree order, descending open shadow roots.
+/**
+ * deepTabbables is all tab stops under `root` in tree order, descending open shadow roots.
+ *
+ * The walk is attribute-based (tabIndex >= 0 on the usual focusable tags plus anything with an explicit tabindex),
+ * not layout-based: it does not check visibility or computed styles.
+ * That is deliberate -- the callers' content is megacolumn containers and native buttons, all visible by construction,
+ * and staying layout-free keeps the walk cheap and jsdom-testable.
+ * aria-hidden subtrees are skipped:
+ * components here hide closed content that way (yk-live-list's collapsing rows),
+ * and a tab stop invisible to AT must not be a tab stop at all.
+ */
 export function deepTabbables(root: ParentNode): HTMLElement[] {
 	const out: HTMLElement[] = [];
 	const walk = (node: ParentNode): void => {
@@ -36,7 +37,7 @@ export function deepTabbables(root: ParentNode): HTMLElement[] {
 	return out;
 }
 
-// The really-focused element, descending the shadow activeElement chain.
+/** deepActiveElement is the really-focused element, descending the shadow activeElement chain. */
 export function deepActiveElement(doc: Document): Element | null {
 	let active = doc.activeElement;
 	while (active?.shadowRoot?.activeElement != null) {
@@ -45,11 +46,13 @@ export function deepActiveElement(doc: Document): Element | null {
 	return active;
 }
 
-// Tab containment: keep Tab cycling within the document instead of escaping to
-// the browser chrome.  Only the edges are intercepted (last stop wrapping to
-// first, first back to last); interior Tabs stay native, so within-page traversal
-// is exactly the browser's own.  Focus outside the tab order (a text-entry dive
-// into a tabindex=-1 input) is left alone: Escape/Enter own that exit path.
+/**
+ * containTab is Tab containment: keep Tab cycling within the document instead of escaping to the browser chrome.
+ * Only the edges are intercepted (last stop wrapping to first, first back to last);
+ * interior Tabs stay native, so within-page traversal is exactly the browser's own.
+ * Focus outside the tab order (a text-entry dive into a tabindex=-1 input) is left alone:
+ * Escape/Enter own that exit path.
+ */
 export function containTab(e: KeyboardEvent, doc: Document): void {
 	if (e.key !== "Tab") return;
 	const stops = deepTabbables(doc.body);
@@ -63,14 +66,17 @@ export function containTab(e: KeyboardEvent, doc: Document): void {
 	(e.shiftKey ? last : first).focus();
 }
 
-// Documents where containment is already installed.  A WeakSet (not a flag on
-// the document) so tests with fresh jsdom documents never see stale state.
+/**
+ * installed is the documents where containment is already installed.
+ * A WeakSet (not a flag on the document) so tests with fresh jsdom documents never see stale state.
+ */
 const installed = new WeakSet<Document>();
 
-// Install Tab containment on a document: the one call a page shell makes to
-// get the kit's default.  Idempotent per document (repeat calls add nothing and
-// return false); the listener lives for the document's life, matching the
-// shells that call it (they never uninstall themselves either).
+/**
+ * installTabContainment installs Tab containment on a document: the one call a page shell makes to get the kit's default.
+ * Idempotent per document (repeat calls add nothing and return false);
+ * the listener lives for the document's life, matching the shells that call it (they never uninstall themselves either).
+ */
 export function installTabContainment(doc: Document): boolean {
 	if (installed.has(doc)) return false;
 	installed.add(doc);