Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "Adds declarative props to clear/submit TextInput",
"packageName": "react-native-windows",
"email": "erozell@outlook.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -455,4 +455,37 @@ exports.examples = ([
return <PressInOutEvents />;
},
},
// [Windows
{
title: 'Clear text on submit',
render: function(): React.Node {
return (
<View>
<Text>Default submit key (Enter):</Text>
<TextInput clearTextOnSubmit style={styles.singleLine} />
<Text>Custom submit key event (Shift + Enter), single-line:</Text>
<TextInput
clearTextOnSubmit
style={styles.singleLine}
submitKeyEvents={[{code: 'Enter', shiftKey: true}]}
/>
<Text>Custom submit key event (Shift + Enter), multi-line:</Text>
<TextInput
multiline
clearTextOnSubmit
style={styles.multiline}
submitKeyEvents={[{code: 'Enter', shiftKey: true}]}
/>
<Text>Submit with Enter key, return key with Shift + Enter</Text>
<TextInput
multiline
clearTextOnSubmit
style={styles.multiline}
submitKeyEvents={[{code: 'Enter'}]}
/>
</View>
);
},
},
// Windows]
]: Array<RNTesterExampleModuleItem>);
6 changes: 3 additions & 3 deletions vnext/Microsoft.ReactNative/Views/KeyboardEventHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<HandledKeyboardEvent> const &handledEvents,
HandledKeyboardEvent currentEvent) {
for (auto const &event : handledEvents) {
Expand Down
6 changes: 3 additions & 3 deletions vnext/Microsoft.ReactNative/Views/KeyboardEventHandler.h
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,6 @@ class HandledKeyboardEventHandler {
KeyboardEventPhase phase,
winrt::IInspectable const &sender,
xaml::Input::KeyRoutedEventArgs const &args);
bool ShouldMarkKeyboardHandled(
std::vector<HandledKeyboardEvent> const &handledEvents,
HandledKeyboardEvent currentEvent);

std::vector<HandledKeyboardEvent> m_handledKeyUpKeyboardEvents;
std::vector<HandledKeyboardEvent> m_handledKeyDownKeyboardEvents;
Expand All @@ -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<HandledKeyboardEvent> const &handledEvents,
HandledKeyboardEvent currentEvent);
};
} // namespace Microsoft::ReactNative
96 changes: 80 additions & 16 deletions vnext/Microsoft.ReactNative/Views/TextInputViewManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<HandledKeyboardEvent> 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
Expand All @@ -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{};
Expand Down Expand Up @@ -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<xaml::Controls::TextBox>().Text()));
} else {
eventDataSubmitEditing = folly::dynamic::object("target", tag)(
"text", react::uwp::HstringToDynamic(control.as<xaml::Controls::PasswordBox>().Password()));
}
GetViewManager()->GetReactContext().DispatchEvent(
tag, "topTextInputSubmitEditing", std::move(eventDataSubmitEditing));
}
});
registerPreviewKeyDown();

if (m_isTextBox) {
auto textBox = control.as<xaml::Controls::TextBox>();
Expand Down Expand Up @@ -373,6 +362,61 @@ void TextInputShadowNode::registerEvents() {
true);
}

void TextInputShadowNode::registerPreviewKeyDown() {
auto control = GetView().as<xaml::Controls::Control>();
auto tag = m_tag;
m_controlPreviewKeyDownRevoker =
control.PreviewKeyDown(winrt::auto_revoke, [=](auto &&, xaml::Input::KeyRoutedEventArgs const &args) {
auto isMultiline = m_isTextBox && control.as<xaml::Controls::TextBox>().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<xaml::Controls::TextBox>().Text()));
} else {
eventDataSubmitEditing = folly::dynamic::object("target", tag)(
"text", react::uwp::HstringToDynamic(control.as<xaml::Controls::PasswordBox>().Password()));
}

if (m_shouldClearTextOnSubmit) {
if (m_isTextBox) {
control.as<xaml::Controls::TextBox>().ClearValue(xaml::Controls::TextBox::TextProperty());
} else {
control.as<xaml::Controls::PasswordBox>().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;
Expand Down Expand Up @@ -423,6 +467,7 @@ void TextInputShadowNode::updateProperties(winrt::Microsoft::ReactNative::JSValu
auto control = GetView().as<xaml::Controls::Control>();
auto textBox = control.try_as<xaml::Controls::TextBox>();
auto passwordBox = control.try_as<xaml::Controls::PasswordBox>();
auto hasKeyDownEvents = false;

for (auto &pair : props) {
const std::string &propertyName = pair.first;
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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) {
Copy link
Contributor

@rectified95 rectified95 Apr 6, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this necessary? I thought Subclass::UpdateProperties gets called after the ShadowNodeBase keyboard handler gets bound in the PreviewKeyboardEventHandlerOnRoot constructor?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes - this is necessary. Although the root-level keyboard event handler for emitting key events gets subscribed before any of these props get set, the view-level keyboard event handler that sets Handled(true) on the routed events gets subscribed in Super::updateProperties.

If we subscribe to PreviewKeyDown for onSubmitEditing before we subscribe to PreviewKeyDown for keyDownEvents, then we'll always call onSubmitEditing, even if the key was intended to be marked Handled by the keyDownEvents prop.

Note, this wasn't a problem before because you could just use keyDownEvents={[{code: 'Enter', handledEventPhase: Capturing}]} to mark the event as Handled in the preview phase, and the KeyDown event that was previously used for onSubmitEditing would not be called.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it. Then, I have a few questions:

  1. Since UpdateProperties will be called at least once (and potentially many times during the lifespan of the component), do we need still need the initial registration up in registerEvents()? Not sure how expensive these are, though..
  2. This seems to result in the re-registration of the Preview handler every time that UpdateProperties is called for a TextInput with keyDownEvents specified - can we avoid that?
  3. Just to confirm - do you need to switch from listening to KeyDown to PreviewKeyDown, so that you can stop the submission on 'Enter'?

Copy link
Contributor Author

@rozele rozele Apr 7, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. We still need the initial registration in case keyDownEvents is never set.
  2. I don't believe it's that expensive, but it will only be re-registered any time someone changes keyDownEvents, which is likely to be infrequent. It's probably not worth the added complexity to add a state field that signals when we've already re-registered.
  3. Not exactly. We now need to handle onSubmitEditing in PreviewKeyDown as opposed to KeyDown because we want submitKeyEvents to apply even to TextBox with AcceptsReturn == true. If we used KeyDown, there would be no way to intercept 'Enter' before it created a newline character.

Copy link
Contributor

@rectified95 rectified95 Apr 16, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we subscribe to PreviewKeyDown for onSubmitEditing before we subscribe to PreviewKeyDown for keyDownEvents, then we'll always call onSubmitEditing, even if the key was intended to be marked Handled by the keyDownEvents prop.

Just for my information - this relies on the event handlers being called in the order of their registration for a given event type - is that guaranteed not to change (and documented somewhere)?

m_controlPreviewKeyDownRevoker.revoke();
registerPreviewKeyDown();
}

m_updating = false;
}

Expand Down Expand Up @@ -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(
Expand Down
27 changes: 27 additions & 0 deletions vnext/src/Libraries/Components/TextInput/TextInput.windows.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<SubmitKeyEvent>,
|}>;

// Windows]

export type Props = $ReadOnly<{|
...$Diff<ViewProps, $ReadOnly<{|style: ?ViewStyleProp|}>>,
...IOSProps,
...AndroidProps,
...WindowsProps, // [Windows]

/**
* Can tell `TextInput` to automatically capitalize certain characters.
Expand Down