Skip to main content

Focus stewardship

The doctrine

Every operator module has one steward input — the search bar the operator lives in. On boot the module registers its steward selector; from then on the Kernel takes responsibility for keeping focus there whenever a legitimate reason to move focus ends.

The steward is the safe sink for the keyboard — most importantly, for barcode scans. A misrouted scan (into a customer name, a cart quantity, a notes field) is worse than a missed scan, because the corruption is silent and shows up at reconciliation, not at the moment it happens. Focus stewardship makes that class of error impossible by construction.

Shippedv0.7.87 (Sales) → v0.7.91 (Inventory) — all five module tabs
ProcessesBIKE.L2-0195 (steward focus recovery) · BIKE.L2-0196 (scanner keystroke interception)
Substratesrc/core/focus-steward.ts — loaded once by the shell, alongside the router

How a module opts in

Each module calls the register API once inside its client IIFE:

window.kbRegisterSteward('sales', '#sales-search');

Stewards registered today, one per module tab:

ModuleSteward selector
Sales#sales-search
Service#sv-search
Customers#customers-search
Purchases#pur-search
Inventory#inv-search
Settings(no steward — not a search-driven surface)

The Kernel maintains exactly one steward at a time — the steward for the currently-active module. Tab switches route through window.kbActiveModule(); modules that don't register (Settings) leave the steward unset and every recovery pathway silently no-ops.

Mechanism 1 — steward focus recovery (BIKE.L2-0195)

Four events trigger a refocus back to the active steward:

TriggerDetection
Modal closeMutationObserver watches every .kb-modal for its open class being removed
Print dismisswindow.addEventListener('afterprint', …) on the main document. Sales' popup-window print flow calls window.kbRefocusSteward() directly because afterprint fires on the popup, not the parent
Blur to nothingfocusout followed by focus landing on document.body (not a deliberate click into some other field)
Keyboard idle5 seconds of no keystroke, refocus back to the steward — reclaims the surface after a hands-off pause

Every trigger runs through window.kbRefocusSteward(), which is safe to call liberally — it no-ops if:

  • Any .kb-modal.open is present (modals take precedence)
  • Another visible, typable element already has focus (input / textarea / select / contenteditable, offsetParent !== null). If the operator put focus there, it stays there
  • The active module has no registered steward, or the steward's offsetParent === null (its tab is hidden)
  • Focus is already on the steward

The guard on legitimately-focused other input is load-bearing: without it the 5-second idle timer would steal focus mid-edit from any other field on the page (the Adjust-tax reason input, an inline field in a modal, a notes textarea). That guard is why the doctrine can afford to be aggressive.

Mechanism 2 — scanner keystroke interception (BIKE.L2-0196)

A USB barcode scanner is a keyboard wedge: it types the whole barcode faster than a human can, ending in Enter. The Kernel watches for that shape at capture-phase — a burst of ≥5 characters with ≤50ms between keys, terminated by Enter — and redirects the whole burst into the active steward input regardless of where focus had drifted to.

To prevent false positives when the operator is legitimately typing fast, the interceptor consults an isUserTypingManually() heuristic: if the focused input's current value already ends with the buffered keys, the operator was typing into that field themselves, and the interceptor stays out.

What NOT to do

  • Do not implement focus stewardship as per-module ad-hoc behaviour. The mechanism lives in one Kernel file; every module registers its steward and stops.
  • Do not yank focus while a modal is open, or while another visible input has focus. The skip conditions exist for good reason.
  • Do not add a UI toggle. It is always on. A cashier who could turn it off would forget to turn it back on, and one transaction with it off is one transaction at risk of a corrupted record.
  • Do not extend it to surfaces that aren't search-driven (Settings). The register call is silent if the module doesn't opt in.

See also