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.