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

Send Modal onDismiss event on iOS (Fabric) and Android #42014

Closed

Conversation

bernhardoj
Copy link
Contributor

Summary:

  1. Modal onDismiss is not working on iOS (Fabric).
  2. Modal onDismiss is currently only available on iOS. On Android, we don't have a way to know when exactly a modal is dismissed.

Currently, the onDismiss is emitted using a device event as a workaround to the RCTModalHostView unable to receive the component event as it's already unmounted when visible is false.

This PR removes the workaround and keeps RCTModalHostView mounted until the onDismiss event is emitted from the host and sends the onDismiss event on Android.

Changelog:

[ANDROID] [ADDED] - Added support for Modal onDismiss prop
[IOS] [FIXED] - Fix onDismiss is not working on Fabric

Test Plan:

  1. Run rn-tester
  2. Open the Modal example
  3. The second example shows the counter for the show and dismiss count
  4. Show and dismiss the modal and verify the count is incremented correctly
Screen.Recording.2023-12-20.at.23.55.29.mov

@facebook-github-bot facebook-github-bot added CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. labels Dec 20, 2023
@bernhardoj
Copy link
Contributor Author

This PR is identical to #38336. I closed #38336 and was planning to split it into smaller parts, but I found it's not possible to split.

I also hope the new PR will attract more attention since the old one has been stale for a few months.

(btw, the old PR fork is deleted)
(sorry in advance if this is something that I shouldn't do (closing and reopening a new PR))

@analysis-bot
Copy link

analysis-bot commented Dec 20, 2023

Platform Engine Arch Size (bytes) Diff
android hermes arm64-v8a 16,565,549 -16,499
android hermes armeabi-v7a n/a --
android hermes x86 n/a --
android hermes x86_64 n/a --
android jsc arm64-v8a 19,942,644 -12,389
android jsc armeabi-v7a n/a --
android jsc x86 n/a --
android jsc x86_64 n/a --

Base commit: e2fb88e
Branch: main

label="onDismiss ⚫️"
disabled={Platform.OS !== 'ios'}
label="onDismiss"
multiSelect={true}
Copy link
Contributor

Choose a reason for hiding this comment

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

why multiselect must be set to true?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's to enable RNTOption

disabled={
props.disabled === false || props.multiSelect === true
? false
: props.selected
}

I see that onShow passes multiSelect={true} instead of disable={false}, so I follow it.

<RNTOption
key="onShow"
style={styles.option}
label="onShow"
multiSelect={true}

Do you think I should change it to disable={false}?

Copy link
Contributor

Choose a reason for hiding this comment

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

yeah, probably it is more clear and accurate. I think that multiSelect is the API that allows you to select multiple choices (e.g.: "Which ice cream flavour do you prefer?" you can reply with "Chocolate", "cream" and "vanilla" and leave "strawberry" out) but in our case is is true/false and it does not make sense to select both.
WDYT?

Copy link
Contributor Author

@bernhardoj bernhardoj Dec 28, 2023

Choose a reason for hiding this comment

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

I think that multiSelect is the API that allows you to select multiple choices

This is what I thought when saw it the first time and I totally agree using disabled makes more sense. Updated the code.

Copy link
Contributor

@cipolleschi cipolleschi left a comment

Choose a reason for hiding this comment

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

It generally looks good to me. I also love that you removed more code than you added and the overall logic looks simpler to me.
Great job! 👏

@cipolleschi
Copy link
Contributor

/rebase - this comment automatically rebase the PR on top of main.

@github-actions github-actions bot force-pushed the fix/modal-ondismiss-event branch from fe01468 to 8334aae Compare December 27, 2023 16:44
@facebook-github-bot
Copy link
Contributor

@cipolleschi has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator.

@cipolleschi
Copy link
Contributor

cipolleschi commented Jan 2, 2024

Quick update on this PR. The changes implemented here are making several internal jobs fail with our CI.

This is a change we do want, but it will require a little bit more work on our side to make it happen. How urgent is this for you? Is it blocking your workflow?

@bernhardoj
Copy link
Contributor Author

@cipolleschi thanks for the update. It's not an urgent one nor blocking any workflow.

if (this._eventSubscription) {
this._eventSubscription.remove();
componentDidUpdate(prevProps: Props) {
if (prevProps.visible !== true && this.props.visible === true) {
Copy link
Contributor

Choose a reason for hiding this comment

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

What is the purpose of `isRendering?

isRendering is mapped to value of visible prop.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

isRendering is being used to keep the Modal rendered until the onDismiss callback is called.

Copy link
Contributor

Choose a reason for hiding this comment

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

What if the parent component unmounts Modal?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In that case, onDismiss won't be called.

@facebook-github-bot
Copy link
Contributor

@cipolleschi merged this pull request in 314cfa8.

@facebook-github-bot facebook-github-bot added Merged This PR has been merged. Reverted labels Jan 16, 2024
@facebook-github-bot
Copy link
Contributor

This pull request has been reverted by a58ec07.

@bernhardoj
Copy link
Contributor Author

@cipolleschi hi, I see that the PR has been reverted. May I know why it was reverted and is there anything I can do to help? Thanks!

@cipolleschi
Copy link
Contributor

Hi there, I was about to write.
We had to revert it because we had some callsites internally that started failing over the weekend, causing an important drop in some widely used surfaces.
Sadly, the problem is that we had some Android code which was relying on the fact that the event was not fired on that platform. 🤦

One of the problems is that the change was quite big (changing both platforms, changing public APIs, ...). This kind of changes are always a little risky as the more code we change the higher the probability of something to go wrong.

What we can do to make it easier for these changes to stick would be to split this PR in smaller chunks and to try and not change the public APIs.
I'll try to figure out how to split it properly

@bernhardoj
Copy link
Contributor Author

@cipolleschi hi, hope you are doing well. I would like to know if Is there any update on the split plan.

From me, I'm thinking that perhaps we can add a new props called useDismissEventFromNativeComponent that will use either the current ModalEventEmitter or the new approach from this PR.

The native component itself will fire onDismiss and ModalEventEmitter.modalDismissed (for iOS) events, so the conditional logic is only applied to the JS component (Modal.js).

For the split part,

  1. Start adding the Android dismiss native code. We can't test the dismiss event, but we know that it works in this PR, so I think it's fine.
  2. Next, we add the iOS dismiss native code. Same as above
  3. Last, we apply the conditional logic in the JS file, for example
componentDidMount() {
  if (ModalEventEmitter) {
    this._eventSubscription = ModalEventEmitter.addListener(
      'modalDismissed',
      event => {
        if (!this.props.useDismissEventFromNativeComponent && event.modalID === this._identifier && this.props.onDismiss) {
          this.props.onDismiss();
        }
      },
    );
  }
}

What do you think?

@bernhardoj
Copy link
Contributor Author

@cipolleschi hi, do you have any updates?

@cipolleschi
Copy link
Contributor

Hi @bernhardoj, sorry for the late reply, but I was quite busy with other high priority stuff and I had little to no time to look into this.

For iOS, I think everything is done now:

  • In the Old Architecture, onDismiss relies on the old EventEmitter and it works.
  • In the New Architecture, onDismiss uses the proper approach with the events and it works.

The problem, as you might expect, comes with Android.
Unfortunately, I don't think we can make it work properly with that platform.
Apparently, it turns out that, internally, we have some Android code that relies on the event not being fired. Start firing it will cause major issues with the internal codebase:

  • it will take a lot of work and time to merge it
  • it will be reverted multiple times, basically, every time an internal app will fail due to this change.

There are 2 stepswe can pursue:

  1. We create a PR in the discussion-and-proposal repo and we get the buy in from various people, including internal people, to make this happens in Android.
  2. While we wait for the above, you can create a component based on <Modal> with the fix. Something like <EXPModal> and include that component as a dependency of Expensify. Even better if, rather than copying and pasting the code, you manage to have a component that extends the native classes of Modal so that when we push some improvements, your component benefits from them too.

This will unblock both you and the app while the discussion proceeds.

How does that sound like?

@bernhardoj
Copy link
Contributor Author

In the New Architecture, onDismiss uses the proper approach with the events and it works.

Last time I tested, the onDismiss doesn't work because the JS component is unmounted before receiving the event.

In the Old Architecture, onDismiss relies on the old EventEmitter and it works.

But I think we should remove the old EventEmitter otherwise there will be double onDismiss call right?

While we wait for the above, you can create a component based on with the fix. ...

We actually use react-native-modal, so I think it's not possible to apply the suggestion (react-native-modal has onModalHide, but it's called too early). Do you think applying a patch while waiting for the proposal discussion is better?

@cipolleschi

@cipolleschi
Copy link
Contributor

Last time I tested, the onDismiss doesn't work because the JS component is unmounted before receiving the event.

What I meant is that, after your diff landed, and got reverted, I split the iOS part and manage to get it landed. So, the iOS side is done and it works as you (almost) did with your diff.

But I think we should remove the old EventEmitter otherwise there will be double onDismiss call right?

No, because The New Architecture does not use the old event emitter and the Old Architecture don't use the event which is Fabric only.
So there would be no double events.

We actually use react-native-modal, so I think it's not possible to apply the suggestion (react-native-modal has onModalHide, but it's called too early). Do you think applying a patch while waiting for the proposal discussion is better?

Yeah, that can work too! Patch-packages is a better temporary solution than forking react-native.

@bernhardoj
Copy link
Contributor Author

Ah, I see, thanks for clarifying it. I will discuss this with the team first. Thanks for your help!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Merged This PR has been merged. Reverted Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants