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

How to use "closeOnOutsideClick" with full screen overlay? #2

Closed
moroshko opened this issue Apr 12, 2015 · 10 comments
Closed

How to use "closeOnOutsideClick" with full screen overlay? #2

moroshko opened this issue Apr 12, 2015 · 10 comments

Comments

@moroshko
Copy link
Contributor

@tajo I'd like to have closeOnOutsideClick={true} with a modal that has a full screen overlay.

The problem is that I cannot really click outside the full screen overlay, thus the expected "click outside the dialog to close it" behaviour doesn't work.

Is there a way to have a full screen overlay with a dialog on top, and close the dialog once the overlay is clicked (outside the dialog)?

Here is my attempt:

    let Help = React.createClass({
      render() {
        let helpButton = (
          <button type="button">Help</button>
        );

        return (
          <Portal openByClickOn={helpButton} closeOnEsc={true} closeOnOutsideClick={true}>
            <HelpModal />
          </Portal>
        );
      }
    });

    let HelpModal = React.createClass({
      render() {
        return (
          <div className="modal-overlay">
            <div className="modal">
              <button type="button"
                      onClick={this.props.closePortal}>Close</button>
              <h3>My modal header</h3>
            </div>
          </div>
        );
      }
    });
    .modal-overlay {
      position: fixed;
      left: 0;
      right: 0;
      top: 0;
      bottom: 0;
      background-color: rgba(0, 0, 0, 0.5);
    }

    .modal {
      position: absolute;
      top: 100px;
      left: calc(50% - 400px);
      width: 800px;
      height: 300px;
    }
@tajo
Copy link
Owner

tajo commented Apr 12, 2015

@moroshko Yes, I also have modals with fullscreen overlay. In that case, you can't use closeOnOutsideClick={true}, so delete it. You have to implement that logic in your modal instead (it's pretty simple).

So this is how it looks:

<Portal closeOnEsc={true} openByClickOn={triggerButton}>
  <Modal title="Modal title">
    Modal content
  </Modal>
</Portal>
import React, {findDOMNode} from 'react';

export default class Modal extends React.Component {

  constructor() {
    super();
    this.handleMouseClickOutside = this.handleMouseClickOutside.bind(this);
  }

  componentDidMount() {
    document.addEventListener('mousedown', this.handleMouseClickOutside);
  }

  componentWillUnmount() {
    document.removeEventListener('mousedown', this.handleMouseClickOutside);
  }

  render() {
    return (
      <div>
        <div className="modal-overlay" />
        <div className="modal" ref="content">
          <h1>{this.props.title}</h1>
          {React.cloneElement(this.props.children, {closePortal: this.props.closePortal})}
        </div>
      </div>
    );
  }

  handleMouseClickOutside(e) {
    if (isNodeInRoot(e.target, findDOMNode(this.refs.content))) {
      return;
    }
    e.stopPropagation();
    this.props.closePortal();
  }

}

Modal.propTypes = {
  title: React.PropTypes.string.isRequired,
  closePortal: React.PropTypes.func,
  children: React.PropTypes.element.isRequired
};

function isNodeInRoot(node, root) {
  while (node) {
    if (node === root) {
      return true;
    }
    node = node.parentNode;
  }
  return false;
}

It's good to have a general modal component (so you don't have to replicate the outside click logic anymore).

I am React cloning the modal's children for the case, when the children is an another React component that needs to have access to the portal's closePortal() function. If you don't need that, you can just simply replace it with {this.props.children}.

(I didn't run the code above, so sorry for possible typos.).

@moroshko
Copy link
Contributor Author

@tajo Thanks for your detailed answer and code example. Now all works as expected :)

@RobinMalfait
Copy link

@tajo

you can't use closeOnOutsideClick={true}, so delete it

You can if you add pointer-events: none to your overlay and it all works beautifully ;)

@dmitry
Copy link

dmitry commented Aug 8, 2015

@RobinMalfait it will invoke a hover events for all the elements below the overlay layer. @tajo example works good. 👍

@AoDev
Copy link

AoDev commented Aug 24, 2015

@tajo Could we include this as a portal feature? A lot of people will probably replicate this behaviour.
On my side I have created my Modal component with Portal and I can pass an option to have an overlay.

<Modal withOverlay>
  <Children />
</Modal>

@tajo
Copy link
Owner

tajo commented Aug 24, 2015

@AoDev Can you please send a PR? I am not sure how to integrate it.

@AoDev
Copy link

AoDev commented Aug 25, 2015

@tajo Sure. I'll start and we can discuss on the PR what is best. For example, in my project I have a hard coded value for the overlay styles but this is not what we want here.

@AoDev
Copy link

AoDev commented Aug 25, 2015

But one thing I wonder is that why you make it so "complicated" here. Maybe I am doing something wrong so let me know but, this is only what I need to do in my modal class:

var overlayStyles = {
  position: 'fixed',
  top: 0,
  right: 0,
  bottom: 0,
  left: 0,
  background: 'rgba(0,0,0,0.4)',
  zIndex: 1031 // because of bootstrap
};

The modal class with outsideclick:

class Modal extends React.Component {

  constructor() {
    super();
    this.closeMe = this.closeMe.bind(this);
  }

  render() {
    var overlay;
    var modal;
    var {
      ...
      onBody,
      withOverlay,
      // note: you could pass props that mimics Portal options like closeOnOutsideClick
    } = this.props;

    ...

    overlay = withOverlay ? <div style={overlayStyles} onClick={this.closeMe}/> : '';

    modal =
      <div className="my-modal">
        {this.props.children}
      </div>;

    if (onBody) {
      modal =
        <Portal isOpened closeOnEsc ref="portal">
          <div>
            {overlay}
            {modal}
          </div>
        </Portal>;
    }

    return modal;
  }

  closeMe() {
    this.refs['portal'].closePortal();
  }
}

Modal.propTypes = {
  children: React.PropTypes.element
};

export default Modal;

The idea is to get a ref from the portal and call the closePortal method.

@tajo
Copy link
Owner

tajo commented Aug 26, 2015

Looks good (I assume it works). Still, it's just a slightly different implementation, isn't? In other words, is there anything that needs to be changed in Portal to support this (support this better)? I agree that Modal is common use-case of Portal, but it's imo out of scope this lib. On the other hand, there could be (should be) another Modal lib using Portal.

@TryingToImprove
Copy link

Another way, which don't require you to create a new component

<Portal closeOnEsc onClose={this.handleCloseVideo} isOpened={isOpen}>
    <div className={styles.videoPortal} onClick={(e) => this.handleCloseVideo()} >
        <div className={styles.videoContainer} onClick={e => e.stopPropagation()}>
            <VideoPlayer provider={videoSource.provider} providerId={videoSource.providerUid} />
        </div>
    </div>
</Portal>

By doing this, you register the click event on the overlay, and register a click-event on the modal content. In this case the content is inside the portal-container. The click event-event on the content prevents propagation, so the parent onClick-event is not called.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants