FoodEase

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.json

This 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:

app/layout.tsx
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:

app/layout.tsx
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 icon
  • enter.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.

Current PIN: (empty)
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: false
  • initValue (string) - Initial value for the keyboard
  • handleChange (function) - (input: string) => void - Called when value changes
  • handleKeyPress (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 states
  • placeholder (string) - Placeholder for input field (when withInput is true)
  • inputClass (string) - CSS class for input field
  • keyboardClass (string) - CSS class for keyboard wrapper
  • All props from react-simple-keyboard options

Amount Keyboard

A numeric keyboard with decimal support for monetary input. Only allows numbers and one decimal point.

Amount: ₦0
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: false
  • initValue (string) - Initial value for the keyboard
  • handleChange (function) - (input: string) => void - Called when value changes
  • handleKeyPress (function) - (button: string, amount: string | number) => void - Called when a key is pressed
  • handleChangeAll (function) - (inputs: Record<string, string>) => void - Called with all input states
  • placeholder (string) - Placeholder for input field (when withInput is true)
  • inputClass (string) - CSS class for input field
  • keyboardClass (string) - CSS class for keyboard wrapper
  • All props from react-simple-keyboard options

Default Keyboard

A full QWERTY keyboard with shift and caps lock support.

Text: (empty)
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: false
  • initValue (string) - Initial value for the keyboard
  • handleChange (function) - (input: string) => void - Called when value changes
  • handleKeyPress (function) - (button: string, callback?: Function) => void - Called when a key is pressed
  • handleChangeAll (function) - (inputs: Record<string, string>) => void - Called with all input states
  • placeholder (string) - Placeholder for input field (when withInput is true)
  • inputClass (string) - CSS class for input field
  • keyboardClass (string) - CSS class for keyboard wrapper
  • All props from react-simple-keyboard options

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.

  • 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 setValue is called.
  • You should set data-field-id on your input or component to match the fieldId you 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): void

Advanced 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

PropTypeDescription
withInputbooleanShow password input field above keyboard. Default: false
initValuestringInitial value
handleChange(input: string) => voidCalled when value changes
handleKeyPress(button: string) => voidCalled when key is pressed
handleChangeAll(inputs: Record<string, string>) => voidCalled with all input states
placeholderstringInput placeholder
inputClassstring | ClassNameCSS class for input
keyboardClassstring | ClassNameCSS class for keyboard

AmountKeyboard

PropTypeDescription
withInputbooleanShow input field above keyboard. Default: false
initValuestringInitial value
handleChange(input: string) => voidCalled when value changes
handleKeyPress(button: string, amount: string | number) => voidCalled when key is pressed
handleChangeAll(inputs: Record<string, string>) => voidCalled with all input states
placeholderstringInput placeholder
inputClassstring | ClassNameCSS class for input
keyboardClassstring | ClassNameCSS class for keyboard

DefaultKeyboard

PropTypeDescription
withInputbooleanShow input field above keyboard. Default: false
initValuestringInitial value
handleChange(input: string) => voidCalled when value changes
handleKeyPress(button: string, callback?: Function) => voidCalled when key is pressed
handleChangeAll(inputs: Record<string, string>) => voidCalled with all input states
placeholderstringInput placeholder
inputClassstring | ClassNameCSS class for input
keyboardClassstring | ClassNameCSS class for keyboard

CustomKeyBoard (Wrapper)

PropTypeDescription
mode"pin" | "amount" | "base"Keyboard type
keyboardPropsPinKeyboardProps | AmountKeyboardProps | BaseKeyboardPropsProps 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 functionality
  • react-rnd - Draggable and resizable modal
  • lucide-react - Icons for password visibility toggle
  • @radix-ui/react-slot - Button component foundation
  • class-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

On this page