fix: resolve mobile typing issues (spacebar, backspace, and text ghosting)#175
Conversation
…side key handling
WalkthroughReplaces IME composition-driven handling in the trackpad input with a unified native onChange flow (using event.inputType/data), synchronous hidden-input reset, explicit handling for Space/Backspace/Enter, refined modifier state transitions, and simplified keyboard logic; also changes key dispatch in InputHandler to press+release for mapped keys including space. Changes
Sequence Diagram(s)sequenceDiagram
participant Mobile as Mobile Keyboard
participant Hidden as HiddenInput
participant UI as TrackpadHandler
participant Remote as RemoteConnection
participant Server as Remote Server
Mobile->>Hidden: user types (native text / composition resolved)
Hidden->>UI: onChange (inputType, data)
UI->>UI: interpret inputType -> (Text / Space / Backspace / Enter / Modifier)
UI->>Hidden: synchronous reset (set value to " " and position caret)
UI->>Remote: send key or text events
Remote->>Server: forward input events
Server-->>Remote: ack/response
Remote-->>UI: delivery confirmation
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/routes/trackpad.tsx`:
- Around line 23-33: The current initializers for sensitivity and invertScroll
(the useState callbacks) read from localStorage and call parseFloat/JSON.parse
without validation, which can throw or yield NaN; update the initializers in
trackpad.tsx to safely parse and validate: wrap JSON.parse in a try/catch and
fall back to false on error for invertScroll, and for sensitivity call
parseFloat then check Number.isFinite (or isNaN) and default to 1.0 if parsing
fails or produces NaN; keep the existing typeof window guard and reference the
sensitivity and invertScroll state initializers by name when making these
changes.
- Around line 155-166: The container's onClick handler (handleContainerClick)
triggers the accessibility lint; replace it with a pointer-based handler and add
keyboard activation so screen-reader/keyboard users can perform the same action.
Specifically, rename/replace handleContainerClick to handleContainerPointer (or
similar) and attach it to the container as onPointerDown/onPointerUp (preserve
the existing guard e.target === e.currentTarget and call focusInput() with
e.preventDefault() as needed), and also add a keyboard handler (onKeyDown) on
the same container that listens for Enter and Space and calls focusInput() when
the event originates from the container; keep the same shallow target check so
behavior remains identical and both pointer and keyboard activations are
supported to satisfy the use-key-with-click-events lint.
- Line 213: Remove the autoFocus JSX attribute from the input in Trackpad
component and instead set focus imperatively after mount: create a ref (e.g.,
inputRef) attached to the input, add a useEffect in the Trackpad component that
calls inputRef.current?.focus() on mount (empty dependency array) to satisfy the
linter, and ensure the ref is typed correctly and checked for null before
calling focus.
In `@src/server/InputHandler.ts`:
- Around line 195-210: Remove the raw key logging and lint-blocking
string/template usages in the input handling block: eliminate console.log calls
that output msg.key, replace template literals like `Processing key: ${msg.key}`
and `Unmapped key: ${msg.key}` with no logging, and simplify bracket/optional
access by using direct keys (e.g., use msg.key and
KEY_MAP[msg.key.toLowerCase()] safely after validation). Keep the same control
flow around KEY_MAP lookup and the actions that call keyboard.pressKey,
keyboard.releaseKey and keyboard.type, but do not log or expose msg.key anywhere
in this function (including the alternative space handling); ensure no
eslint-disable is needed after making these changes.
- Around line 88-90: The catch block in InputHandler that calls
this.handleMessage(pending).catch(...) uses a misleading error label ("Error
processing pending move event"); update that log to accurately reflect the
scroll throttling path by changing the message to something like "Error
processing pending scroll event" (or similar) so the catch in the scroll timer
clearly references scroll-related pending events; locate this within the
InputHandler class where handleMessage is invoked for pending scroll timer
callbacks and replace the error message string in the console.error call.
- Around line 218-221: In the loop inside InputHandler that iterates "for (const
k of msg.keys)" you must validate each entry before calling k.toLowerCase():
check that k is a non-empty string (e.g., typeof k === 'string' and k.length >
0) and skip or handle non-strings/null/undefined rather than calling toLowerCase
on them; then use the validated lowerKey to lookup KEY_MAP[lowerKey]; update the
loop to either continue on invalid entries or coerce safely (String(k)) if
coercion is desired, and ensure any downstream uses expect skipped entries.
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/routes/trackpad.tsx (1)
1-4:⚠️ Potential issue | 🟠 MajorMissing
"use client"directive for NextJS client component.This component uses client-side APIs (
useState,useRef,localStorage, event handlers) and must be marked as a client component in NextJS.Proposed fix
+"use client" + import { BufferBar } from "@/components/Trackpad/Buffer" import type { ModifierState } from "@/types"As per coding guidelines: "Ensure that 'use client' is being used" for NextJS components using client-side features.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/routes/trackpad.tsx` around lines 1 - 4, This file uses client-side hooks and APIs (useState, useRef, localStorage, event handlers) but is missing the Next.js client directive; add the literal "use client" directive as the very first line of the module (before any imports) so the component that references BufferBar, ModifierState, createFileRoute, useRef and useState is treated as a client component.
♻️ Duplicate comments (2)
src/routes/trackpad.tsx (2)
29-33:⚠️ Potential issue | 🟠 Major
JSON.parsecan throw on malformed localStorage data, crashing the component.The
invertScrollinitializer still lacks error handling. If localStorage contains invalid JSON, the component will fail to render.Proposed fix
const [invertScroll] = useState(() => { if (typeof window === 'undefined') return false; const s = localStorage.getItem('rein_invert'); - return s ? JSON.parse(s) : false; + if (!s) return false; + try { + return JSON.parse(s) === true; + } catch { + return false; + } });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/routes/trackpad.tsx` around lines 29 - 33, The useState initializer for invertScroll (const [invertScroll] = useState(() => { ... })) can throw if JSON.parse is fed malformed data from localStorage.getItem('rein_invert'); wrap the parsing in a try/catch and return a safe default (false) on any error: read the raw value with localStorage.getItem('rein_invert'), if falsy return false, otherwise attempt JSON.parse inside try/catch and on catch return false (and optionally clear the bad key) so the component won't crash.
155-167: 🧹 Nitpick | 🔵 TrivialConsider using
onPointerDowninstead of suppressing the lint rule.While the
biome-ignorecomment is acceptable for this use case, replacingonClickwithonPointerDownwould eliminate the need for the suppression entirely and provide slightly better mobile touch responsiveness.Proposed fix
- const handleContainerClick = (e: React.MouseEvent) => { + const handleContainerPointerDown = (e: React.PointerEvent<HTMLDivElement>) => { if (e.target === e.currentTarget) { e.preventDefault(); focusInput(); } }; return ( - // biome-ignore lint/a11y/useKeyWithClickEvents: Layout container delegates focus to hidden input, not an interactive element <div className="flex flex-col h-full overflow-hidden" - onClick={handleContainerClick} + onPointerDown={handleContainerPointerDown} >🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/routes/trackpad.tsx` around lines 155 - 167, The container currently uses onClick with handleContainerClick and a biome-ignore lint suppression; replace the onClick handler with onPointerDown on the same div and update handleContainerClick to accept React.PointerEvent (still checking e.target === e.currentTarget and calling focusInput()), removing the biome-ignore comment; this preserves the current focus-delegation behavior while improving mobile touch responsiveness and eliminating the accessibility lint suppression.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/routes/trackpad.tsx`:
- Around line 10-13: The formatting mismatch is caused by the Route export
block; run the project's configured formatter (Prettier or Biome) and reformat
the file so the export of Route = createFileRoute("/trackpad")({ component:
TrackpadPage }) matches the project's style. Locate the export using the symbols
Route, createFileRoute and TrackpadPage in the trackpad route file, run the
project's format script (npm/yarn pnpm run format or the configured
biome/prettier command), and commit the reformatted file to resolve the CI
failure.
- Around line 100-106: The Enter-key branch resets the hidden input's value but
omits the same cursor repositioning used by resetInput(), which can leave the
caret in the wrong place on some mobile keyboards; update the Enter key handling
in the key === 'enter' block to also call
hiddenInputRef.current.setSelectionRange(1, 1) (after setting
hiddenInputRef.current.value = ' ') so its behavior matches resetInput(),
ensuring consistent cursor placement; keep the existing send({ type: 'key', key:
'enter' }) call and null-check for hiddenInputRef.current.
- Around line 205-207: The onBlur handler currently calls setTimeout which may
fire after unmount and try to focus hiddenInputRef.current; change this by
storing the timeout id (e.g., timeoutIdRef) when calling setTimeout in the
onBlur handler and clear it in a useEffect cleanup (or clear it before setting a
new timeout) so the pending timer is cancelled on unmount; alternatively keep a
mountedRef checked inside the timeout callback before calling
hiddenInputRef.current?.focus(); update the onBlur, add the timeoutIdRef and a
useEffect cleanup that clears the timer to prevent focusing a null ref.
---
Outside diff comments:
In `@src/routes/trackpad.tsx`:
- Around line 1-4: This file uses client-side hooks and APIs (useState, useRef,
localStorage, event handlers) but is missing the Next.js client directive; add
the literal "use client" directive as the very first line of the module (before
any imports) so the component that references BufferBar, ModifierState,
createFileRoute, useRef and useState is treated as a client component.
---
Duplicate comments:
In `@src/routes/trackpad.tsx`:
- Around line 29-33: The useState initializer for invertScroll (const
[invertScroll] = useState(() => { ... })) can throw if JSON.parse is fed
malformed data from localStorage.getItem('rein_invert'); wrap the parsing in a
try/catch and return a safe default (false) on any error: read the raw value
with localStorage.getItem('rein_invert'), if falsy return false, otherwise
attempt JSON.parse inside try/catch and on catch return false (and optionally
clear the bad key) so the component won't crash.
- Around line 155-167: The container currently uses onClick with
handleContainerClick and a biome-ignore lint suppression; replace the onClick
handler with onPointerDown on the same div and update handleContainerClick to
accept React.PointerEvent (still checking e.target === e.currentTarget and
calling focusInput()), removing the biome-ignore comment; this preserves the
current focus-delegation behavior while improving mobile touch responsiveness
and eliminating the accessibility lint suppression.
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (3)
src/routes/trackpad.tsx (3)
104-111:⚠️ Potential issue | 🟡 MinorInconsistent input reset: missing
setSelectionRange(1, 1)for Enter key.The
resetInput()helper at lines 61-66 sets bothvalue = " "andsetSelectionRange(1, 1), but the Enter fallback only sets the value. This inconsistency may cause cursor positioning issues on some mobile keyboards.Align with resetInput behavior
if (key === "enter") { send({ type: "key", key: "enter" }) if (hiddenInputRef.current) { hiddenInputRef.current.value = " " + hiddenInputRef.current.setSelectionRange(1, 1) } return }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/routes/trackpad.tsx` around lines 104 - 111, The Enter-key fallback in the key handler mirrors resetInput() by setting hiddenInputRef.current.value = " " but omits positioning the caret; update the Enter branch (the block that calls send({ type: "key", key: "enter" })) to, when hiddenInputRef.current exists, also call hiddenInputRef.current.setSelectionRange(1, 1) so behavior matches resetInput() and avoids mobile cursor issues.
29-33:⚠️ Potential issue | 🟡 Minor
JSON.parsecan throw on malformed localStorage data.If
rein_invertcontains invalid JSON, the component will crash during initialization.Wrap in try/catch
const [invertScroll] = useState(() => { if (typeof window === "undefined") return false const s = localStorage.getItem("rein_invert") - return s ? JSON.parse(s) : false + if (!s) return false + try { + return JSON.parse(s) === true + } catch { + return false + } })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/routes/trackpad.tsx` around lines 29 - 33, The initializer for the invertScroll state calls JSON.parse on localStorage data which can throw on malformed content; update the useState initializer (the function that reads localStorage.getItem("rein_invert") used to set invertScroll) to wrap the JSON.parse call in a try/catch, returning false as a safe fallback when parsing fails (and optionally logging the error), so the component won't crash on invalid stored data.
215-217: 🧹 Nitpick | 🔵 Trivial
setTimeoutinonBlurcan fire after component unmount.If the component unmounts while the timeout is pending,
hiddenInputRef.current?.focus()will safely no-op due to optional chaining, but it's better practice to clear the timeout on unmount.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/routes/trackpad.tsx` around lines 215 - 217, The onBlur handler currently calls setTimeout(() => hiddenInputRef.current?.focus(), 10) which can fire after the component unmounts; capture the timeout id (from setTimeout) into a ref (e.g., blurTimeoutRef) when scheduling, clear any existing timeout before setting a new one, and clearTimeout(blurTimeoutRef.current) in a useEffect cleanup (or componentWillUnmount equivalent) so the pending focus callback is canceled on unmount; update the onBlur handler and add the cleanup logic referencing hiddenInputRef and the new blurTimeoutRef.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/server/InputHandler.ts`:
- Around line 199-204: The else-if branch checking msg.key === " " ||
msg.key?.toLowerCase() === "space" contains an unreachable clause; remove the
redundant msg.key?.toLowerCase() === "space" check so the branch only handles
the literal space character (msg.key === " "), or eliminate the branch entirely
if KEY_MAP lookup already handles "space"; update the block around KEY_MAP,
msg.key, and the keyboard.pressKey/keyboard.releaseKey calls in InputHandler
(where KEY_MAP.space and nutKey are used) so only one clear path handles the
space key.
---
Duplicate comments:
In `@src/routes/trackpad.tsx`:
- Around line 104-111: The Enter-key fallback in the key handler mirrors
resetInput() by setting hiddenInputRef.current.value = " " but omits positioning
the caret; update the Enter branch (the block that calls send({ type: "key",
key: "enter" })) to, when hiddenInputRef.current exists, also call
hiddenInputRef.current.setSelectionRange(1, 1) so behavior matches resetInput()
and avoids mobile cursor issues.
- Around line 29-33: The initializer for the invertScroll state calls JSON.parse
on localStorage data which can throw on malformed content; update the useState
initializer (the function that reads localStorage.getItem("rein_invert") used to
set invertScroll) to wrap the JSON.parse call in a try/catch, returning false as
a safe fallback when parsing fails (and optionally logging the error), so the
component won't crash on invalid stored data.
- Around line 215-217: The onBlur handler currently calls setTimeout(() =>
hiddenInputRef.current?.focus(), 10) which can fire after the component
unmounts; capture the timeout id (from setTimeout) into a ref (e.g.,
blurTimeoutRef) when scheduling, clear any existing timeout before setting a new
one, and clearTimeout(blurTimeoutRef.current) in a useEffect cleanup (or
componentWillUnmount equivalent) so the pending focus callback is canceled on
unmount; update the onBlur handler and add the cleanup logic referencing
hiddenInputRef and the new blurTimeoutRef.
Addressed Issues:
Fixes #174
Description
This PR implements a complete rewrite of the mobile typing system to resolve critical regressions on mobile devices.
###KEY CHANGES
Synchronous Volatile Input Strategy: Replaced the previous complex diffing logic with a "Volatile" approach. The hidden input field is now forcefully reset to a single space placeholder (" ") immediately and synchronously after every keystroke. This prevents mobile keyboards from building a prediction buffer, which was the root cause of "ghost text" and character duplication.
Precise Input Mapping: Utilizes nativeEvent.inputType to explicitly distinguish between character insertion, backspace (deleteContentBackward), and line breaks.
Robust Server-Side Simulation: Updated the NutJS InputHandler to simulate the Space key as a physical hardware key press rather than a typed character, ensuring compatibility with applications that have strict keyboard hooks.
Browser Interface Optimization: Added aggressive HTML hints (inputMode="text", autoCorrect="off", etc.) to minimize mobile browser interference with the typing flow.
Functional Verification
Basic Gestures
One-finger tap: Verified as Left Click.
Two-finger tap: Verified as Right Click.
Click and drag: Verified selection behavior.
Pinch to zoom: Verified zoom functionality (if applicable).
Modes & Settings
Cursor mode: Cursor moves smoothly and accurately.
Scroll mode: Page scrolls as expected.
Sensitivity: Verified changes in cursor speed/sensitivity settings.
Invert Scrolling: Verified scroll direction toggles correctly.
Advanced Input
Key combinations: Verified "hold" behavior for modifiers (e.g., Ctrl+C) and held keys are shown in buffer.
Keyboard input: Verified Space, Backspace, and Enter keys work correctly.
Glide typing: Verified path drawing and text output.
Voice input: Verified speech-to-text functionality.
Backspace doesn't send the previous input.
Any other gesture or input behavior introduced:
Checklist
My PR addresses a single issue, fixes a single bug or makes a single improvement.
My code follows the project's code style and conventions
I have performed a self-review of my own code
I have commented my code, particularly in hard-to-understand areas
If applicable, I have made corresponding changes or additions to the documentation
If applicable, I have made corresponding changes or additions to tests
My changes generate no new warnings or errors
I have joined the and I will share a link to this PR with the project maintainers there
I have read the contributing guidlines.
Once I submit my PR, CodeRabbit AI will automatically review it and I will address CodeRabbit's comments.
Incase of UI change I've added a demo video.
Summary by CodeRabbit
Bug Fixes
Refactor