busy-office-ui
Menu

Pattern: RF goods receipt

Who uses it: a warehouse floor worker, one-handed and often gloved, holding a scanner instead of a mouse. What "done" looks like: every expected line accounted for — received in full, received partial, or explicitly not received — before the receipt posts. A GR (goods receipt) screen driven by a handheld RF scanner — the scanner types the barcode fast and sends Enter, so the field needs to stay focused and clear itself between scans. Composed from data-scan-input (new opt-in behavior, no new CSS — a plain .bo-input), the existing .bo-quantity at data-density="spacious" for a warehouse-floor large target, and the data table for the receiving log. A visually-hidden aria-live="polite" region (linked via the field's own aria-describedby) announces each successful scan — a screen-reader/low-vision RF user gets non-visual confirmation without it being read for every sighted user. Not for a desk-based receiving clerk keying a paper packing slip. Without a physical scanner there is no barcode to feed the always-focused field, the spacious steppers just waste screen space, and the workflow is really "type these numbers in and check them" — that's a detail form at normal density, not this pattern.

The screen — try it

Shown the way the scanner gets it: an isolated 360×640 document styled by ONLY the rf-essentials profile (roadmap 59.2, Chrome/WebView 108) — not the docs bundle. This is where "separated and used only on RF scanner / mobile / tablet" is actually demonstrated: the profile is what's separated (a build target), not the markup or the components. One screen, not two — this page rendered the same component a second time at desktop width until roadmap 135.1, which is a size no receiving clerk holds. (131.1 removed that duplicate from the other three RF screens and wrongly spared this one, on the belief that its top demo showed something different. It did not: both rendered <ScanToReceive />, and this page said so itself.)

What is in the profile — all 14 of them. Generated from the same array the bundle is compiled from, so this list and what ships cannot disagree. Everything else in the framework is deliberately absent: a scanner runs a few screens, so the profile carries what those screens need and nothing more. A component missing from this list is a scope statement, not a gap.

Same scan-clear-refocus behavior, same data-density="spacious" steppers — initScanInput() and initQuantity() are JS, unaffected by which CSS profile loads (the RF floor research found JS already safe back to Chrome 80; this profile is the CSS half of the ask). Try it: click into the field and type a barcode, then Enter.

Scan feedback — a cue for someone watching the rack

Type anything into the screen above and press Enter: the device's viewport flashes green for ~600ms — the flash is position: fixed, so it covers whatever viewport the screen is running in, which here is the handheld rather than this page. Scan a code starting with REJECT and it flashes red with the reason announced. A scanner user's eyes are on the shelf, not the screen — the flash is readable in peripheral vision, and it is deliberately not the only channel: every capture and every rejection also lands in the data-scan-status live region this page already links. Capture is not validity — data-scan-flash makes the behavior paint ok when the terminator arrives, and your validation calls flashScanResult('error', reason) when a captured code is wrong. Under reduced motion the flash is a static wash; under forced colors it becomes a Highlight frame, because a repainted background would vanish exactly when contrast matters most.

Markup

<input class="bo-input bo-input--code" data-scan-input autofocus
    aria-describedby="scan-status" />
<p id="scan-status" data-scan-status aria-live="polite" class="bo-visually-hidden"></p>

<script type="module">
  import { initScanInput, flashScanResult } from "@busy-office/ui/js";
  initScanInput(); // once — clears + refocuses on Enter, fires bo:scan,
                    // announces "Scanned <value>" in data-scan-status if linked;
                    // with data-scan-flash on the input, capture also paints
                    // the ok flash. Your validation:
                    //   flashScanResult('error', 'Not on this PO')
  document.querySelector('[data-scan-input]')
    .addEventListener('bo:scan', (e) => console.log(e.detail.value));
</script>

Anatomy

  1. Scan field — always focused; a hardware scanner types then presses Enter, which initScanInput() treats as a submit.
  2. Expected vs received list (the demo above logs scans as a flat list; a real screen pre-seeds one row per expected line — barcode, ordered qty, received-so-far — from the PO, and each scan updates its own row instead of appending a new one) — every line with its ordered quantity and a running received count.
  3. Quantity control — spacious density and big steppers: this screen is used one-handed, often gloved.
  4. Confirm bar (not in this demo — the live scan log has no post action) — post the receipt; disabled until at least one line has a quantity.

Data contract

RequestReturns / on failure
POST /receipts/:id/scanthe matched line fragment with its updated count; 404 → an unknown-barcode alert that does NOT steal focus from the scan field
POST /receipts/:idthe posted receipt; 422 for over-receipt beyond tolerance
GET /receipts/:idresume a partially entered receipt — warehouse sessions get interrupted

No scanner? The camera is the fallback, as a recipe. On a consumer phone, add capture="environment" to a file input for photo evidence (damage, labels) — file upload's own camera section spells out the two-input shape, because a lone capture input takes the gallery away, and for live barcode reads feed the BarcodeDetector API's result into the same bo:scan flow where supported — the screen's contract does not change, only the way the string arrives. Deliberately a recipe: camera pipelines are a device capability, not framework CSS.

States

StateWhat ships
Waiting for scanthe field holds focus permanently; every state change must return focus to it
Unknown barcodean alert plus an audible or visible cue — the user is looking at the goods, not the screen
Over-receipta warning with the tolerance stated; block only past the policy limit
Partial receiptthe line shows received/ordered; posting a partial is normal and must not warn
Completea clear terminal state — the next scan starts a NEW receipt rather than silently appending
Session lost / flaky wifithe RECONNECT state: on regaining the server, re-render as the server has it. Session persistence is INFRASTRUCTURE (the reference products sell it as a server), not a screen promise — an unsent scan is retyped, which is honest and cheaper than a silently dropped one that reads as saved
No JSthe scan field, quantity input, and table render as plain, focusable, typable native controls, but the core flow cannot complete: initScanInput() is what listens for the terminator key, clears/refocuses the field, and dispatches bo:scan — without it, pressing Enter after a scan does nothing, so no line ever reaches the log. The +/− quantity steppers also need initQuantity(), though the number input itself stays usable via keyboard/typing

Components used

complexity 3 of 4

↑↓ move Enter open Esc close