react-native-modal / react-native-modal

An enhanced, animated, customizable Modal for React Native.
MIT License
5.48k stars 618 forks source link

Tap outside to close #11

Closed Doko-Demo-Doa closed 7 years ago

Doko-Demo-Doa commented 7 years ago

Is there a way to implement that function? Right now when I tap on the darkened region, the modal doesn't close.

mmazzarolo commented 7 years ago

Hey!
It should be possible by toying a bit with the touchable options of this View.
Would you mind giving it a try and submitting a PR?
Otherwise I'll take a look at it in my spare time :)

DavidKongDesheng commented 7 years ago

😆 pls add this function~~ thanks

Doko-Demo-Doa commented 7 years ago

Thank you @mmazzarolo , actually I fixed using exactly same method you mentioned. It works, but since the first image in your preview images (on README.md) does it correctly, I thought it was implemented already, wasn't it?

DavidKongDesheng commented 7 years ago

@Doko-Demo-Doa according to the source code, haven't implemented yet, there is no close event on backdrop view or you can do sth like this, not so good but should be work.

<TouchableWithoutFeedback onPress={() => this.setState({ visible: false })}>
        <Modal isVisible={this.state.visible}  ...>
            <TouchableWithoutFeedback onPress={() => {}}>
                    ... your custom view
            </TouchableWithoutFeedback
       </Modal>
</TouchableWithoutFeedback>
mmazzarolo commented 7 years ago

@Doko-Demo-Doa oh wow, honestly I don't remember how I implemented it for that screenshot (I did it in a customized version of this lib 😞 ).

@DavidKongDesheng the problem with that solution is that clicking everywhere on the modal will close it. In my opinion the right solution would be using onStartShouldSetResponderCapture (or something similar in the right place.
The perfect solution would be that:

What do you think guys?

Also, thank you @Doko-Demo-Doa and @DavidKongDesheng for the discussion, any feedback is of course warmly welcomed! 😸

@Doko-Demo-Doa could you share with us your solution?

mmazzarolo commented 7 years ago

I played a bit with it.
It seems that the right way to handle it is by applying something like this:

onStartShouldSetResponder={() => {
  console.log('You pressed the backdrop!');
  return true;
}}

to the backdrop view.

The problem with this approach is that it won't work correctly if you're centering the modal content using this:

content: {
    flex: 1,
    justifyContent: 'center'
  }

...which is the default style applied to the content.

I guess we should wait until react-native 0.43 stable lands and try changing that style to the new margin: auto, which I hope will make the backdrop press work correctly.

I'm obviously open to discussions on the matter :)

DavidKongDesheng commented 7 years ago

@mmazzarolo On my solution I added two touch events, one for Modal itself which will close the modal, the other one is on the Modal content view which will do nothing 😸, so only when you click the backdrop will close the modal. <TouchableWithoutFeedback onPress={() => this.setState({ visible: false })}> <Modal isVisible={this.state.visible} ...> <TouchableWithoutFeedback onPress={() => {}}> ... your custom view </TouchableWithoutFeedback

Yup, the right solution should be make the backdrop respond to user touch.

mmazzarolo commented 7 years ago

Oh, nice! @DavidKongDesheng did you test your solution? Do you think it works correctly in most cases? Would you mind pasting here your complete snippet? Thanks!

DavidKongDesheng commented 7 years ago

@mmazzarolo Just added touch event to your example code, you can download and try, but this is not the best way to implement. app.js.zip

Doko-Demo-Doa commented 7 years ago

It seems that RN's modal component is pretty meh. If there's something like Dialog on Android or UIAlertView on iOS, it would be much better in this case :(

davidroman0O commented 7 years ago

Hi,

I did exactly the same thing as @DavidKongDesheng but still don't working on my side !

<TouchableWithoutFeedback onPress={() => this.setState({ modalVisible: false })}>
  <Modal transparent={true} ref="modal" visible={this.state.modalVisible} onRequestClose={this.close.bind(this)} animationType={this.state.animationType}>
    {this.renderOptionList()}
  </Modal>
</TouchableWithoutFeedback>

Or maybe i don't make it well... no ?

react-native 0.42.3

emalorenzo commented 7 years ago

Hi! For me the solution of @DavidKongDesheng works great 🔥 This is my code:

` constructor(props) { super(props); this.state = { isModalVisible: false } }

_showModal() { this.setState({ isModalVisible: true }) }

_hideModal() { this.setState({ isModalVisible: false }) }

render() { const { alerts, showAlert } = this.props; return (

this._hideModal() }> ....` Hope to be helpfull!
davidroman0O commented 7 years ago

@emaLorenzo Thanks! But it doesn't work :(

I don't know why it doesn't triggered the background :/

DavidKongDesheng commented 7 years ago

@warka0 Can't reproduce it according the attached codes, but it looks like you are using Modal from 'react-native' not from 'react-native-modal'

Or maybe try

  1. Remove onRequestClose, use onBackButtonPress instead
  2. Set transparent={false} and set 'red' as background color of your option list view, and check if really click on the background or somewhere else.
  3. Check your renderOptionList().

Or more codes attach...

jm90m commented 7 years ago

@emaLorenzo Thanks for your solution, this worked for me 👏 👍

kdenz commented 7 years ago

@DavidKongDesheng Your solution worked for me, thanks! I hope this functionality is added soon to react-native-modal, it's such an important function.

davidroman0O commented 7 years ago

I'll test it in two days. Cannot do it right now, i'm between two airports. :p

mmazzarolo commented 7 years ago

Hey, I'll try to take a look at it soon (PRs are obviously warmly welcome) :) I still haven't tried the touchable wrapper but... doesn't it swallow the click buttons inside the modal (if any)? 🤔

davidroman0O commented 7 years ago

Ok, i've tried and it's working, thanks!

mmazzarolo commented 7 years ago

@warka0, does it also work if you add a button inside the modal? Doesn't the TouchableWithoutFeedback wrapper on the modal swallow any touch to components inside it?

jm90m commented 7 years ago

@mmazzarolo So the modal is wrapped in TouchableWithoutFeedback but inside the modal I have a bunch of buttons wrapped around in TouchableOpacity and it works perfectly 👍

mmazzarolo commented 7 years ago

It was easier than I thought then :) Anyone willing to submit a PR?

jm90m commented 7 years ago

Sure I'll submit an example

mmazzarolo commented 7 years ago

@jm90m thank you for the PRs! However... Guys, with @emaLorenzo 's solution the modal is closed wherever you tap, while the issue was focused on closing the modal only when clicking on the backdrop 😿

jm90m commented 7 years ago

@mmazzarolo ohh, yeah looks like the main issue is more complex than it seems. I see, if I ever come across a solution, I'll submit a PR if no one has done so yet.

jzhan-canva commented 7 years ago

You guys can take a look at my repo, the idea is surrounding modal with 4 touchables. My initial solution was use a screen size touchable to perform tapToClose, a modal size touchable wrapping the modal to prevent the tap event goes up(no onPress). But then I found I can't use scrollView inside the touchable. In the end I came with up the solution I used in my repo

jzhan-canva commented 7 years ago

And also, by adjusting the flex value of the four touchables, we can change the position of the modal, which also solve #22 , I already used my modal to create a popover

mmazzarolo commented 7 years ago

Hey @zhantx , thank you for contributing to the discussion 👍
Glad there's someone else out there working with the modals.
It seems you're solution is super-flexible, at the cost of being a bit more harder to grasp to someone new to RN.
For this repo I'd like to keep the things as simple as possible, but your solution might be a good source of info for future implementations.
Also, I only took a quick look at your code, can the modal be showed without setting all the dimensions (for example: can it adapt itself to a flex content)? P.S.: PRs ans support are super welcome here if you're interested :)

jzhan-canva commented 7 years ago

Hey @mmazzarolo , yeah I've been trying hard to simplify my modal by setting default props. so if none props are set then it will be at the center of the screen. For you modal, to be honest adding tapToClose is impossible cuz your modal is controlled only by prop isVisible, so the modal can't close itself directly(that's the reason why I choose ref methods open() and close()). What I suggest is implement four touchables in your modal, and trigger an onTapBackdrop callback to let parent component to change the isVisible

mmazzarolo commented 7 years ago

Hey @zhantx, thanks!

so if none props are set then it will be at the center of the screen.

Does it also resize itself based on the content size? 🤔

For you modal, to be honest adding tapToClose is impossible cuz your modal is controlled only by prop isVisible, so the modal can't close itself directly

I'm not aiming at making the modal close itself directly, just to exposing an onBackdropPress listener, which can be used to close it from the modal's parent.

(that's the reason why I choose ref methods open() and close())

I'm trying to keep react-native-modal component as controlled as possible by staying away from refs method (at least until they're really needed).

jzhan-canva commented 7 years ago

@mmazzarolo , check this out Simulator Screen Shot 29 Apr 2017, 7.26.11 pm.png By using four touchables, they won't interference any touch event within the modal content

mmazzarolo commented 7 years ago

@zhantx this is super-cool, thanks!
I'll take a look at a way to implement a similar solution here as soon as I have some free time.
Obviously, backward-compatible PRs are welcome too.

jzhan-canva commented 7 years ago

@mmazzarolo you are welcome mate, glad to be helpful. my modal is just a side product of my work, may not have time to contribute yours anyway, best wishes with remaining works, cheers

schmaluk commented 7 years ago

Dont understand the Modal is taking the full screen for the backdrop overlay. So the solution doesnt work to wrap it inside a Touchable

jm90m commented 7 years ago

@zhantx very helpful thx. For my case, I only had to use 2 TouchableWithoutFeedback (One for wrapping around the entire Modal and another for wrapping around the modal content):

screen shot 2017-05-13 at 11 18 07 am
jzhan-canva commented 7 years ago

as long as you don't have scrollview within modal:)

jm90m commented 7 years ago

@zhantx nope its a simple menu, but good point :)

hellomaya commented 7 years ago

This worked for me, edit the index.js to add a PanResponder:

componentWillMount() {

    ......

        // for the backdrop touch to close
        this._panResponder = PanResponder.create({
            // Ask to be the responder:
            onStartShouldSetPanResponder: (evt, gestureState) => true,
            onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
            onMoveShouldSetPanResponder: (evt, gestureState) => true,
            onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,

            onPanResponderGrant: (evt, gestureState) => {
            },
            onPanResponderMove: (evt, gestureState) => {
            },
            onPanResponderTerminationRequest: (evt, gestureState) => true,
            onPanResponderRelease: (evt, gestureState) => {

                // close the modal
                this._close();
            },
            onPanResponderTerminate: (evt, gestureState) => {
            },
            onShouldBlockNativeResponder: (evt, gestureState) => {
                return true;
            },
        });
}

render() {
        ........
        <View
                    {...this._panResponder.panHandlers}
                    onLayout={this._handleLayout}
                    ref={ref => (this.backdropRef = ref)}
                    style={[
                        styles.backdrop,
                        {backgroundColor: backdropColor, width: deviceWidth, height: deviceHeight},
                    ]}
                />
        ........
}
mmazzarolo commented 7 years ago

Hey @hellomaya , thank you for sharing your snippet.
How does it handle buttons inside the modal? Doesn't the panresponder swallow the press on them?

jkomyno commented 7 years ago

@hellomaya's solution seems to work only if we click the backdrop shadow placed at the horizontal margins. For instance, if I click above or below the Modal, onPanResponderRelease doesn't get triggered (I'm testing it with the official example on Android)

hellomaya commented 7 years ago

Hi, @mmazzarolo thanks for your asking and @jkomyno thanks for your test.

Sorry I have been busy working on something, my test initially is in my own project, and it's working well in both iOS simulator and Android device, I didn't see the issue you guys mentioned.

In the official example, it do have an issue like @jkomyno mentioned, the issue is because the default content style had flex: 1, so there is no room for backdrop, check index.style.js file.

But to avoid the panresponder swallow issue, you will have to put all buttons in a separate component, like this:

<Modal ....>
   <Content />
</Modal>

....
class Content extends Component {

    ......
    return (<View><Button onPress=.....></Button></View>)
mmazzarolo commented 7 years ago

In the official example, it do have an issue like @jkomyno mentioned, the issue is because the default content style had flex: 1, so there is no room for backdrop, check index.style.js file.

That's right, but unfortunately using flex: 1 is the only way to have the modal size resize itself based on its content (obviously, I'll be glad to be proven wrong).

hellomaya commented 7 years ago

Yes, it's necessary. And we can try add some margin around the content view, still be able to close the modal by tap outside. marginVertical: 25 for example, larger than 20 was recommended to avoid invalid tap at top in iOS.

Also, @mmazzarolo, thank you for this good job, I tried another modal component before, until I found that it's clearly your works are much more concise and elegant.

<Modal style={{marginVertical: 25}}>
tdimnet commented 7 years ago

Hi there, I've just found a solution (I do not know if someone has already found it). I add two touchable and text elements within my component and I link to them my onPress event (and its relative closing modal window method).

Here is at my screen looks like:

Below is the snippet of my code:

        <Modal
          isVisible={this.props.isVisible}
          backdropOpacity={.9} backdropColor={'#2B333D'} animationIn={'slideInDown'} animationOut={'slideOutUp'}
          style={Styles.modalContainer}
        >
            <TouchableWithoutFeedback
              onPress={() => this.props.hideModal()}
            >
              <Text style={{ backgroundColor: 'blue', height: 100, width: 350 }}> </Text>
            </TouchableWithoutFeedback>
          <View
            style={Styles.viewContainer}
          >
            {this.showPicker(this.props.metricFormat)}
            {this.showWeekSelection()}
            <TouchableOpacity
              onPress={() => {
                this.props.hideModal();
                this.props.onSelectDay(this.state.selectedStartDate, this.state.selectedEndDate);
              }}
              style={Styles.buttonContainer}
            >
              <Text
                style={Styles.text}
              >
                {I18n.t('select')}
              </Text>
            </TouchableOpacity>
          </View>
            <TouchableWithoutFeedback onPress={() => this.props.hideModal()} >
              <Text style={{ backgroundColor: 'blue', height: 100, width: 350 }}> </Text>
            </TouchableWithoutFeedback>
        </Modal>
mmazzarolo commented 7 years ago

Hey @tdimnet, thank you for contributing.
The pattern you're following mimics the one showed by @zhantx (he uses 4 touchables in his solution).
I'm stil not 100% sure on adding a similar solution to react-native-modal though, because I'm worried it might be a bit to complex to tweak later on (and might be a bit hard to configure for the users without adding a ton of props).

What do you think?

tdimnet commented 7 years ago

Well, this is a really though question :).

I have been through this problem during this afternoon and I think this is maybe the most graceful solution for now. It lets you hide or show the modal window as you would need.

However I think you're right: adding this to your package could make the project more complex to custom and harder to understand and it really depends on the situation you are dealing with.

On my case I was looking for several ways to close the modal window:

That being said I think we could simply update the readme file in order to other people have a better understanding and a good grasp of how to solve this common problem.

If you want, I can open a PR this week which updates the readme file and presents this solution.

mmazzarolo commented 7 years ago

Thank you for your quick reply, I would be grateful for a PR on the README :) Thanks!

RichardLindhout commented 7 years ago

Guys, my PR should fix it without too much changes.

You can also still make other handlers to e.g. buttons inside the modal.

schermafbeelding 2017-07-29 om 21 00 21

mmazzarolo commented 7 years ago

Hi @RichardLindhout , thank you for the PR, I'll paste here what I already wrote in the PR thread:

I took a close look at it and it seems that the idea of the touchable wrapper + the custom pointerEvents still won't work if you tap on the side of the modal content, am I right?

RichardLindhout commented 7 years ago

If you press the black it will close. The modal itself will not receive touch events but the children will. See https://facebook.github.io/react-native/docs/view.html#pointerevents

So I can still have other handlers for my 'OK' button or select text inside the modal. (or date pickers for instance will still work)

Also the touchable is not a wrapper, but just below the modal. Since the modal itself is not clickable the underlying view can receive the touch events. Except when there is content in the modal.

schermafbeelding 2017-07-29 om 21 31 37