diff --git a/change/react-native-windows-049db1de-5833-4cb0-a208-fec0fa53e060.json b/change/react-native-windows-049db1de-5833-4cb0-a208-fec0fa53e060.json
new file mode 100644
index 00000000000..9866fe0f1b1
--- /dev/null
+++ b/change/react-native-windows-049db1de-5833-4cb0-a208-fec0fa53e060.json
@@ -0,0 +1,7 @@
+{
+ "type": "prerelease",
+ "comment": "Adds declarative props to clear/submit TextInput",
+ "packageName": "react-native-windows",
+ "email": "erozell@outlook.com",
+ "dependentChangeType": "patch"
+}
diff --git a/packages/@react-native-windows/tester/src/js/examples/TextInput/TextInputExample.windows.js b/packages/@react-native-windows/tester/src/js/examples/TextInput/TextInputExample.windows.js
index 156e48d4fe5..f06629623dc 100644
--- a/packages/@react-native-windows/tester/src/js/examples/TextInput/TextInputExample.windows.js
+++ b/packages/@react-native-windows/tester/src/js/examples/TextInput/TextInputExample.windows.js
@@ -455,4 +455,37 @@ exports.examples = ([
return ;
},
},
+ // [Windows
+ {
+ title: 'Clear text on submit',
+ render: function(): React.Node {
+ return (
+
+ Default submit key (Enter):
+
+ Custom submit key event (Shift + Enter), single-line:
+
+ Custom submit key event (Shift + Enter), multi-line:
+
+ Submit with Enter key, return key with Shift + Enter
+
+
+ );
+ },
+ },
+ // Windows]
]: Array);
diff --git a/vnext/Microsoft.ReactNative/Views/KeyboardEventHandler.cpp b/vnext/Microsoft.ReactNative/Views/KeyboardEventHandler.cpp
index be8d00e012f..6021280f11c 100644
--- a/vnext/Microsoft.ReactNative/Views/KeyboardEventHandler.cpp
+++ b/vnext/Microsoft.ReactNative/Views/KeyboardEventHandler.cpp
@@ -177,15 +177,15 @@ void HandledKeyboardEventHandler::KeyboardEventHandledHandler(
bool shouldMarkHandled = false;
if (phase == KeyboardEventPhase::PreviewKeyDown || phase == KeyboardEventPhase::KeyDown)
- shouldMarkHandled = ShouldMarkKeyboardHandled(m_handledKeyDownKeyboardEvents, event);
+ shouldMarkHandled = KeyboardHelper::ShouldMarkKeyboardHandled(m_handledKeyDownKeyboardEvents, event);
else
- shouldMarkHandled = ShouldMarkKeyboardHandled(m_handledKeyUpKeyboardEvents, event);
+ shouldMarkHandled = KeyboardHelper::ShouldMarkKeyboardHandled(m_handledKeyUpKeyboardEvents, event);
if (shouldMarkHandled)
args.Handled(true);
}
-bool HandledKeyboardEventHandler::ShouldMarkKeyboardHandled(
+/* static */ bool KeyboardHelper::ShouldMarkKeyboardHandled(
std::vector const &handledEvents,
HandledKeyboardEvent currentEvent) {
for (auto const &event : handledEvents) {
diff --git a/vnext/Microsoft.ReactNative/Views/KeyboardEventHandler.h b/vnext/Microsoft.ReactNative/Views/KeyboardEventHandler.h
index c2009ccc5ac..43d74efbf63 100644
--- a/vnext/Microsoft.ReactNative/Views/KeyboardEventHandler.h
+++ b/vnext/Microsoft.ReactNative/Views/KeyboardEventHandler.h
@@ -113,9 +113,6 @@ class HandledKeyboardEventHandler {
KeyboardEventPhase phase,
winrt::IInspectable const &sender,
xaml::Input::KeyRoutedEventArgs const &args);
- bool ShouldMarkKeyboardHandled(
- std::vector const &handledEvents,
- HandledKeyboardEvent currentEvent);
std::vector m_handledKeyUpKeyboardEvents;
std::vector m_handledKeyDownKeyboardEvents;
@@ -131,5 +128,8 @@ struct KeyboardHelper {
static std::string CodeFromVirtualKey(winrt::Windows::System::VirtualKey key);
static bool IsModifiedKeyPressed(winrt::CoreWindow const &coreWindow, winrt::Windows::System::VirtualKey virtualKey);
static bool IsModifiedKeyLocked(winrt::CoreWindow const &coreWindow, winrt::Windows::System::VirtualKey virtualKey);
+ static bool ShouldMarkKeyboardHandled(
+ std::vector const &handledEvents,
+ HandledKeyboardEvent currentEvent);
};
} // namespace Microsoft::ReactNative
diff --git a/vnext/Microsoft.ReactNative/Views/TextInputViewManager.cpp b/vnext/Microsoft.ReactNative/Views/TextInputViewManager.cpp
index 8888e77e6bf..84016df127f 100644
--- a/vnext/Microsoft.ReactNative/Views/TextInputViewManager.cpp
+++ b/vnext/Microsoft.ReactNative/Views/TextInputViewManager.cpp
@@ -124,6 +124,7 @@ class TextInputShadowNode : public ShadowNodeBase {
private:
void dispatchTextInputChangeEvent(winrt::hstring newText);
void registerEvents();
+ void registerPreviewKeyDown();
void HideCaretIfNeeded();
void setPasswordBoxPlaceholderForeground(
xaml::Controls::PasswordBox passwordBox,
@@ -138,6 +139,8 @@ class TextInputShadowNode : public ShadowNodeBase {
bool m_hideCaret = false;
bool m_isTextBox = true;
winrt::Microsoft::ReactNative::JSValue m_placeholderTextColor;
+ bool m_shouldClearTextOnSubmit = false;
+ std::vector m_submitKeyEvents{};
// Javascripts is running in a different thread. If the typing is very fast,
// It's possible that two TextChanged are raised but TextInput just got the
@@ -158,7 +161,7 @@ class TextInputShadowNode : public ShadowNodeBase {
xaml::Controls::Control::GotFocus_revoker m_controlGotFocusRevoker{};
xaml::Controls::Control::LostFocus_revoker m_controlLostFocusRevoker{};
- xaml::Controls::Control::KeyDown_revoker m_controlKeyDownRevoker{};
+ xaml::Controls::Control::PreviewKeyDown_revoker m_controlPreviewKeyDownRevoker{};
xaml::Controls::Control::SizeChanged_revoker m_controlSizeChangedRevoker{};
xaml::Controls::Control::CharacterReceived_revoker m_controlCharacterReceivedRevoker{};
xaml::Controls::ScrollViewer::ViewChanging_revoker m_scrollViewerViewChangingRevoker{};
@@ -277,21 +280,7 @@ void TextInputShadowNode::registerEvents() {
}
});
- m_controlKeyDownRevoker =
- control.KeyDown(winrt::auto_revoke, [=](auto &&, xaml::Input::KeyRoutedEventArgs const &args) {
- if (args.Key() == winrt::Windows::System::VirtualKey::Enter && !args.Handled()) {
- folly::dynamic eventDataSubmitEditing = {};
- if (m_isTextBox) {
- eventDataSubmitEditing = folly::dynamic::object("target", tag)(
- "text", react::uwp::HstringToDynamic(control.as().Text()));
- } else {
- eventDataSubmitEditing = folly::dynamic::object("target", tag)(
- "text", react::uwp::HstringToDynamic(control.as().Password()));
- }
- GetViewManager()->GetReactContext().DispatchEvent(
- tag, "topTextInputSubmitEditing", std::move(eventDataSubmitEditing));
- }
- });
+ registerPreviewKeyDown();
if (m_isTextBox) {
auto textBox = control.as();
@@ -373,6 +362,61 @@ void TextInputShadowNode::registerEvents() {
true);
}
+void TextInputShadowNode::registerPreviewKeyDown() {
+ auto control = GetView().as();
+ auto tag = m_tag;
+ m_controlPreviewKeyDownRevoker =
+ control.PreviewKeyDown(winrt::auto_revoke, [=](auto &&, xaml::Input::KeyRoutedEventArgs const &args) {
+ auto isMultiline = m_isTextBox && control.as().AcceptsReturn();
+ auto shouldSubmit = !args.Handled();
+ if (shouldSubmit) {
+ if (!isMultiline && m_submitKeyEvents.size() == 0) {
+ // If no 'submitKeyEvents' are supplied, use the default behavior for single-line TextInput
+ shouldSubmit = args.Key() == winrt::Windows::System::VirtualKey::Enter;
+ } else if (m_submitKeyEvents.size() > 0) {
+ // If 'submitKeyEvents' are supplied, use them to determine whether to emit
+ // 'onSubmitEditing' for either single-line or multi-line TextInput
+
+ // This must be kept in sync with the default value for HandledKeyboardEvent.handledEventPhase
+ auto defaultEventPhase = HandledEventPhase::Bubbling;
+ auto currentEvent = KeyboardHelper::CreateKeyboardEvent(defaultEventPhase, args);
+ shouldSubmit = KeyboardHelper::ShouldMarkKeyboardHandled(m_submitKeyEvents, currentEvent);
+ } else {
+ // If no 'submitKeyEvents' are supplied, do not emit 'onSubmitEditing' for multi-line TextInput
+ shouldSubmit = false;
+ }
+ }
+
+ if (shouldSubmit) {
+ folly::dynamic eventDataSubmitEditing = {};
+ if (m_isTextBox) {
+ eventDataSubmitEditing = folly::dynamic::object("target", tag)(
+ "text", react::uwp::HstringToDynamic(control.as().Text()));
+ } else {
+ eventDataSubmitEditing = folly::dynamic::object("target", tag)(
+ "text", react::uwp::HstringToDynamic(control.as().Password()));
+ }
+
+ if (m_shouldClearTextOnSubmit) {
+ if (m_isTextBox) {
+ control.as().ClearValue(xaml::Controls::TextBox::TextProperty());
+ } else {
+ control.as().ClearValue(xaml::Controls::PasswordBox::PasswordProperty());
+ }
+ }
+
+ GetViewManager()->GetReactContext().DispatchEvent(
+ tag, "topTextInputSubmitEditing", std::move(eventDataSubmitEditing));
+
+ // For multi-line TextInput, we have to mark the PreviewKeyDown event as
+ // handled to prevent the TextInput from adding a newline character
+ if (isMultiline) {
+ args.Handled(true);
+ }
+ }
+ });
+}
+
xaml::Shapes::Shape TextInputShadowNode::FindCaret(xaml::DependencyObject element) {
if (element == nullptr)
return nullptr;
@@ -423,6 +467,7 @@ void TextInputShadowNode::updateProperties(winrt::Microsoft::ReactNative::JSValu
auto control = GetView().as();
auto textBox = control.try_as();
auto passwordBox = control.try_as();
+ auto hasKeyDownEvents = false;
for (auto &pair : props) {
const std::string &propertyName = pair.first;
@@ -544,6 +589,16 @@ void TextInputShadowNode::updateProperties(winrt::Microsoft::ReactNative::JSValu
} else if (m_isTextBox != true && react::uwp::IsValidColorValue(propertyValue)) {
setPasswordBoxPlaceholderForeground(passwordBox, propertyValue);
}
+ } else if (propertyName == "clearTextOnSubmit") {
+ if (propertyValue.Type() == winrt::Microsoft::ReactNative::JSValueType::Boolean)
+ m_shouldClearTextOnSubmit = propertyValue.AsBoolean();
+ } else if (propertyName == "submitKeyEvents") {
+ if (propertyValue.Type() == winrt::Microsoft::ReactNative::JSValueType::Array)
+ m_submitKeyEvents = KeyboardHelper::FromJS(propertyValue);
+ else if (propertyValue.IsNull())
+ m_submitKeyEvents.clear();
+ } else if (propertyName == "keyDownEvents") {
+ hasKeyDownEvents = propertyValue.ItemCount() > 0;
} else {
if (m_isTextBox) { // Applicable properties for TextBox
if (TryUpdateTextAlignment(textBox, propertyName, propertyValue)) {
@@ -602,6 +657,13 @@ void TextInputShadowNode::updateProperties(winrt::Microsoft::ReactNative::JSValu
}
Super::updateProperties(props);
+
+ // We need to re-register the PreviewKeyDown handler so it is invoked after the ShadowNodeBase handler
+ if (hasKeyDownEvents) {
+ m_controlPreviewKeyDownRevoker.revoke();
+ registerPreviewKeyDown();
+ }
+
m_updating = false;
}
@@ -697,6 +759,8 @@ void TextInputViewManager::GetNativeProps(const winrt::Microsoft::ReactNative::I
React::WriteProperty(writer, L"contextMenuHidden", L"boolean");
React::WriteProperty(writer, L"caretHidden", L"boolean");
React::WriteProperty(writer, L"autoCapitalize", L"string");
+ React::WriteProperty(writer, L"clearTextOnSubmit", L"boolean");
+ React::WriteProperty(writer, L"submitKeyEvents", L"array");
}
void TextInputViewManager::GetExportedCustomDirectEventTypeConstants(
diff --git a/vnext/src/Libraries/Components/TextInput/TextInput.windows.js b/vnext/src/Libraries/Components/TextInput/TextInput.windows.js
index 0f6dec3d4a5..5e84be89819 100644
--- a/vnext/src/Libraries/Components/TextInput/TextInput.windows.js
+++ b/vnext/src/Libraries/Components/TextInput/TextInput.windows.js
@@ -421,10 +421,37 @@ type AndroidProps = $ReadOnly<{|
underlineColorAndroid?: ?ColorValue,
|}>;
+// [Windows
+
+type SubmitKeyEvent = $ReadOnly<{|
+ altKey?: ?boolean,
+ ctrlKey?: ?boolean,
+ metaKey?: ?boolean,
+ shiftKey?: ?boolean,
+ code: string,
+|}>;
+
+type WindowsProps = $ReadOnly<{|
+ /**
+ * If `true`, clears the text field synchronously before `onSubmitEditing` is emitted.
+ * @platform windows
+ */
+ clearTextOnSubmit?: ?boolean,
+
+ /**
+ * Configures keys that can be used to submit editing for the TextInput.
+ * @platform windows
+ */
+ submitKeyEvents?: ?$ReadOnlyArray,
+|}>;
+
+// Windows]
+
export type Props = $ReadOnly<{|
...$Diff>,
...IOSProps,
...AndroidProps,
+ ...WindowsProps, // [Windows]
/**
* Can tell `TextInput` to automatically capitalize certain characters.