diff --git a/change/react-native-windows-edbf9be7-245f-42c4-9644-96e44ed5d3d5.json b/change/react-native-windows-edbf9be7-245f-42c4-9644-96e44ed5d3d5.json
new file mode 100644
index 00000000000..77ddf766794
--- /dev/null
+++ b/change/react-native-windows-edbf9be7-245f-42c4-9644-96e44ed5d3d5.json
@@ -0,0 +1,7 @@
+{
+ "type": "prerelease",
+ "comment": "add fabric onKeyPress to textinput",
+ "packageName": "react-native-windows",
+ "email": "tatianakapos@microsoft.com",
+ "dependentChangeType": "patch"
+}
diff --git a/packages/@react-native-windows/tester/src/js/examples-win/LegacyTests/TextInputTestPage.tsx b/packages/@react-native-windows/tester/src/js/examples-win/LegacyTests/TextInputTestPage.tsx
index 7875884689e..739f3f49458 100644
--- a/packages/@react-native-windows/tester/src/js/examples-win/LegacyTests/TextInputTestPage.tsx
+++ b/packages/@react-native-windows/tester/src/js/examples-win/LegacyTests/TextInputTestPage.tsx
@@ -69,7 +69,9 @@ export class TextInputTestPage extends React.Component<
placeholder="autoCapitalize"
autoCapitalize="characters"
/>
- {this.state.log}
+
+ {this.state.log}
+
);
}
diff --git a/packages/e2e-test-app-fabric/test/LegacyTextInputTest.test.ts b/packages/e2e-test-app-fabric/test/LegacyTextInputTest.test.ts
new file mode 100644
index 00000000000..da49f5c3b10
--- /dev/null
+++ b/packages/e2e-test-app-fabric/test/LegacyTextInputTest.test.ts
@@ -0,0 +1,170 @@
+/**
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT License.
+ *
+ * @format
+ */
+import {app} from '@react-native-windows/automation';
+import {goToComponentExample} from './RNTesterNavigation';
+import {verifyNoErrorLogs} from './Helpers';
+import {dumpVisualTree} from '@react-native-windows/automation-commands';
+
+beforeAll(async () => {
+ // If window is partially offscreen, tests will fail to click on certain elements
+ await app.setWindowPosition(0, 0);
+ await app.setWindowSize(1000, 1250);
+ await goToComponentExample('LegacyTextInputTest');
+});
+
+afterEach(async () => {
+ await verifyNoErrorLogs();
+});
+
+describe('LegacyTextInputTest', () => {
+ test('Click on TextInput to focus', async () => {
+ const textInput = await textInputField();
+ await textInput.click();
+ await assertLogContains('onFocus');
+ });
+
+ test('Click on multiline TextInput to move focus away from single line TextInput', async () => {
+ const textInput = await multiLineTextInputField();
+ await textInput.click();
+ await assertLogContains('onBlur');
+ });
+ test('Type abc on TextInput', async () => {
+ const textInput = await textInputField();
+ await textInput.setValue('abc');
+ expect(await textInput.getText()).toBe('abc');
+ await assertLogContains('onKeyPress key: c');
+ });
+ test('Type def on TextInput', async () => {
+ const textInput = await textInputField();
+ await app.waitUntil(
+ async () => {
+ await textInput.setValue('def');
+ return (await textInput.getText()) === 'def';
+ },
+ {
+ interval: 1500,
+ timeout: 5000,
+ timeoutMsg: `Unable to enter correct text.`,
+ },
+ );
+ expect(await textInput.getText()).toBe('def');
+ });
+ /* Issue to enable these tests: #12778
+ test('Type hello world on autoCap TextInput', async () => {
+ const textInput = await autoCapsTextInputField();
+ await textInput.setValue('def');
+ expect(await textInput.getText()).toBe('DEF');
+
+ await textInput.setValue('hello world');
+ expect(await textInput.getText()).toBe('HELLO WORLD');
+ });
+ */
+ test('Type abc on multiline TextInput then press Enter key', async () => {
+ const textInput = await textInputField();
+ await textInput.setValue('abc');
+ await textInput.addValue('Enter');
+
+ await assertLogContains('onSubmitEditing text: abc');
+ });
+ test('Type abc on multiline TextInput', async () => {
+ const textInput = await multiLineTextInputField();
+ await textInput.setValue('abc');
+
+ expect(await textInput.getText()).toBe('abc');
+ });
+
+ test('Enter key then type def on multiline TextInput', async () => {
+ const textInput = await multiLineTextInputField();
+
+ await textInput.addValue('End');
+ await textInput.addValue('Enter');
+ await textInput.addValue('def');
+
+ expect(await textInput.getText()).toBe('abc\rdef');
+ });
+
+ test('TextInput onChange before onSelectionChange', async () => {
+ const textInput = await textInputField();
+ await textInput.setValue('a');
+ await assertLogContains('onChange text: a\nonSelectionChange range: 1,1');
+ /* Issue to enable these tests: #12778
+ await assertLogContainsInOrder([
+ 'onChange text: a',
+ 'onSelectionChange range: 1,1',
+ ]);
+ */
+ });
+});
+
+async function textInputField() {
+ const component = await app.findElementByTestID('textinput-field');
+ await component.waitForDisplayed({timeout: 5000});
+ return component;
+}
+
+/* Issue to enable these tests: #12778
+async function autoCapsTextInputField() {
+ const component = await app.findElementByTestID('auto-caps-textinput-field');
+ await component.waitForDisplayed({timeout: 5000});
+ return component;
+}
+*/
+
+async function multiLineTextInputField() {
+ const component = await app.findElementByTestID('multi-line-textinput-field');
+ await component.waitForDisplayed({timeout: 5000});
+ return component;
+}
+
+async function assertLogContains(_text: string) {
+ const textLogComponent = await app.findElementByTestID('textinput-log');
+ await textLogComponent.waitForDisplayed({timeout: 5000});
+
+ const dump = await dumpVisualTree('textinput-log');
+ expect(dump).toMatchSnapshot();
+ /*
+ await app.waitUntil(
+ async () => {
+ const loggedText = await textLogComponent.getText();
+ return loggedText.split('\n').includes(text);
+ },
+ {
+ timeoutMsg: `"${await textLogComponent.getValue()}" "${await textLogComponent.getText()}" did not contain "${text}"`,
+ },
+ );
+ */
+}
+
+/*
+async function assertLogContainsInOrder(expectedLines: string[]) {
+ const textLogComponent = await app.findElementByTestID('textinput-log');
+ await textLogComponent.waitForDisplayed({timeout: 5000});
+
+ await app.waitUntil(
+ async () => {
+ const loggedText = await textLogComponent.getText();
+ const actualLines = loggedText.split('\n');
+ let previousIndex = Number.MAX_VALUE;
+ for (const line of expectedLines) {
+ const index = actualLines.findIndex(l => l === line);
+ if (index === -1 || index > previousIndex) {
+ return false;
+ }
+
+ previousIndex = index;
+ }
+
+ return true;
+ },
+ {
+ timeoutMsg: `"${await textLogComponent.getText()}" did not contain lines "${expectedLines.join(
+ ', ',
+ )}"`,
+ },
+ );
+}
+*/
diff --git a/packages/e2e-test-app-fabric/test/__snapshots__/LegacyTextInputTest.test.ts.snap b/packages/e2e-test-app-fabric/test/__snapshots__/LegacyTextInputTest.test.ts.snap
new file mode 100644
index 00000000000..74bd9c140f5
--- /dev/null
+++ b/packages/e2e-test-app-fabric/test/__snapshots__/LegacyTextInputTest.test.ts.snap
@@ -0,0 +1,242 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`LegacyTextInputTest Click on TextInput to focus 1`] = `
+{
+ "Automation Tree": {
+ "AutomationId": "textinput-log",
+ "ControlType": 50020,
+ "HelpText": "",
+ "IsEnabled": true,
+ "IsKeyboardFocusable": false,
+ "LocalizedControlType": "text",
+ "Name": "onFocus
+",
+ },
+ "Visual Tree": {
+ "Comment": "textinput-log",
+ "Offset": [
+ 0,
+ 0,
+ 0,
+ ],
+ "Opacity": 1,
+ "Size": [
+ 998,
+ 38,
+ ],
+ "Visual Type": "SpriteVisual",
+ },
+}
+`;
+
+exports[`LegacyTextInputTest Click on multiline TextInput to move focus away from single line TextInput 1`] = `
+{
+ "Automation Tree": {
+ "AutomationId": "textinput-log",
+ "ControlType": 50020,
+ "HelpText": "",
+ "IsEnabled": true,
+ "IsKeyboardFocusable": false,
+ "LocalizedControlType": "text",
+ "Name": "onBlur
+onFocus
+",
+ },
+ "Visual Tree": {
+ "Comment": "textinput-log",
+ "Offset": [
+ 0,
+ 0,
+ 0,
+ ],
+ "Opacity": 1,
+ "Size": [
+ 998,
+ 57,
+ ],
+ "Visual Type": "SpriteVisual",
+ },
+}
+`;
+
+exports[`LegacyTextInputTest TextInput onChange before onSelectionChange 1`] = `
+{
+ "Automation Tree": {
+ "AutomationId": "textinput-log",
+ "ControlType": 50020,
+ "HelpText": "",
+ "IsEnabled": true,
+ "IsKeyboardFocusable": false,
+ "LocalizedControlType": "text",
+ "Name": "onChange text: a
+onSelectionChange range: 1,1
+onKeyPress key: a
+onChange text:
+onSelectionChange range: 0,0
+onFocus
+onBlur
+onSubmitEditing text: abc
+onChange text: abc
+onSelectionChange range: 3,3
+onKeyPress key: c
+onChange text: ab
+onSelectionChange range: 2,2
+onKeyPress key: b
+onChange text: a
+onSelectionChange range: 1,1
+onKeyPress key: a
+onChange text:
+onSelectionChange range: 0,0
+onChange text: def
+onSelectionChange range: 3,3
+onKeyPress key: f
+onChange text: de
+onSelectionChange range: 2,2
+onKeyPress key: e
+onChange text: d
+onSelectionChange range: 1,1
+onKeyPress key: d
+onChange text:
+onSelectionChange range: 0,0
+onChange text: abc
+onSelectionChange range: 3,3
+onKeyPress key: c
+onChange text: ab
+onSelectionChange range: 2,2
+onKeyPress key: b
+onSelectionChange range: 1,1
+onChange text: a
+onSelectionChange range: 0,0
+onChange text: a
+onSelectionChange range: 1,1
+onKeyPress key: a
+onFocus
+onBlur
+onFocus
+",
+ },
+ "Visual Tree": {
+ "Comment": "textinput-log",
+ "Offset": [
+ 0,
+ 0,
+ 0,
+ ],
+ "Opacity": 1,
+ "Size": [
+ 998,
+ 858,
+ ],
+ "Visual Type": "SpriteVisual",
+ },
+}
+`;
+
+exports[`LegacyTextInputTest Type abc on TextInput 1`] = `
+{
+ "Automation Tree": {
+ "AutomationId": "textinput-log",
+ "ControlType": 50020,
+ "HelpText": "",
+ "IsEnabled": true,
+ "IsKeyboardFocusable": false,
+ "LocalizedControlType": "text",
+ "Name": "onChange text: abc
+onSelectionChange range: 3,3
+onKeyPress key: c
+onChange text: ab
+onSelectionChange range: 2,2
+onKeyPress key: b
+onSelectionChange range: 1,1
+onChange text: a
+onSelectionChange range: 0,0
+onChange text: a
+onSelectionChange range: 1,1
+onKeyPress key: a
+onFocus
+onBlur
+onFocus
+",
+ },
+ "Visual Tree": {
+ "Comment": "textinput-log",
+ "Offset": [
+ 0,
+ 0,
+ 0,
+ ],
+ "Opacity": 1,
+ "Size": [
+ 998,
+ 299,
+ ],
+ "Visual Type": "SpriteVisual",
+ },
+}
+`;
+
+exports[`LegacyTextInputTest Type abc on multiline TextInput then press Enter key 1`] = `
+{
+ "Automation Tree": {
+ "AutomationId": "textinput-log",
+ "ControlType": 50020,
+ "HelpText": "",
+ "IsEnabled": true,
+ "IsKeyboardFocusable": false,
+ "LocalizedControlType": "text",
+ "Name": "onSubmitEditing text: abc
+onChange text: abc
+onSelectionChange range: 3,3
+onKeyPress key: c
+onChange text: ab
+onSelectionChange range: 2,2
+onKeyPress key: b
+onChange text: a
+onSelectionChange range: 1,1
+onKeyPress key: a
+onChange text:
+onSelectionChange range: 0,0
+onChange text: def
+onSelectionChange range: 3,3
+onKeyPress key: f
+onChange text: de
+onSelectionChange range: 2,2
+onKeyPress key: e
+onChange text: d
+onSelectionChange range: 1,1
+onKeyPress key: d
+onChange text:
+onSelectionChange range: 0,0
+onChange text: abc
+onSelectionChange range: 3,3
+onKeyPress key: c
+onChange text: ab
+onSelectionChange range: 2,2
+onKeyPress key: b
+onSelectionChange range: 1,1
+onChange text: a
+onSelectionChange range: 0,0
+onChange text: a
+onSelectionChange range: 1,1
+onKeyPress key: a
+onFocus
+onBlur
+onFocus
+",
+ },
+ "Visual Tree": {
+ "Comment": "textinput-log",
+ "Offset": [
+ 0,
+ 0,
+ 0,
+ ],
+ "Opacity": 1,
+ "Size": [
+ 998,
+ 727,
+ ],
+ "Visual Type": "SpriteVisual",
+ },
+}
+`;
diff --git a/packages/e2e-test-app-fabric/test/__snapshots__/snapshotPages.test.js.snap b/packages/e2e-test-app-fabric/test/__snapshots__/snapshotPages.test.js.snap
index 7965ea5627d..d2ee275e9c7 100644
--- a/packages/e2e-test-app-fabric/test/__snapshots__/snapshotPages.test.js.snap
+++ b/packages/e2e-test-app-fabric/test/__snapshots__/snapshotPages.test.js.snap
@@ -29713,6 +29713,7 @@ exports[`snapshotAllPages LegacyTextInputTest 1`] = `
testID="auto-caps-textinput-field"
/>
<Log Start>
diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.cpp
index 4af9d1fcee8..2454fe78f5f 100644
--- a/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.cpp
+++ b/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.cpp
@@ -20,6 +20,8 @@
#include "WindowsTextInputState.h"
#include "guid/msoGuid.h"
+#include
+
// convert a BSTR to a std::string.
std::string &BstrToStdString(const BSTR bstr, std::string &dst, int cp = CP_UTF8) {
if (!bstr) {
@@ -865,6 +867,22 @@ void WindowsTextInputComponentView::OnCharacterReceived(
return;
}
+ // convert keyCode to std::string
+ wchar_t key[2] = L" ";
+ key[0] = static_cast(args.KeyCode());
+ std::string keyString = ::Microsoft::Common::Unicode::Utf16ToUtf8(key, 1);
+ // Call onKeyPress event
+ auto emitter = std::static_pointer_cast(m_eventEmitter);
+ facebook::react::WindowsTextInputEventEmitter::OnKeyPress onKeyPressArgs;
+ if (keyString.compare("\r") == 0) {
+ onKeyPressArgs.key = "Enter";
+ } else if (keyString.compare("\b") == 0) {
+ onKeyPressArgs.key = "Backspace";
+ } else {
+ onKeyPressArgs.key = keyString;
+ }
+ emitter->onKeyPress(onKeyPressArgs);
+
WPARAM wParam = static_cast(args.KeyCode());
LPARAM lParam = 0;
lParam = args.KeyStatus().RepeatCount; // bits 0-15
@@ -1186,6 +1204,7 @@ void WindowsTextInputComponentView::OnTextUpdated() noexcept {
m_state->updateState(std::move(data));
if (m_eventEmitter && !m_comingFromJS) {
+ // call onChange event
auto emitter = std::static_pointer_cast(m_eventEmitter);
facebook::react::WindowsTextInputEventEmitter::OnChange onChangeArgs;
onChangeArgs.text = GetTextFromRichEdit();
diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputEventEmitter.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputEventEmitter.cpp
index 628e8fe4b79..edd0d55ab0b 100644
--- a/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputEventEmitter.cpp
+++ b/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputEventEmitter.cpp
@@ -38,4 +38,12 @@ void WindowsTextInputEventEmitter::onSubmitEditing(OnSubmitEditing event) const
});
}
+void WindowsTextInputEventEmitter::onKeyPress(OnKeyPress event) const {
+ dispatchEvent("textInputKeyPress", [event = std::move(event)](jsi::Runtime &runtime) {
+ auto payload = jsi::Object(runtime);
+ payload.setProperty(runtime, "key", event.key);
+ return payload;
+ });
+}
+
} // namespace facebook::react
diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputEventEmitter.h b/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputEventEmitter.h
index e35a4ab47ea..900fe45dd3f 100644
--- a/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputEventEmitter.h
+++ b/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputEventEmitter.h
@@ -32,9 +32,14 @@ class WindowsTextInputEventEmitter : public ViewEventEmitter {
std::string text;
};
+ struct OnKeyPress {
+ std::string key;
+ };
+
void onChange(OnChange value) const;
void onSelectionChange(const OnSelectionChange &value) const;
void onSubmitEditing(OnSubmitEditing value) const;
+ void onKeyPress(OnKeyPress value) const;
};
} // namespace facebook::react