Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: non-editable wrapped TextInput events #1385

Conversation

TMaszko
Copy link
Contributor

@TMaszko TMaszko commented Apr 12, 2023

Summary

[Description by @mdjastrzebski]
This PR modifies the fireEvent handling for TextInputs by checking the editable prop status of the nearest touch responder of given element. The nearest touch responder is the nearest descendant element that is both host element as well as advertises as touch responder. One such element is TextInput.

The previous version of the code, checked the editable prop status on the element itself, which could lead to situations when TextInput wrappers would execute events, e.g. changeText, focus, etc on non-editable TextInput, because the check has been limited to host TextInput and wrapping it composite TextInput coming from RN package. Any additional TextInput wrapper would not have it's editable prop checked and would refer to host TextInput editable prop.

During experiments conducted on iOS and Android, I've discovered that most of the events are blocked by editable={false} prop, but some events like layout, contentSizeChange, etc are being called anyway. This PR includes special exclusion for such events.

Fixes #1384
Fixes #1261

Test plan

Green tests

src/fireEvent.ts Outdated
@@ -56,6 +56,9 @@ const isEventEnabled = (
eventName?: string
) => {
if (isTextInput(element)) return element?.props.editable !== false;
if (isTextInput(touchResponder)) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm not too sure about this. It will fix this issue indeed but saying that no event should be fired if at any point we pass through a non editable TextInput is not really correct either. For instance if I did a fireEvent.press on a non editable TextInput that's wrapped by a Pressable the press would not be run while it should. This may be a lesser evil still but there may be some other problems that we haven't thought of

Copy link
Collaborator

Choose a reason for hiding this comment

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

In the end fireEvent is just a low level API that calls props and we try to do too much with it so it has many unexpected behavior. How I see it long term is to fix this kind of issues using the new API userEvent but it's still at its early stage. It's not ideal that we can change text of non editable TextInputs but maybe to test those cases the easiest would be to use a custom matcher instead

Copy link
Contributor Author

@TMaszko TMaszko Apr 13, 2023

Choose a reason for hiding this comment

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

If we want to call fireEvent.press on a non editable TextInput that's wrapped by a Pressable the press handler shouldn't be called (at least that's the behaviour that user will experience and we already have in the browser). If touch responder isn't able to handle the event I think that's enough because it means that we base on the host component props in the first place and that should match the end user experience.

Copy link
Member

Choose a reason for hiding this comment

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

@pierrezimmermannbam could you post some test cases that you suspect wouldn't work so that @TMaszko could include in this PR? With fireEvent and handling Responder API we kinda reverse-engineer the event system of RN, which may actually change depending on the platform (especially web), so expect edge cases that would be great to understand and document

Copy link
Collaborator

Choose a reason for hiding this comment

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

The following test will fail

test('can pres pressable that wraps non editable textinput', () => {
    const mockOnPress = jest.fn();

    const { getByPlaceholderText } = render(
      <Pressable onPress={mockOnPress}>
        <TextInput placeholder="hello" editable={false} />
      </Pressable>
    );

    fireEvent.press(getByPlaceholderText('hello'));

    expect(mockOnPress).toHaveBeenCalled();
  });

We consider all text inputs to be touch responders, so when we do a press targeting a TextInput, it is considered to be the touch responder, even though it may be non editable. So we fix a bug but introduce another, possibly more. I'm fine with this pr being merged, it looks like a lesser evil but I feel like we're trying to extend too much the fireEvent API so it will always be flawed and it should in my opinion be considered as just an API that calls a prop (in fact I think it should be renamed to fireProp). For the long term i believe we need the userEvent API to really fix those issues and keep the fireEvent API only as a way to call custom props.

Copy link
Collaborator

Choose a reason for hiding this comment

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

The main issue with fireEvent is that is has almost the same behavior no matter what prop we call, so we disable all events on non editable text inputs for instance, but we should still be able to call onPressIn

test('can trigger onPressIn on disabled textinput', () => {
    const mockOnPressIn = jest.fn();

    const { getByPlaceholderText } = render(
      <TextInput
        placeholder="hello"
        editable={false}
        onPressIn={mockOnPressIn}
      />
    );

    fireEvent(getByPlaceholderText('hello'), 'pressIn');

    expect(mockOnPressIn).toHaveBeenCalled();
  });

This test fails as well but it shouldn't

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't get why this test should pass. Isn't it a proper behaviour to block events when component is basically disabled?

Copy link
Collaborator

Choose a reason for hiding this comment

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

The editable prop controls wether the value of the input can be modified or not but it still responds to touch events when it's not editable. You can try to press a non editable text input, the onPressIn prop is called in both cases

Copy link
Member

@mdjastrzebski mdjastrzebski Apr 23, 2023

Choose a reason for hiding this comment

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

@TMaszko, @pierrezimmermannbam I've been curious to get the understanding of how the TexInput events actually work when running in a regular RN app. I've build a small experiments app to test it, and submitted it as PR #1391.

The relevant experiment code is here: https://github.com/callstack/react-native-testing-library/pull/1391/files#diff-800ebe5060dbf16ec52e1cf9c65e7c2fa0f0cd86af32dcfdf913e13576831a70

For editable: false scenario both onPressIn and onPressOut events are being triggered on iOS:

 LOG  Event: pressIn {"changedTouches": [[Circular]], "identifier": 1, "locationX": 57.33332824707031, "locationY": 35, "pageX": 77.33332824707031, "pageY": 146, "target": 117, "timestamp": 707054389.9047917, "touches": [[Circular]]}
 LOG  Event: pressOut {"changedTouches": [[Circular]], "identifier": 1, "locationX": 57.33332824707031, "locationY": 35, "pageX": 77.33332824707031, "pageY": 146, "target": 117, "timestamp": 707054477.4037917, "touches": []}

But they are not being called on Android(!).

As for original @TMaszko issue #1384, it is correct in the respect that non-editable TextInput does not rise the focus/blur events.

I'm yet to prepare experiments involving Pressable.

Copy link
Member

@mdjastrzebski mdjastrzebski Apr 23, 2023

Choose a reason for hiding this comment

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

I've also created a second test case, when non-editable TextInput is wrapped in Pressable:

<Pressable
  onPress={handlePressablePress}
  onPressIn={handlePressablePressIn}
  onPressOut={handlePressablePressOut}
  style={styles.pressable}
>
  <TextInput
    style={styles.textInput}
    value={value}
    onChangeText={handleChangeText}
    editable={false}
    onPressIn={handlePressIn}
    onPressOut={handlePressOut}
  />
</Pressable>

In this cases, when I press on the TextInput I get following events on iOS:

 LOG  Event: pressIn {"changedTouches": [[Circular]], "identifier": 1, "locationX": 165.66665649414062, "locationY": 16.333328247070312, "pageX": 205.66665649414062, "pageY": 147.3333282470703, "target": 65, "timestamp": 717273074.8629583, "touches": [[Circular]]}
 LOG  Event: pressOut {"changedTouches": [[Circular]], "identifier": 1, "locationX": 165.66665649414062, "locationY": 16.333328247070312, "pageX": 205.66665649414062, "pageY": 147.3333282470703, "target": 65, "timestamp": 717273155.2179583, "touches": []}

So TextInputs pressIn/pressOunt event are being invoked.

On the other hand on Android I get following events:

 LOG  Event: Pressable.pressIn {"changedTouches": [[Circular]], "identifier": 0, "locationX": 146.18182373046875, "locationY": 60.727272033691406, "pageX": 146.18182373046875, "pageY": 140.72727966308594, "target": 795, "targetSurface": -1, "timestamp": 514525, "touches": [[Circular]]}
 LOG  Event: Pressable.press {"changedTouches": [[Circular]], "identifier": 0, "locationX": 146.18182373046875, "locationY": 60.727272033691406, "pageX": 146.18182373046875, "pageY": 140.72727966308594, "target": 795, "targetSurface": -1, "timestamp": 514619, "touches": []}
 LOG  Event: Pressable.pressOut {"changedTouches": [[Circular]], "identifier": 0, "locationX": 146.18182373046875, "locationY": 60.727272033691406, "pageX": 146.18182373046875, "pageY": 140.72727966308594, "target": 795, "targetSurface": -1, "timestamp": 514619, "touches": []}

So now it's Pressabless pressIn/pressOut event are being invoked.

There is a surprising and significant difference the platforms.

@mdjastrzebski
Copy link
Member

mdjastrzebski commented Apr 23, 2023

Based on the experiments described here, and here I've came to the conclusion that there is a difference between iOS and Android in terms of handling the editable prop. On Android, making TextInput non-editable seems to prevent all events (incl. pressIn) but on iOS it does seem to prevent some events (e.g. focus) but not others (e.g. pressIn).

I hope that will help us focus the review discussion. Regarding the idea of checking touch responder editable prop in case of touch responder being TextInput that could still be a valid case but deserves more consideration, and probably some more tests.

As per @pierrezimmermannbam comment, the touch responder variable is being reset when walking up the element tree a new touch responder is encountered. So in the Pressable example given by Pierre, the host View rendered by Pressable would become the nearest touch responder when found. However, as shown above, this does not happen because even non-editable TextInput is still a valid touch responder for pressIn event.

@mdjastrzebski
Copy link
Member

mdjastrzebski commented Apr 23, 2023

I think we need to figure out what to do with the discrepancy between iOS and Android platforms mentioned above. Both approaches seems to have a reasoning behind them, but they are not compatibile with each other. At the moment we do not support separate simulations for iOS and Android, we have one unified event processing.

The good part is that the mentioned events (pressIn,pressOut) are relatively minor for TextInput, and the major events are being ignored. The bad thing is that the event propagation also differs, as on iOS "consumes" the event preventing further propagation, while on Android the event is not "consumed" and it looks for another responder.

@pierrezimmermannbam, @TMaszko I am curious your thoughts on this topic.

@mdjastrzebski mdjastrzebski changed the title fix: wrapped TextInput events fix: non-editable TextInput events Apr 24, 2023
@mdjastrzebski mdjastrzebski changed the title fix: non-editable TextInput events fix: non-editable wrapped TextInput events Apr 24, 2023
@mdjastrzebski
Copy link
Member

mdjastrzebski commented Apr 24, 2023

@TMaszko I've added tests based on #1261 that I believe should be part of this PR. They basically check if focus, changeText, submitEditing events are prevented on non-editable TextInput while layout event is allowed. We would need to add some kind of inclusion / exclusions list of events blocked by editable={false}.

Note: the new test are failing, specifically because layout event is being blocked.

@pierrezimmermannbam
Copy link
Collaborator

@mdjastrzebski this is very interesting, I hadn't done tests on Android, it's quite surprising indeed! I'm wondering if we want to simulate all of this behavior, including the difference between platforms. I feel like we're hitting the limitations of testing in a node environment here, specially with TextInput component being mocked in React Native preset and it feels like we'd need to reimplement the TextInput for everything to work as expected so I'm not really sure it's worth it. Maybe we should just recommend using matchers from jest-native to test disabled pressables / text inputs? I guess the issue is that fireEvent already partially handles non editable text inputs or disabled pressables which gives the impression it handles all cases which is not true. Simplifying fireEvent could be an option but it would be a huge breaking change and it may not be understood so this may not be the best option. I think it would be better to implement all of this in userEvent instead and then keep fireEvent as just an API that calls props

@mdjastrzebski
Copy link
Member

@pierrezimmermannbam I am coming to the conclusion that if editable + touch propagation differs between the platforms, then the users should not rely on it working either iOS or Android way, as that would work only on as single platform. Therefore, we should be able to simulate it either way. It seems like going with Android approach would just be simpler, as we could shutdown all touch-related events when editable == false.

In the real world of RN apps, putting Pressable (or any other touch consuming container) around TextInput is rather rare, so we should not worry too much about this.

As for this PR, it seems to be improving our simulation by basing the editable check on nearest touch responder (i.e. host TextInput), rather than composite or host element. Let's add necessary tests, and perform necessary research using the new experiments app, but otherwise it seems like the right direction.

@mdjastrzebski mdjastrzebski force-pushed the fix/non-editable-wrapped-text-input-events branch 2 times, most recently from d5e19b4 to 7eeae11 Compare April 25, 2023 12:03
@mdjastrzebski
Copy link
Member

I've prepared the PR for review:

  • rebased on current main
  • moved text-input related test to separate file, as they started to grow
  • added support for allowing events not affected by editable prop to be called, e.g. layout
  • refactored the TextInput event check to take into account only nearest touch responder instead of checking the element itself
  • cleaned up the code

The subject is quite complex, so I am looking forward for a challenging review @pierrezimmermannbam, @MattAgn, @AugustinLF, @thymikee, @TMaszko :-)

@AugustinLF
Copy link
Collaborator

AugustinLF commented Apr 26, 2023

It seems like going with Android approach would just be simpler, as we could shutdown all touch-related events when editable == false.

That sounds like a fair tradeoff to me, we should however make sure that we document it.

Overall I agree that it seems like this PR makes it better. It's not perfect, but it's going to be a bit better.

@mdjastrzebski mdjastrzebski force-pushed the fix/non-editable-wrapped-text-input-events branch from 6af91e4 to 8065370 Compare April 27, 2023 09:06
@pierrezimmermannbam
Copy link
Collaborator

It seems like going with Android approach would just be simpler, as we could shutdown all touch-related events when editable == false.

That sounds like a fair tradeoff to me, we should however make sure that we document it.

Overall I agree that it seems like this PR makes it better. It's not perfect, but it's going to be a bit better.

I agree, I think this is a good tradeoff for now!

@mdjastrzebski mdjastrzebski force-pushed the fix/non-editable-wrapped-text-input-events branch from 8065370 to c6f2d96 Compare May 2, 2023 08:58
@mdjastrzebski
Copy link
Member

@pierrezimmermannbam @AugustinLF @MattAgn From my perspective this PR is ready for merging. I'm looking forward for code review approval or further change request for this PR.

Copy link
Member

@mdjastrzebski mdjastrzebski left a comment

Choose a reason for hiding this comment

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

LGTM but I've got quite involved in the PR, so looking for more objective reviewers.

Copy link
Collaborator

@AugustinLF AugustinLF left a comment

Choose a reason for hiding this comment

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

Left a small clarification regarding the test name/comment, but nothing that prevents from merging. And since I don't have a good suggestion (I'm not sure what exactly this test is testing), it's good to be merged for me :)

src/__tests__/fireEvent-textInput.test.tsx Show resolved Hide resolved
@mdjastrzebski mdjastrzebski force-pushed the fix/non-editable-wrapped-text-input-events branch from c6f2d96 to 81f216b Compare May 3, 2023 10:40
@codecov
Copy link

codecov bot commented May 4, 2023

Codecov Report

Patch coverage: 100.00% and no project coverage change.

Comparison is base (674b2f9) 96.85% compared to head (ab70817) 96.85%.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #1385   +/-   ##
=======================================
  Coverage   96.85%   96.85%           
=======================================
  Files          51       51           
  Lines        3398     3399    +1     
  Branches      506      507    +1     
=======================================
+ Hits         3291     3292    +1     
  Misses        107      107           
Impacted Files Coverage Δ
src/fireEvent.ts 100.00% <100.00%> (ø)

☔ View full report in Codecov by Sentry.
📢 Do you have feedback about the report comment? Let us know in this issue.

@mdjastrzebski mdjastrzebski merged commit a110fd8 into callstack:main May 4, 2023
@TMaszko TMaszko deleted the fix/non-editable-wrapped-text-input-events branch May 5, 2023 13:45
hyochan pushed a commit to dooboolab-community/react-native-iap that referenced this pull request Jun 22, 2023
….2 (#2445)

[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
|
[@testing-library/react-native](https://callstack.github.io/react-native-testing-library)
([source](https://togithub.com/callstack/react-native-testing-library))
| [`12.0.1` ->
`12.1.2`](https://renovatebot.com/diffs/npm/@testing-library%2freact-native/12.0.1/12.1.2)
|
[![age](https://badges.renovateapi.com/packages/npm/@testing-library%2freact-native/12.1.2/age-slim)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://badges.renovateapi.com/packages/npm/@testing-library%2freact-native/12.1.2/adoption-slim)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://badges.renovateapi.com/packages/npm/@testing-library%2freact-native/12.1.2/compatibility-slim/12.0.1)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://badges.renovateapi.com/packages/npm/@testing-library%2freact-native/12.1.2/confidence-slim/12.0.1)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>callstack/react-native-testing-library</summary>

###
[`v12.1.2`](https://togithub.com/callstack/react-native-testing-library/releases/tag/v12.1.2)

[Compare
Source](https://togithub.com/callstack/react-native-testing-library/compare/v12.1.1...v12.1.2)

#### What's Changed

#### Fixes

- fix: pointer events evaluation by
[@&#8203;mdjastrzebski](https://togithub.com/mdjastrzebski) in
[callstack/react-native-testing-library#1395
- fix: non-editable wrapped TextInput events by
[@&#8203;TMaszko](https://togithub.com/TMaszko) in
[callstack/react-native-testing-library#1385
- fix: support `onXxx` event name for TextInput event checks by
[@&#8203;mdjastrzebski](https://togithub.com/mdjastrzebski) in
[callstack/react-native-testing-library#1404

#### Docs, Chores, etc

- docs: add config example for pnpm by
[@&#8203;yjose](https://togithub.com/yjose) in
[callstack/react-native-testing-library#1400
- chore: move/remove deprecation functions by
[@&#8203;mdjastrzebski](https://togithub.com/mdjastrzebski) in
[callstack/react-native-testing-library#1402
- refactor: component tree dead code by
[@&#8203;mdjastrzebski](https://togithub.com/mdjastrzebski) in
[callstack/react-native-testing-library#1403
- refactor: `fireEvent` cleanup by
[@&#8203;mdjastrzebski](https://togithub.com/mdjastrzebski) in
[callstack/react-native-testing-library#1401

#### New Contributors

- [@&#8203;yjose](https://togithub.com/yjose) made their first
contribution in
[callstack/react-native-testing-library#1400
👏
- [@&#8203;TMaszko](https://togithub.com/TMaszko) made their first
contribution in
[callstack/react-native-testing-library#1385
👏

**Full Changelog**:
callstack/react-native-testing-library@v12.1.1...v12.1.2

###
[`v12.1.1`](https://togithub.com/callstack/react-native-testing-library/releases/tag/v12.1.1)

[Compare
Source](https://togithub.com/callstack/react-native-testing-library/compare/v12.1.0...v12.1.1)

#### What's Changed

#### Fixes

- fix: remove incorrect dependencies by
[@&#8203;mdjastrzebski](https://togithub.com/mdjastrzebski) in
[callstack/react-native-testing-library#1399

**Full Changelog**:
callstack/react-native-testing-library@v12.1.0...v12.1.1

###
[`v12.1.0`](https://togithub.com/callstack/react-native-testing-library/releases/tag/v12.1.0)

[Compare
Source](https://togithub.com/callstack/react-native-testing-library/compare/v12.0.1...v12.1.0)

#### What's Changed

##### Improvements

- feat: Render element tree in query error messages by
[@&#8203;stevehanson](https://togithub.com/stevehanson) in
[callstack/react-native-testing-library#1378

##### Bugfixes

- Proper stack trace for findBy\* and findAllBy\* queries by
[@&#8203;mdjastrzebski](https://togithub.com/mdjastrzebski) in
[callstack/react-native-testing-library#1394

#### New Contributors

- [@&#8203;stevehanson](https://togithub.com/stevehanson) made their
first contributions in
[#&#8203;1377](https://togithub.com/callstack/react-native-testing-library/issues/1377),
[#&#8203;1378](https://togithub.com/callstack/react-native-testing-library/issues/1378)
and
[#&#8203;1390](https://togithub.com/callstack/react-native-testing-library/issues/1390)
👏

##### Chores, docs, deps, etc

- Fix broken link in contributing.md by
[@&#8203;stevehanson](https://togithub.com/stevehanson) in
[callstack/react-native-testing-library#1377
- chore: update deps 2023-04-04 by
[@&#8203;mdjastrzebski](https://togithub.com/mdjastrzebski) in
[callstack/react-native-testing-library#1380
- Fix typo in "derived" in v12 migration guide by
[@&#8203;CodingItWrong](https://togithub.com/CodingItWrong) in
[callstack/react-native-testing-library#1376
- chore: fix migration guide role prop naming by
[@&#8203;mdjastrzebski](https://togithub.com/mdjastrzebski) in
[callstack/react-native-testing-library#1382
- fix: "Edit this Page" link in docs results in 404 by
[@&#8203;stevehanson](https://togithub.com/stevehanson) in
[callstack/react-native-testing-library#1390
- refactor: remove stale tests by
[@&#8203;mdjastrzebski](https://togithub.com/mdjastrzebski) in
[callstack/react-native-testing-library#1392
- chore: experiments app by
[@&#8203;mdjastrzebski](https://togithub.com/mdjastrzebski) in
[callstack/react-native-testing-library#1391

**Full Changelog**:
callstack/react-native-testing-library@v12.0.1...v12.1.0

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/dooboolab-community/react-native-iap).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNS45OC40IiwidXBkYXRlZEluVmVyIjoiMzUuMTMxLjAiLCJ0YXJnZXRCcmFuY2giOiJtYWluIn0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
6 participants