スキャン入力ScanInputExperimental

Barcode / QR scan field: a scan gun (or hand entry) types a code then Enter; it fires `onScan`, announces the result, auto-clears and re-focuses for the next scan, and keeps an optional running feed. For POS, stocktake, returns and goods-receiving — scan → match → tick up.

プレビュー

ハンディスキャナで読み取るか、コードを入力して Enter。例: 4901234567894 / 0000(対象外)

スキャン履歴

—

  • 有機ほうじ茶 500ml検品 0 / 発注 24-24 不足
  • 玄米おにぎり検品 0 / 発注 12-12 不足
  • 豆乳 1L検品 0 / 発注 6-6 不足

状態とバリエーション

読み取りの作法は現場によって変わります。読み上げに割り込ませるか(assertive)、先頭のアイコンをカメラを開くボタンにするか(onScannerOpen)、読んだコードを欄に残すか(clearOnScan と retainFocus)の3つが、その違いを吸収する口です。

取り違えを見逃せない場面

既定では結果を polite に読み上げるので、連続で読ませても互いに割り込みません。投薬や部品の照合のように、一致しないことが事故になる場面では assertive を付け、alert として割り込ませます。

一致する例: 4901234567894 / 一致しない例: 0000

照合できた回数: 0

先頭のアイコンをカメラにする

onScannerOpen を渡すと、飾りだった先頭のバーコードが押せるボタンになります。カメラの画面を開き、読めたら action.commit(code) を呼ぶと、ハンディスキャナが打ったときと同じ道を通ります。名前は scannerLabel です。この見本ではカメラの代わりに決め打ちのコードを渡しています。

左のアイコンはカメラの代わりです。押すと決め打ちのコードを読んだことにします。

    読んだコードを欄に残す

    既定は「読んだら消して、そのまま次を待つ」です。何十点も連続で読む現場ではこれが正しいのですが、1点を引き当てて内容を確かめる画面では逆に困ります。clearOnScan={false} と retainFocus={false} にすると、読んだコードが残り、カーソルも欄に居座りません。

    台帳にある例: A-1042 / A-1187

    Props

    表は横にスクロールできます
    プロパティ型初期値説明
    onScan(code: string) => ScanResult | void-Fires when a code is committed (scan gun types then Enter, or manual Enter). Return { ok, message } to drive the announcement, tone and feed.
    labelReactNode-Accessible label rendered above the field.
    descriptionReactNode-Helper text rendered under the field.
    retainFocusboolean-Re-focus the field after each scan so a scan gun fires continuously. Default true.
    clearOnScanboolean-Clear the field after each scan. Default true.
    onScannerOpen(action: ScanInputAction) => void-Turns the leading barcode icon into a scan action. Open a scanner UI and call action.commit(code) to commit the read.
    scannerLabelstring-Accessible label and tooltip for the barcode scan action.
    lockMsnumber-Ignore repeat commits within this many ms (scan guns can double-fire). Default 150.
    showFeedboolean-Render a running feed of recent scans (newest first). Default false.
    feedLimitnumber-Max feed entries retained. Default 8.
    iconReactNode-Leading adornment. Defaults to a barcode icon; pass null to hide.
    assertiveboolean-Announce results assertively (role="alert" + aria-live="assertive") instead of politely — for safety-critical scanning where a mismatch must interrupt. Default false.
    classNamestring-Additional CSS class names on the field wrapper.

    Usage

    "use client";
    
    import * as React from "react";
    import { ScanInput, type ScanResult } from "@gunjo/ui";
    
    const ORDER_LINES = [
      { jan: "4901234567894", name: "アスコルビン酸 500mg" },
      { jan: "4901234567900", name: "コットンパッド 80枚" },
    ];
    
    export function ReceivingScanField() {
      const [counts, setCounts] = React.useState<Record<string, number>>({});
    
      function handleScan(code: string): ScanResult {
        const line = ORDER_LINES.find((l) => l.jan === code);
        if (!line) {
          return { ok: false, message: "発注に無い商品です(" + code + ")" };
        }
        setCounts((prev) => ({ ...prev, [line.jan]: (prev[line.jan] ?? 0) + 1 }));
        return { ok: true, message: line.name + " を1点 検品" };
      }
    
      return (
        <div className="flex w-full max-w-sm flex-col gap-3">
          <ScanInput
            label="バーコード / JAN をスキャン"
            placeholder="コードを入力して Enter"
            inputMode="numeric"
            onScan={handleScan}
            showFeed
          />
          <p className="text-sm text-muted-foreground">
            検品済み: {Object.values(counts).reduce((a, b) => a + b, 0)} 点
          </p>
        </div>
      );
    }

    使用コンポーネント