new updates
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
"use client"
|
||||
|
||||
// Reusable capture box for USB/Bluetooth barcode & RFID scanners. Those devices are plain HID
|
||||
// keyboards: they "type" the scanned code into whatever input is focused, then send Enter — so
|
||||
// there is no camera/getUserMedia code here, just a text input that reacts to Enter (and to
|
||||
// losing focus with content still in it, in case a scanner is configured without a trailing
|
||||
// Enter) and clears itself after each read. It stays auto-focused so the next scan always lands
|
||||
// here rather than wherever the cursor last was in a large form.
|
||||
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { ScanBarcode } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
|
||||
export interface ScanInputProps {
|
||||
/** Fired once per scan with the trimmed code. The input is already cleared by the time this runs. */
|
||||
onScan: (code: string) => void
|
||||
label?: string
|
||||
/** Helper text under the input, e.g. "Scan item or lot barcode". */
|
||||
hint?: string
|
||||
placeholder?: string
|
||||
disabled?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function ScanInput({
|
||||
onScan,
|
||||
label = "Scan",
|
||||
hint = "Scan item or lot barcode",
|
||||
placeholder = "Scan or type a barcode, then press Enter",
|
||||
disabled = false,
|
||||
className,
|
||||
}: ScanInputProps) {
|
||||
const [value, setValue] = useState("")
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
// Refocus whenever the field becomes usable again (e.g. after being briefly disabled while a
|
||||
// lookup for the previous scan was in flight) so a scanner's next read always lands here.
|
||||
useEffect(() => {
|
||||
if (!disabled) inputRef.current?.focus()
|
||||
}, [disabled])
|
||||
|
||||
function commit() {
|
||||
const code = value.trim()
|
||||
setValue("")
|
||||
if (!code) return
|
||||
onScan(code)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-2", className)}>
|
||||
<Label className="flex items-center gap-1.5 text-base">
|
||||
<ScanBarcode className="size-4 text-muted-foreground" />
|
||||
{label}
|
||||
</Label>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
placeholder={placeholder}
|
||||
autoComplete="off"
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key !== "Enter") return
|
||||
// A scanner's Enter is just another keystroke to the browser — stop it from also
|
||||
// submitting an enclosing <form> or activating a nearby default button.
|
||||
e.preventDefault()
|
||||
commit()
|
||||
}}
|
||||
onBlur={() => {
|
||||
// Guards a scanner configured without a trailing Enter — but only fires for content
|
||||
// that's actually there, so a plain click-away with an empty box does nothing.
|
||||
if (value.trim()) commit()
|
||||
}}
|
||||
className="h-11 text-base"
|
||||
/>
|
||||
{hint && <p className="text-sm text-muted-foreground">{hint}</p>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user