Skip to content

Conversation

@rozele
Copy link
Contributor

@rozele rozele commented May 7, 2021

Previously, TextInputViewManager subscribed to two events to implement onChangeText:

  1. TextChanging - used to increment a native counter to ensure JS changes to text do not overwrite native changes to text.
  2. TextChanged - used to emit the onChangeText event and surprisingly also incremented the native counter.

Additionally, the view manager subscribed to SelectionChanged to emit the onSelectionChange event.

In XAML, the sequence of native events is as follows:

  1. TextChanging
  2. SelectionChanged
  3. TextChanged

Since we emit the event that ultimately triggers onChangeText via the TextChanged native event, this means that we actually receive the cursor position change before the text change. This is different from other React Native platforms and makes it more difficult to implement behaviors based off the seonSelectionChange event.

One concern with this change is if there are any repercussions of eliminating the two phased approach to the native counter and firing of the event. Here is why I don't believe this will create any issues:

  1. Previously, we incremented m_nativeEventCount twice, once in TextChanging and once in TextChanged. Reducing this to incrementing only once will not introduce any bugs, as JavaScript does not increment the mostRecentEventCount prop, so the only requirement is that a native change to the TextBox content increases the count.

  2. Previously, we emitted the event in TextChanged. It appears that the TextBox.Text() property is already updated at the time that TextChanging is called, whether the Text() property was set externally or it was updated internally by a key event. The only difference between the two events is that TextChanged is called after the text is rendered (which may occur asynchronously from the actual text change). This is the reason we needed to introduce the dependency on TextChanging in the first place, as TextChanging is guaranteed to be called synchronously with the value change (and could not be interrupted by, e.g., an update from JS).

  3. Is it a problem that JavaScript receives the text changed event before it actually renders? This is a good question, but I can't think of any scenarios where it's important that the TextBox has actually rendered the change before emitting the event, and since JS runs asynchronously from the UI thread, it's likely that by the time any UI changes / side effects triggered by JS make their way back to the UI thread, the text will have been rendered.

Fixes #7740

Microsoft Reviewers: Open in CodeFlow

Previously, TextInputViewManager subscribed to two events to implement
`onChangeText`:
1. TextChanging - used to increment a native counter to ensure JS
changes to text do not overwrite native changes to text.
2. TextChanged - used to emit the `onChangeText` event and surprisingly
also incremented the native counter.

Additionally, the view manager subscribed to SelectionChanged to emit
the `onSelectionChange` event.

In XAML, the sequence of native events is as follows:
1. TextChanging
2. SelectionChanged
3. TextChanged

Since we emit the event that ultimately triggers `onChangeText` via the
TextChanged native event, this means that we actually receive the cursor
position change before the text change. This is different from other
React Native platforms and makes it more difficult to implement
behaviors based off the se`onSelectionChange` event.

One concern with this change is if there are any repercussions of
eliminating the two phased approach to the native counter and firing of
the event. Here is why I don't believe this will create any issues:

1. Previously, we incremented `m_nativeEventCount` twice, once in
`TextChanging` and once in `TextChanged`. Reducing this to incrementing
only once will not introduce any bugs, as JavaScript does not increment
the `mostRecentEventCount` prop, so the only requirement is that a
native change to the TextBox content increases the count.

2. Previously, we emitted the event in `TextChanged`. It appears that
the `TextBox.Text()` property is already updated at the time that
`TextChanging` is called, whether the `Text()` property was set
externally or it was updated internally by a key event. The only
difference between the two events is that `TextChanged` is called after
the text is rendered (which may occur asynchronously from the actual
text change). This is the reason we needed to introduce the dependency
on `TextChanging` in the first place, as `TextChanging` is guaranteed to
be called synchronously with the value change (and could not be
interrupted by, e.g., an update from JS).

3. Is it a problem that JavaScript receives the text changed event
before it actually renders? This is a good question, but I can't think
of any scenarios where it's important that the TextBox has actually
rendered the change before emitting the event, and since JS runs
asynchronously from the UI thread, it's likely that by the time any UI
changes / side effects triggered by JS make their way back to the UI
thread, the text will have been rendered.

Fixes microsoft#7740
@rozele rozele requested a review from a team as a code owner May 7, 2021 15:57
@NickGerleman
Copy link
Contributor

Any ideas on how we could test this sort of change?

@rozele
Copy link
Contributor Author

rozele commented May 7, 2021

Any ideas on how we could test this sort of change?

It would probably be pretty trivial to create a test with an event counter, simulate typing a character and assert that the event count for onSelectionChange is less than the event count for onChange.

I'm not sure how WinAppDriver works, but if there's a way to simulate typing a character (that would also move the cursor forward), then I should be able to add an e2e test for this.

@NickGerleman
Copy link
Contributor

We are able to simulate typing into a TextInput. See below for an example:

async function setUsername(username: string) {
const usernameField = await $('~username-field');
await usernameField.setValue(username);
}

@rectified95
Copy link
Contributor

Make sure this doesn't break the recently fixed scenario in #7658

@rozele
Copy link
Contributor Author

rozele commented May 7, 2021

Make sure this doesn't break the recently fixed scenario in #7658

I don't believe it does for the reasons I documented. This really shouldn't change anything other than the order of events, since I don't believe we should have had any dependencies on the onChange(Text) event firing after rendering as opposed to synchronously with the text change.

@rozele
Copy link
Contributor Author

rozele commented May 11, 2021

@NickGerleman I added an E2E test for this. Please take another look.

@rectified95 rectified95 requested a review from a team May 12, 2021 00:31
@NickGerleman
Copy link
Contributor

@rozele you will have to run yarn lint:fix in the repo root to fix prettier formatting errors. If you have the eslint vscode extensions configured this formatting should be automatic on save.

@NickGerleman NickGerleman added the AutoMerge Causes a PR to be automatically merged once all requirements are passed (label drives bot activity) label May 12, 2021
@ghost
Copy link

ghost commented May 12, 2021

Hello @NickGerleman!

Because this pull request has the AutoMerge label, I will be glad to assist with helping to merge this pull request once all check-in policies pass.

p.s. you can customize the way I help with merging this pull request, such as holding this pull request until a specific person approves. Simply @mention me (@msftbot) and give me an instruction to get started! Learn more here.

@ghost ghost merged commit a7e7f54 into microsoft:master May 12, 2021
This pull request was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AutoMerge Causes a PR to be automatically merged once all requirements are passed (label drives bot activity)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TextInput onChangeText and onSelectionChange out of order

3 participants