From 7e4a53aa108c3ccfbd6b9f8261cca9df1874c7a1 Mon Sep 17 00:00:00 2001 From: Eric Rozell Date: Tue, 9 Mar 2021 13:08:36 -0500 Subject: [PATCH 1/6] Adds declarative props to clear/submit TextInput This change adds two new props and a new behavior for multiline TextInput. New props: 1. `clearTextOnSubmit`: similar to `clearTextOnFocus`, except that it clears out the TextInput whenever the `onSubmitEditing` event would be dispatched. 2. `submitKeyEvents`: registers a set of key down events that will trigger `onSubmitEditing` for multiline TextInput. New behavior: Previously, `onSubmitEditing` would never fire when `multiline={true}` for a TextInput. Now, `onSubmitEditing` may fire for a multiline TextInput if a key down event matching the configuration in `submitKeyEvents` is observed. Fixes #7330 --- .../Views/KeyboardEventHandler.cpp | 7 +-- .../Views/KeyboardEventHandler.h | 6 +-- .../Views/TextInputViewManager.cpp | 53 +++++++++++++++++-- 3 files changed, 57 insertions(+), 9 deletions(-) diff --git a/vnext/Microsoft.ReactNative/Views/KeyboardEventHandler.cpp b/vnext/Microsoft.ReactNative/Views/KeyboardEventHandler.cpp index be8d00e012f..9034889502a 100644 --- a/vnext/Microsoft.ReactNative/Views/KeyboardEventHandler.cpp +++ b/vnext/Microsoft.ReactNative/Views/KeyboardEventHandler.cpp @@ -177,15 +177,16 @@ 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..d78411764e9 100644 --- a/vnext/Microsoft.ReactNative/Views/TextInputViewManager.cpp +++ b/vnext/Microsoft.ReactNative/Views/TextInputViewManager.cpp @@ -138,6 +138,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 +160,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_controlKeyDownRevoker{}; xaml::Controls::Control::SizeChanged_revoker m_controlSizeChangedRevoker{}; xaml::Controls::Control::CharacterReceived_revoker m_controlCharacterReceivedRevoker{}; xaml::Controls::ScrollViewer::ViewChanging_revoker m_scrollViewerViewChangingRevoker{}; @@ -278,8 +280,28 @@ 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()) { + 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)( @@ -288,8 +310,23 @@ void TextInputShadowNode::registerEvents() { 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); + } } }); @@ -544,6 +581,14 @@ 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 (m_isTextBox) { // Applicable properties for TextBox if (TryUpdateTextAlignment(textBox, propertyName, propertyValue)) { @@ -697,6 +742,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( From a30a0777f436074ac722547c1e7f97400a108a71 Mon Sep 17 00:00:00 2001 From: Eric Rozell Date: Tue, 9 Mar 2021 13:20:27 -0500 Subject: [PATCH 2/6] Change files --- ...ative-windows-049db1de-5833-4cb0-a208-fec0fa53e060.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/react-native-windows-049db1de-5833-4cb0-a208-fec0fa53e060.json 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" +} From 499f5499cfe4148d49b99ea910c3a01926b329d8 Mon Sep 17 00:00:00 2001 From: Eric Rozell Date: Wed, 10 Mar 2021 10:03:08 -0500 Subject: [PATCH 3/6] Fixes from yarn format --- vnext/Microsoft.ReactNative/Views/KeyboardEventHandler.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/vnext/Microsoft.ReactNative/Views/KeyboardEventHandler.cpp b/vnext/Microsoft.ReactNative/Views/KeyboardEventHandler.cpp index 9034889502a..6021280f11c 100644 --- a/vnext/Microsoft.ReactNative/Views/KeyboardEventHandler.cpp +++ b/vnext/Microsoft.ReactNative/Views/KeyboardEventHandler.cpp @@ -185,7 +185,6 @@ void HandledKeyboardEventHandler::KeyboardEventHandledHandler( args.Handled(true); } - /* static */ bool KeyboardHelper::ShouldMarkKeyboardHandled( std::vector const &handledEvents, HandledKeyboardEvent currentEvent) { From 83b6ef984e587188b966fdcdd559f66b15a4122c Mon Sep 17 00:00:00 2001 From: Eric Rozell Date: Tue, 16 Mar 2021 10:30:45 -0400 Subject: [PATCH 4/6] Ensure keyDownEvents are handled before submitKeyEvents --- .../Views/TextInputViewManager.cpp | 117 ++++++++++-------- 1 file changed, 67 insertions(+), 50 deletions(-) diff --git a/vnext/Microsoft.ReactNative/Views/TextInputViewManager.cpp b/vnext/Microsoft.ReactNative/Views/TextInputViewManager.cpp index d78411764e9..ecd9de455a9 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, @@ -279,56 +280,7 @@ void TextInputShadowNode::registerEvents() { } }); - m_controlKeyDownRevoker = - 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); - } - } - }); + registerPreviewKeyDown(); if (m_isTextBox) { auto textBox = control.as(); @@ -410,6 +362,61 @@ void TextInputShadowNode::registerEvents() { true); } +void TextInputShadowNode::registerPreviewKeyDown() { + auto control = GetView().as(); + auto tag = m_tag; + m_controlKeyDownRevoker = + 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; @@ -460,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; @@ -589,6 +597,8 @@ void TextInputShadowNode::updateProperties(winrt::Microsoft::ReactNative::JSValu 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)) { @@ -647,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_controlKeyDownRevoker.revoke(); + registerPreviewKeyDown(); + } + m_updating = false; } From da2653a2fc0e5f1408c9112d7957421fb8bc0c82 Mon Sep 17 00:00:00 2001 From: Eric Rozell Date: Fri, 9 Apr 2021 11:52:08 -0400 Subject: [PATCH 5/6] Fixes nit for revoker field name --- vnext/Microsoft.ReactNative/Views/TextInputViewManager.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vnext/Microsoft.ReactNative/Views/TextInputViewManager.cpp b/vnext/Microsoft.ReactNative/Views/TextInputViewManager.cpp index ecd9de455a9..84016df127f 100644 --- a/vnext/Microsoft.ReactNative/Views/TextInputViewManager.cpp +++ b/vnext/Microsoft.ReactNative/Views/TextInputViewManager.cpp @@ -161,7 +161,7 @@ class TextInputShadowNode : public ShadowNodeBase { xaml::Controls::Control::GotFocus_revoker m_controlGotFocusRevoker{}; xaml::Controls::Control::LostFocus_revoker m_controlLostFocusRevoker{}; - xaml::Controls::Control::PreviewKeyDown_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{}; @@ -365,7 +365,7 @@ void TextInputShadowNode::registerEvents() { void TextInputShadowNode::registerPreviewKeyDown() { auto control = GetView().as(); auto tag = m_tag; - m_controlKeyDownRevoker = + m_controlPreviewKeyDownRevoker = control.PreviewKeyDown(winrt::auto_revoke, [=](auto &&, xaml::Input::KeyRoutedEventArgs const &args) { auto isMultiline = m_isTextBox && control.as().AcceptsReturn(); auto shouldSubmit = !args.Handled(); @@ -660,7 +660,7 @@ void TextInputShadowNode::updateProperties(winrt::Microsoft::ReactNative::JSValu // We need to re-register the PreviewKeyDown handler so it is invoked after the ShadowNodeBase handler if (hasKeyDownEvents) { - m_controlKeyDownRevoker.revoke(); + m_controlPreviewKeyDownRevoker.revoke(); registerPreviewKeyDown(); } From eb517c8d725b1b8184ee71a96e9850a0b3fcddb7 Mon Sep 17 00:00:00 2001 From: Eric Rozell Date: Fri, 9 Apr 2021 12:30:58 -0400 Subject: [PATCH 6/6] Updates TextInput prop type for RNW and adds RNTester example --- .../TextInput/TextInputExample.windows.js | 33 +++++++++++++++++++ .../Components/TextInput/TextInput.windows.js | 27 +++++++++++++++ 2 files changed, 60 insertions(+) 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/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.