Keyboard
Complete keyboard system with custom keyboards and global integration for touch-friendly input
Overview
The Keyboard component system provides a comprehensive solution for on-screen keyboards in FoodEase applications. It includes three specialized keyboard types and a global keyboard system that automatically shows/hides based on input focus.
Installation
Install the component
npx shadcn@latest add https://foodease-dev-registry.cap.reachcinema.io/r/v1/keyboard.jsonThis will install:
- Custom keyboards (Pin, Amount, Default)
- Global keyboard system
- All required dependencies and components
Import the keyboard CSS
Add the keyboard styles to your root layout or global CSS:
import "./keyboard.css"Set up the global keyboard (optional)
If you want automatic keyboard display on input focus, add the AppKeyboard component to your root layout:
import { AppKeyboard } from "@/components/ui/global-keyboard/AppKeyboard"
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<AppKeyboard />
</body>
</html>
)
}Add keyboard images
The keyboard components require backspace and enter key images. Create a public/images folder and add these images:
bksp.png- Backspace iconenter.png- Enter icon
You can download the default images here:
Or use your own custom icons (recommended size: 24x24px to 32x32px).
Usage
Custom Keyboards (Standalone)
Use custom keyboards directly in your components without the global system.
Pin Keyboard
A numeric keyboard with masked input for PIN entry.
import { PinKeyboard } from "@/components/ui/custom-keyboard/PinKeyboard"
export default function LoginForm() {
const keyboardRef = useRef<any>()
const handleChange = (pin: string) => {
console.log("PIN changed:", pin)
}
const handleKeyPress = (button: string) => {
if (button === "{enter}") {
const currentPin = keyboardRef.current?.getInnerValue()
console.log("Submitted PIN:", currentPin)
}
}
return (
<PinKeyboard
ref={keyboardRef}
withInput
initValue=""
handleChange={handleChange}
handleKeyPress={handleKeyPress}
placeholder="Enter PIN"
inputClass="w-full"
keyboardClass="custom-theme"
/>
)
}Props:
withInput(boolean) - Show the password input field above keyboard. Default:falseinitValue(string) - Initial value for the keyboardhandleChange(function) -(input: string) => void- Called when value changeshandleKeyPress(function) -(button: string) => void- Called when a key is pressed (use for enter key)handleChangeAll(function) -(inputs: Record<string, string>) => void- Called with all input statesplaceholder(string) - Placeholder for input field (whenwithInputis true)inputClass(string) - CSS class for input fieldkeyboardClass(string) - CSS class for keyboard wrapper- All props from
react-simple-keyboardoptions
Amount Keyboard
A numeric keyboard with decimal support for monetary input. Only allows numbers and one decimal point.
import { AmountKeyboard } from "@/components/ui/custom-keyboard/AmountKeyboard"
export default function PaymentForm() {
const keyboardRef = useRef<any>()
const handleChange = (amount: string) => {
console.log("Amount:", amount)
}
const handleKeyPress = (button: string, amount: string | number) => {
if (button === "{enter}") {
console.log("Submit amount:", amount)
}
}
return (
<AmountKeyboard
ref={keyboardRef}
withInput
initValue=""
handleChange={handleChange}
handleKeyPress={handleKeyPress}
placeholder="0.00"
inputClass="text-right text-2xl"
keyboardClass=""
/>
)
}Props:
withInput(boolean) - Show the input field above keyboard. Default:falseinitValue(string) - Initial value for the keyboardhandleChange(function) -(input: string) => void- Called when value changeshandleKeyPress(function) -(button: string, amount: string | number) => void- Called when a key is pressedhandleChangeAll(function) -(inputs: Record<string, string>) => void- Called with all input statesplaceholder(string) - Placeholder for input field (whenwithInputis true)inputClass(string) - CSS class for input fieldkeyboardClass(string) - CSS class for keyboard wrapper- All props from
react-simple-keyboardoptions
Default Keyboard
A full QWERTY keyboard with shift and caps lock support.
import { DefaultKeyboard } from "@/components/ui/custom-keyboard/DefaultKeyboard"
export default function SearchForm() {
const keyboardRef = useRef<any>()
const handleChange = (text: string) => {
console.log("Text:", text)
}
const handleKeyPress = (button: string) => {
if (button === "{enter}") {
const query = keyboardRef.current?.getInnerValue()
console.log("Search:", query)
}
}
return (
<DefaultKeyboard
ref={keyboardRef}
withInput
initValue=""
handleChange={handleChange}
handleKeyPress={handleKeyPress}
placeholder="Search items..."
inputClass="w-full"
keyboardClass=""
/>
)
}Props:
withInput(boolean) - Show the input field above keyboard. Default:falseinitValue(string) - Initial value for the keyboardhandleChange(function) -(input: string) => void- Called when value changeshandleKeyPress(function) -(button: string, callback?: Function) => void- Called when a key is pressedhandleChangeAll(function) -(inputs: Record<string, string>) => void- Called with all input statesplaceholder(string) - Placeholder for input field (whenwithInputis true)inputClass(string) - CSS class for input fieldkeyboardClass(string) - CSS class for keyboard wrapper- All props from
react-simple-keyboardoptions
Global Keyboard System
The global keyboard automatically shows when inputs are focused and supports data attributes for configuration.
Basic Usage
<input
type="text"
placeholder="Name"
data-keyboard-mode="default"
/>
<input
type="text"
placeholder="Amount"
data-keyboard-mode="amount"
/>
<input
type="password"
placeholder="PIN"
data-keyboard-mode="pin"
/>Data Attributes
data-keyboard-mode: Keyboard type ("default"|"pin"|"amount")data-field-id: Unique identifier for the field (used with keyboard bus)
Keyboard Bus
The keyboard bus allows components to listen for keyboard events:
import keyboardBus from "@/components/ui/global-keyboard/keyboardBus"
useEffect(() => {
const unsubscribe = keyboardBus.subscribe((data) => {
console.log("Field:", data.fieldId)
console.log("Value:", data.value)
})
return unsubscribe
}, [])Disabling Global Keyboard
To disable the global keyboard for specific inputs:
<input
type="text"
readOnly // Prevents keyboard from showing
/>
<input
type="text"
disabled // Prevents keyboard from showing
/>
<input
type="checkbox" // Checkbox/radio automatically skipped
/>Keyboard Modal
The keyboard appears in a draggable, resizable modal that can be customized.
Modal Features
- Draggable: Drag from the header handle
- Resizable: Resize from edges and corners
- Responsive: Adapts to different screen sizes
- Overlay: Optional backdrop overlay
Customizing Modal
The modal's appearance and behavior can be customized in KeyboardModal.tsx:
const MIN_DIMS = {
default: {
width: window.innerWidth * 0.65,
height: window.innerHeight * 0.45,
},
pin: {
width: 400,
height: 380,
},
amount: {
width: 380,
height: 180,
},
}useKeyboardInput Hook (for Controlled Components)
The useKeyboardInput hook allows controlled React components to receive input from the global on-screen keyboard system. This is essential for custom or complex UI elements (comboboxes, selects, custom inputs, etc.) that do not use native <input> or <textarea> elements, or when you want to synchronize keyboard input with your own state eg within a form.
Usage
import { useKeyboardInput } from "@/hooks/useKeyboardInput"
const [value, setValue] = useState("");
useKeyboardInput("my-field-id", setValue);
// ...
<YourCustomInput
value={value}
onChange={setValue}
data-field-id="my-field-id"
data-keyboard-mode="default"
/>How it works:
- The hook subscribes to the keyboard bus for the given
fieldId. - When the global keyboard emits a value for that field, your
setValueis called. - You should set
data-field-idon your input or component to match thefieldIdyou pass to the hook.
When to use:
- For any controlled component that should receive input from the on-screen keyboard.
- For custom select, combobox, or masked input components.
API:
useKeyboardInput(fieldId: string, setValue: (v: string) => void): voidAdvanced Usage
Custom Keyboard Implementation
Create your own keyboard variant by extending the base pattern:
import Keyboard from "react-simple-keyboard"
import { Input } from "@/components/ui/input"
import { useState, useRef, forwardRef, useImperativeHandle } from "react"
export const CustomKeyboard = forwardRef((props, ref) => {
const [value, setValue] = useState("")
const keyboardRef = useRef<any>()
useImperativeHandle(ref, () => ({
resetInnerValue: (newValue = "") => {
setValue(newValue)
keyboardRef.current?.setInput(newValue)
},
getInnerValue: () => value,
}))
const handleKeyPress = (button: string) => {
if (button === "{enter}") {
props.handleKeyPress?.(button)
}
}
const handleChange = (input: string) => {
setValue(input)
props.handleChange?.(input)
}
return (
<div>
{props.withInput && (
<Input value={value} readOnly placeholder={props.placeholder} />
)}
<Keyboard
keyboardRef={(r) => (keyboardRef.current = r)}
onChange={handleChange}
onKeyPress={handleKeyPress}
layout={{
default: ["1 2 3", "4 5 6", "7 8 9", "{bksp} 0 {enter}"],
}}
/>
</div>
)
})Integration with Forms
import { useForm } from "react-hook-form"
import { AmountKeyboard } from "@/components/ui/custom-keyboard/AmountKeyboard"
import { useRef } from "react"
export default function CheckoutForm() {
const { register, setValue, watch } = useForm()
const keyboardRef = useRef<any>()
const handleChange = (value: string) => {
setValue("amount", value)
}
const handleKeyPress = (button: string, amount: string | number) => {
if (button === "{enter}") {
console.log("Submit amount:", amount)
}
}
return (
<form>
<AmountKeyboard
ref={keyboardRef}
withInput
initValue=""
handleChange={handleChange}
handleKeyPress={handleKeyPress}
placeholder="0.00"
/>
</form>
)
}API Reference
PinKeyboard
| Prop | Type | Description |
|---|---|---|
withInput | boolean | Show password input field above keyboard. Default: false |
initValue | string | Initial value |
handleChange | (input: string) => void | Called when value changes |
handleKeyPress | (button: string) => void | Called when key is pressed |
handleChangeAll | (inputs: Record<string, string>) => void | Called with all input states |
placeholder | string | Input placeholder |
inputClass | string | ClassName | CSS class for input |
keyboardClass | string | ClassName | CSS class for keyboard |
AmountKeyboard
| Prop | Type | Description |
|---|---|---|
withInput | boolean | Show input field above keyboard. Default: false |
initValue | string | Initial value |
handleChange | (input: string) => void | Called when value changes |
handleKeyPress | (button: string, amount: string | number) => void | Called when key is pressed |
handleChangeAll | (inputs: Record<string, string>) => void | Called with all input states |
placeholder | string | Input placeholder |
inputClass | string | ClassName | CSS class for input |
keyboardClass | string | ClassName | CSS class for keyboard |
DefaultKeyboard
| Prop | Type | Description |
|---|---|---|
withInput | boolean | Show input field above keyboard. Default: false |
initValue | string | Initial value |
handleChange | (input: string) => void | Called when value changes |
handleKeyPress | (button: string, callback?: Function) => void | Called when key is pressed |
handleChangeAll | (inputs: Record<string, string>) => void | Called with all input states |
placeholder | string | Input placeholder |
inputClass | string | ClassName | CSS class for input |
keyboardClass | string | ClassName | CSS class for keyboard |
CustomKeyBoard (Wrapper)
| Prop | Type | Description |
|---|---|---|
mode | "pin" | "amount" | "base" | Keyboard type |
keyboardProps | PinKeyboardProps | AmountKeyboardProps | BaseKeyboardProps | Props for selected keyboard |
Ref Methods
All keyboards expose these methods via ref:
interface KeyboardRef {
resetInnerValue: (value?: string) => void
getInnerValue: () => string
}Dependencies
The keyboard system requires:
react-simple-keyboard- Core keyboard functionalityreact-rnd- Draggable and resizable modallucide-react- Icons for password visibility toggle@radix-ui/react-slot- Button component foundationclass-variance-authority- Button variants
Examples
POS Terminal with Amount Entry
"use client"
import { useState, useRef } from "react"
import { AmountKeyboard } from "@/components/ui/custom-keyboard/AmountKeyboard"
import { Button } from "@/components/ui/button"
export default function POSTerminal() {
const [amount, setAmount] = useState("")
const keyboardRef = useRef<any>()
const handleChange = (value: string) => {
setAmount(value)
}
const handlePayment = () => {
console.log("Processing payment:", amount)
}
const handleKeyPress = (button: string, amountValue: string | number) => {
if (button === "{enter}") {
handlePayment()
}
}
return (
<div className="flex flex-col gap-4 p-4">
<h2>Enter Payment Amount</h2>
<AmountKeyboard
ref={keyboardRef}
withInput
initValue=""
handleChange={handleChange}
handleKeyPress={handleKeyPress}
placeholder="0.00"
inputClass="text-2xl text-right"
/>
<Button onClick={handlePayment}>
Process ${amount || "0.00"}
</Button>
</div>
)
}Login Form with PIN
"use client"
import { useState, useRef } from "react"
import { PinKeyboard } from "@/components/ui/custom-keyboard/PinKeyboard"
import { Button } from "@/components/ui/button"
export default function LoginForm() {
const [pin, setPin] = useState("")
const keyboardRef = useRef<any>()
const handleChange = (value: string) => {
setPin(value)
}
const handleKeyPress = (button: string) => {
if (button === "{enter}") {
const pinValue = keyboardRef.current?.getInnerValue()
if (pinValue?.length === 4) {
console.log("Logging in with PIN:", pinValue)
}
}
}
return (
<div className="flex flex-col items-center gap-4 p-8">
<h2>Enter Your PIN</h2>
<PinKeyboard
ref={keyboardRef}
withInput
initValue=""
handleChange={handleChange}
handleKeyPress={handleKeyPress}
placeholder="••••"
inputClass="text-center text-2xl tracking-widest"
/>
<Button onClick={() => console.log("Login")}>Login</Button>
</div>
)
}Global Keyboard with Multiple Fields
"use client"
export default function CustomerForm() {
return (
<form className="flex flex-col gap-4 p-4">
<input
type="text"
placeholder="Customer Name"
data-keyboard-mode="default"
data-field-id="customer-name"
className="p-2 border rounded"
/>
<input
type="text"
placeholder="Phone Number"
data-keyboard-mode="default"
data-field-id="phone"
className="p-2 border rounded"
/>
<input
type="text"
placeholder="Order Amount"
data-keyboard-mode="amount"
data-field-id="amount"
className="p-2 border rounded"
/>
<button type="submit">Submit Order</button>
</form>
)
}Notes
- The global keyboard system is primarily designed for touch-screen devices
- Keyboard input is automatically synchronized with native input changes
- The modal can be dragged and resized for better screen management
- Keyboards support both controlled and uncontrolled modes
- Radio and checkbox inputs are automatically excluded from the global keyboard