Skip to content

Report hardware keyboard keys in Android TextInput onKeyPress - #59120

Open
YevheniiKotyrlo wants to merge 5 commits into
react:mainfrom
YevheniiKotyrlo:fix/android-hardware-key-press
Open

YevheniiKotyrlo wants to merge 5 commits into
react:mainfrom
YevheniiKotyrlo:fix/android-hardware-key-press

Conversation

@YevheniiKotyrlo

@YevheniiKotyrlo YevheniiKotyrlo commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Summary:

With a keyboard attached to an Android phone or tablet, TextInput's onKeyPress misses keys, and which ones depends on the keyboard app. With Samsung Keyboard, the default on Samsung devices, nothing typed on the keyboard is reported: not letters, not Enter, not Backspace. With Gboard, letters and Backspace are reported, but arrows, Home, End and every other key are not. A code field that steps back on Backspace, or a suggestion list moved with the arrow keys, works on the web and not on Android. The docs record the limitation ("on Android only the inputs from soft keyboard are handled"), and #19507 met the same gap on the soft numeric keypad, which #29046 closed for digits only.

onKeyPress only reported what an input method types through the InputConnection. A hardware key reaches ReactEditText as a key event instead: directly with Samsung Keyboard, and through InputConnection.sendKeyEvent with Gboard. This reports every key event from ReactEditText.onKeyDown, where both arrive, by its KeyboardEvent.key value, with altKey, ctrlKey, metaKey and shiftKey, as onKeyPress reports on the web. System keys (Back, volume, media) are not reported, and a character typed after a dead key is reported composed, as on the web. The input connection wrapper keeps reporting typed text, without its sendKeyEvent special cases.

The names come from the table the View onKeyDown event (#54308) already used, completed with the UI Events key values as Firefox for Android pairs them with key codes, so onKeyDown also names Home, End, the function keys and Ctrl shortcuts that it reported as Unidentified. onKeyDown alone does not cover this: it is behind enableKeyEvents, and it never sees a key that Gboard types as text.

Changelog:

[ANDROID] [ADDED] - TextInput onKeyPress reports every hardware keyboard key by its KeyboardEvent.key value, with altKey, ctrlKey, metaKey and shiftKey

Test Plan:

ReactEditTextKeyPressTest focuses the view in a window and sends keys the two ways a device does: to the window, as a hardware keyboard does, and through the view's input connection, which the framework delivers back to the view. On main 10 of its 15 cases fail; with the change all pass. KeyDownEventTest fails on main with Unidentified for Home, PageDown, Delete and F1. The ReactAndroid unit suite passes with -PenableWarningsAsErrors=true, as do Flow, both TypeScript checks, ESLint, Prettier and ktfmt, and ReactNativeApi.d.ts is regenerated.

RNTester release builds with the Playground below, typing a, Shift+B, Ctrl+C, ←, →, Home, End, F1, Alt+E, E, volume up, volume down, ←, Backspace and z on a USB keyboard (registered through uinput), report:

main this PR
Samsung SM-S911B, Android 14, Samsung Keyboard [] a, shift+Shift, shift+B, ctrl+Control, ctrl+c, ArrowLeft, ArrowRight, Home, End, F1, alt+Alt, alt+e, e, ArrowLeft, Backspace, z
nubia NX809J, Android 16, Gboard a, B, e, Backspace, z a, shift+Shift, B, e, ctrl+Control, ctrl+c, ArrowLeft, ArrowRight, Home, End, F1, alt+Alt, alt+e, ArrowLeft, Backspace, z

Gboard types letters as text at once and forwards the other keys later, so the two interleave in the order Gboard delivers them. The volume keys are not reported on either device. An accent key (Alt+E in Android's virtual key map, sent with adb shell input keycombination) reports alt+Dead and then é on the S23. The soft keyboard (q, Backspace, Backspace, w) and the soft numeric keypad (1, 2, Backspace, 3) report the same keys on both builds and both devices.

packages/rn-tester/js/examples/Playground/RNTesterPlayground.js
/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow strict-local
 * @format
 */

import type {RNTesterModuleExample} from '../../types/RNTesterTypes';
import type {TextInputKeyPressEvent} from 'react-native';

import RNTesterText from '../../components/RNTesterText';
import ExampleTextInput from '../TextInput/ExampleTextInput';
import * as React from 'react';
import {useState} from 'react';
import {StyleSheet, View} from 'react-native';

function describeKeyPress(event: TextInputKeyPressEvent): string {
  const {altKey, ctrlKey, metaKey, shiftKey, key} = event.nativeEvent;
  return [
    altKey === true ? 'alt' : null,
    ctrlKey === true ? 'ctrl' : null,
    metaKey === true ? 'meta' : null,
    shiftKey === true ? 'shift' : null,
    key,
  ]
    .filter(part => part != null)
    .join('+');
}

function KeyPressLog({
  label,
  keyboardType,
}: {
  label: string,
  keyboardType: 'default' | 'number-pad',
}): React.Node {
  const [text, setText] = useState('');
  const [keys, setKeys] = useState<ReadonlyArray<string>>([]);
  return (
    <>
      <ExampleTextInput
        testID={`${label}-input`}
        autoCapitalize="none"
        keyboardType={keyboardType}
        placeholder={label}
        onChangeText={setText}
        onKeyPress={event => {
          const description = describeKeyPress(event);
          setKeys(previousKeys => [...previousKeys, description]);
        }}
      />
      <RNTesterText>
        {label} text: {JSON.stringify(text)}
      </RNTesterText>
      <RNTesterText>
        {label} onKeyPress: {JSON.stringify(keys)}
      </RNTesterText>
    </>
  );
}

function Playground() {
  return (
    <View style={styles.container}>
      <KeyPressLog label="keys" keyboardType="default" />
      <KeyPressLog label="digits" keyboardType="number-pad" />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    padding: 10,
    rowGap: 8,
  },
});

export default {
  title: 'Playground',
  name: 'playground',
  description: 'Test out new features and ideas.',
  render: (): React.Node => <Playground />,
} as RNTesterModuleExample;

onKeyPress on Android is driven by ReactEditTextInputConnectionWrapper,
which sees only what the input method sends through the InputConnection.
A hardware keyboard's keys never pass through it: they reach the EditText
through dispatchKeyEvent and the KeyListener, so an arrow, Home/End, a
separator typed on a paired keyboard, or any other hardware key produced
no onKeyPress at all.

ReactEditText.onKeyDown now reports those keys: a key that types a
character as that character, and a key that types none by its web
KeyboardEvent.key value (ArrowLeft, Home, PageDown, Escape, ...).
Enter and Backspace keep the names the soft keyboard path reports, a
modifier on its own or a Ctrl/Meta chord is not reported, and a key event
the input method sent through the InputConnection is left to the wrapper,
which already reports it, so no key is reported twice.
An input method that handles a hardware keyboard forwards its keys through
InputConnection.sendKeyEvent: Gboard commits the letters as text and sends
arrows, Home, End and Backspace as key events. So every key event is reported
where it reaches the view, ReactEditText.onKeyDown, whether a hardware
keyboard or an input method sent it, and
ReactEditTextInputConnectionWrapper reports only the keys an input method
types as text. No key is reported twice, and keys an input method sends
together are reported in the order the view receives them.
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Oct 15, 2026
@YevheniiKotyrlo
YevheniiKotyrlo marked this pull request as ready for review October 15, 2026 18:51
@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Oct 15, 2026
@YevheniiKotyrlo
YevheniiKotyrlo marked this pull request as draft October 15, 2026 20:09
onKeyPress names each key by its KeyboardEvent.key value, from the
key table the View key events use, and carries altKey, ctrlKey,
metaKey and shiftKey. A key that types a character after a dead key
is reported as the composed character, and system keys such as Back
and volume are not reported.

The key table follows the UI Events key values as Firefox for Android
pairs them with key codes, so View onKeyDown also names Home, End,
the function keys and Ctrl shortcuts instead of reporting them as
Unidentified.

The tests deliver an input method's key events through the window, as
the framework delivers them to a focused view.
On a layout where Alt+E types no character, the key value is "e" with
altKey set, as on the web, rather than Unidentified. The tests type on
a layout of their own for this and for a dead key, which Robolectric's
key character map cannot express.
@github-actions

Copy link
Copy Markdown

Warning

JavaScript API change detected

This PR commits an update to ReactNativeApi.d.ts, indicating a change to React Native's public JavaScript API.

  • Please include a clear changelog message.
  • This change will be subject to additional review.

This change was flagged as: POTENTIALLY_BREAKING

@YevheniiKotyrlo
YevheniiKotyrlo marked this pull request as ready for review October 15, 2026 23:52

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant