Giter Club home page Giter Club logo

react-portal's Introduction

React-portal

npm version npm downloads Build Status

Struggling with modals, lightboxes or loading bars in React? React-portal creates a new top-level React tree and injects its children into it. That's necessary for proper styling (especially positioning).

Looking for v3 documentation? Go here.

Features

  • uses React v16 and its official API for creating portals
  • has a fallback for React v15
  • transports its children into a new React Portal which is appended by default to document.body
  • can target user specified DOM element
  • supports server-side rendering
  • supports returning arrays (no wrapper divs needed)
  • <Portal /> and <PortalWithState /> so there is no compromise between flexibility and convenience
  • doesn't produce any DOM mess
  • provides close on ESC and close on outside mouse click out of the box
  • no dependencies, minimalistic

Installation

yarn add react react-dom react-portal

Usage

Portal

import { Portal } from 'react-portal';

<Portal>
  This text is portaled at the end of document.body!
</Portal>

<Portal node={document && document.getElementById('san-francisco')}>
  This text is portaled into San Francisco!
</Portal>

That's it! Do you want to toggle portal? It's a plain React component, so you can simply do:

{isOpen && <Portal>Sometimes portaled?</Portal>}

This gives you absolute flexibility and control and I would recommend you to use it as a basic building block for your components like modals or notifications. This code also works with server-side rendering. If you think about just using official ReactDOM.createPortal(), you would have to check for existence of DOM environment.

React-portal used to come packed with some extra goodies because sometimes you are ok with giving up some flexibility for convenience. For that case, V4 introduces another component that handles its own state for you:

PortalWithState

import { PortalWithState } from 'react-portal';

<PortalWithState closeOnOutsideClick closeOnEsc>
  {({ openPortal, closePortal, isOpen, portal }) => (
    <React.Fragment>
      <button onClick={openPortal}>
        Open Portal
      </button>
      {portal(
        <p>
          This is more advanced Portal. It handles its own state.{' '}
          <button onClick={closePortal}>Close me!</button>, hit ESC or
          click outside of me.
        </p>
      )}
    </React.Fragment>
  )}
</PortalWithState>

Don't let this example intimidate you! PortalWithState expects one child, a function. This function gets a few parameters (mostly functions) and returns a React component.

There are 4 optional parameters:

  • openPortal - function that you can call to open the portal
  • closePortal - function that you can call to close the portal
  • portal - the part of component that should be portaled needs to be wrapped by this function
  • isOpen - boolean, tells you if portal is open/closed

<PortalWithState /> accepts this optional props:

  • node - same as <Portal>, you can target a custom DOM element
  • closeOnOutsideClick - boolean, portal closes when you click outside of it
  • closeOnEsc - boolean, portal closes when the ESC key is hit
  • defaultOpen - boolean, the starting state of portal is being open
  • onOpen - function, will get triggered after portal is open
  • onClose - function, will get triggered after portal is closed

Also notice, that the example returns a Fragment since React 16.2 supports it! You can also return:

  • an array - available from React v16, remember to add key attribute
  • regular component - the example would be wrapped by a div, not a fragment

If you start running into limits of <PortalWithState /> (complex animations), you probably want to use <Portal /> instead and build a component tailored to your specific taste.

Run Examples

git clone https://github.com/tajo/react-portal
cd react-portal
yarn install
yarn build:examples
open examples/index.html

Contributions Welcome!

git clone https://github.com/tajo/react-portal
cd react-portal
yarn install
yarn build:examples --watch
open examples/index.html

Run Tests

yarn test

Author

Vojtech Miksu 2017, miksu.cz, @vmiksu

react-portal's People

Contributors

brucewpaul avatar dependabot[bot] avatar devinxian avatar dlindenkreuz avatar eagleeye avatar evenchange4 avatar googol7 avatar gucheen avatar jakubwolny avatar jussikinnula avatar krijoh92 avatar meriadec avatar mgalante avatar moroshko avatar mrorz avatar netsgnut avatar nicolashery avatar odelijairo avatar oluckyman avatar pascalduez avatar sebasgarcep avatar sediug avatar shahankit avatar silvenon avatar sinewyk avatar slorber avatar sompylasar avatar strml avatar tajo avatar timomeh avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

react-portal's Issues

Add onClose() hook

Is there a way to execute something when modal is closed?
I have the following scenario:

  1. User clicks the Help button which opens a modal dialog
  2. User closed the modal dialog
  3. At this stage, I'd like to set the focus back to the Help button.
    This is the desired behaviour according to the WAI-ARIA spec:

When the dialog is closed or cancelled focus should return to the element in the application which had focus before the dialog is invoked. This is usually the control which opened the dialog.

It would be great to have an onClose() hook to be able to create more accessible components.

Redbox dependency in 1.5.4

Trying to import the node module, throws the following error:

 Cannot find module '/Users/miksu/Projects/react-portal/node_modules/redbox-react/lib/index.js' from '../node_modules/react-portal/build'

Portal does not update `className` and `style` when new props arrive

It renders className and style into the element only once, during creation of the element:

if (props.style) {

  renderPortal(props) {
    if (!this.node) {
      this.node = document.createElement('div');
      if (props.className) {
        this.node.className = props.className;
      }
      if (props.style) {
        CSSPropertyOperations.setValueForStyles(this.node, props.style);
      }
      document.body.appendChild(this.node);
    }
    this.portal = ReactDOM.unstable_renderSubtreeIntoContainer(
      this,
      React.cloneElement(props.children, { closePortal: this.closePortal }),
      this.node,
      this.props.onUpdate
    );
  }

If the new values arrive through the props, they are simply ignored, only the children get updated.

Right click for closeOnOutsideClick closes portal

This is up for discussion but currently, doing a right-click when closeOnOutsideClick is set still closes the portal. Personally, I think it should only close if it was a left-click. This allows people to right click -> inspect their overlay for example. Any comments or concerns before I open a PR for this?

Portal.onOpen() bug

Hey, this seems like a pretty straightforward bug, although I'm not sure the best way to fix it. Currently, the onOpen callback isn't firing. When I looked into it further, I found that while the openPortal method in lib/portal.js calls this.props.onOpen, the same method in the main portal.js file does not. The lines are simply missing. According to the commits the main file hasn't changed in two months, so maybe the source was updated but the project wasn't rebuilt.

Whatever it was, I fixed it on my local copy by manually adding:

      if (this.props.onOpen){
        this.props.onOpen(this.node);
      }

to the end of the openPortal method in the top-level portal.js. Thought I'd let you know the issue was there, even if I'm not certain of the right way to fix it with a PR.

Callbacks for closeOnEsc and closeOnOutsideClick

When in controlled mode (using isOpened), we loose closeOnEsc and closeOnOutsideClick

I'm proposing a onRequestClose callback to be used when in controlled state. That will give the consumer a chance to update state without having to reimplement esc-closing, etc.

Click outside in the demo doesn't handle part of the page

When clicking on the bottom part of the screen, "click outside" functionality doesn't work, apparently due to handling of the bubbling events up to <body> (that can take only a part of the screen) while it's properly done up to <html>.

Props for children element

Hey
Im using this library and have a little problem with passing props to children. Take this code for example:

<Portal openByClickOn={ this.props.children }>
      <Body application={ this.props.application } />
</Portal>

this.props.application is not undefined but when I click element passed in openByClickOn, Body component have undefined as params. Any idea how to handle that? ๐Ÿ˜„

openByClickOn element does not accept styles - CSS Modules

Hi,

Thanks for the nice library. I'm using CSS Modules + React CSS Modules. The element I pass to 'openByClickOn' does not render with styles assigned to it.

The default React properties className and style on the element seem to work fine and the element is rendered with appropriate styles. Using the styleName syntax does not seem to work for this element.

const link = ( <span styleName="link"> Set </span> );

I've not gone through the source, but would it be possible not have the element rendered by the Portal component and let the user explicitly provide/render his own? Working around this by managing the state myself.

Duplicated document events when having multiple portal components

Try including multiple portals on your page and then put a console log inside 'handleOutsideMouseClick' method. It triggers once for every portal.

So if I have a list of 100 items each wrapped in a portal, the event will trigger 100 times in a row when clicking on one item.

Solution: put addEventListeners inside 'openPortal' and removeEventListeners inside 'closePortal' instead of compDidMount/willUnmount

Warning: owner-based and parent-based contexts differ

There is a new warning after update to React v.0.13.3. I am not sure how to fix it (if possible).

Warning: owner-based and parent-based contexts differ (values: `function (props, context) {
      // This constructor is overridden by mocks. The argument is used
      // by mocks to assert on what gets mounted.

      if ("production" !== "development") {
        ("production" !== "development" ? warning(
          this instanceof Constructor,
          'Something is calling a React component directly. Use a factory or ' +
          'JSX instead. See: https://fb.me/react-legacyfactory'
        ) : null);
      }

      // Wire up auto-binding
      if (this.__reactAutoBindMap) {
        bindAutoBindMethods(this);
      }

      this.props = props;
      this.context = context;
      this.state = null;

      // ReactClasses doesn't have constructors. Instead, they use the
      // getInitialState and componentWillMount methods for initialization.

      var initialState = this.getInitialState ? this.getInitialState() : null;
      if ("production" !== "development") {
        // We allow auto-mocks to proceed as if they're returning null.
        if (typeof initialState === 'undefined' &&
            this.getInitialState._isMockFunction) {
          // This is probably bad practice. Consider warning here and
          // deprecating this convenience.
          initialState = null;
        }
      }
      ("production" !== "development" ? invariant(
        typeof initialState === 'object' && !Array.isArray(initialState),
        '%s.getInitialState(): must return an object or null',
        Constructor.displayName || 'ReactCompositeComponent'
      ) : invariant(typeof initialState === 'object' && !Array.isArray(initialState)));

      this.state = initialState;
    }` vs `undefined`) for key (router) while mounting Lightbox (see: http://fb.me/react-context-by-parent)

Style prop on the Portal element does not get updated

It seems like the style property on the Portal element is only set on the first render. Changes to the style prop are ignored in subsequent renders. I think this might be related to renderPortal() which seems to create the node and apply the style prop.

I came across this while trying to reposition the portals after a window resize, the style={{left:x, top:y}} just doesn't have any effect.

How to use "closeOnOutsideClick" with full screen overlay?

@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;
    }

`isOpened` to `isOpen`

Just a random thing I noticed, might be nicer to call it isOpen, since React is stateless, so everything should really be in the present tense.

Render in place?

Is there a way to tell Portal to do nothing and just render the component in place? (keeping the closeOnEsc, closeOnOutsideClick etc?)

It would be useful when I need to render a tooltip in the same context of its parent (I don't always want to move it to the body)

`closePortal` prop causes React 15.2.0 warning

warning.js:44 Warning: Unknown prop `closePortal` on <span> tag. Remove this prop from the element. For details, see https://fb.me/react-unknown-prop
    in span (created by Tooltip)

A thought - could check if component.type === 'string' and only pass the prop along if it is not.

Other portal targets?

Currently the package only allows the portal target to be document.body, which is useful for e.g. modals, loading bars and so. However, if we can allow the target to be specified individually it can also be useful for other use cases, for instance integrating with other rendering libraries where the target DOM may not be registered on React.

Any ideas?

Error with event bubbling

There's an error happening when a parent of the child which is closing the portal has an onClick handler:

import jsdom from 'jsdom';
import sinon from 'sinon';
import assert from 'assert';

describe('Portal', () => {
  let React, utils, Portal;

  before(() => {
    global.document = jsdom.jsdom('<!DOCTYPE html><html><head></head><body></body></html>');
    global.window = global.document.parentWindow;
    global.navigator = window.navigator;

    React = require('react/addons');
    utils = React.addons.TestUtils;
    Portal = require('react-portal');
  });

  it('handles event bubbling after close', () => {
    const cb = sinon.spy();

    const Component = React.createClass({
      handleClose() {
        this.refs.portal.closePortal();
        console.log('close');
      },

      render() {
        return (
          <Portal ref="portal" openByClickOn={<span ref="toggle" />} onClose={cb}>
            <div onClick={function () { }}> // it works without the handler
              <button ref="close" type="button" onClick={this.handleClose}>Close</button>
            </div>
          </Portal>
        );
      }
    });

    const el = utils.renderIntoDocument(<Component />);

    utils.Simulate.click(React.findDOMNode(el.refs.toggle));
    utils.Simulate.click(React.findDOMNode(el.refs.close));

    assert(cb.called);
  });
});
TypeError: Cannot read property 'firstChild' of undefined

I created a demo where you can run this test.

mimic react lifecycle props with `on*` handlers

Right now the handlers are defined as:

  • onOpen
  • onClose
  • onBeforeClose

It could be nice to mimic the React lifecycle method naming, to make it more clear right away:

  • onWillMount (would be as if there was a onBeforeOpen right now)
  • onDidMount (aka onOpen)
  • onWillUnmount (aka onBeforeClose)
  • onDidUnmount (aka onClose)

ref function not called

<Portal>
  <div ref={e => (this.myel = e)}>foobar</div>
</Portal>

this.myel wll be undefined. If I don't wrap it with Portal, everything works as expected.

ReferenceError: document is not defined

My app uses react-portal to display modals.
I need to generate a static version of my app's HTML for server side rendering.

The problem is that when react-portal is loaded in node context (where document doesn't exist), it throws the error below.

The error is coming from the keymaster dependency, and I wonder whether this dependency is necessary. Looks like it is used only to bind the ESC key...

What would be the best way to fix this?

$ node
> var portal = require('react-portal')
ReferenceError: document is not defined
    at /Users/mishamoroshko/job-search-panel/node_modules/react-portal/node_modules/keymaster/keymaster.js:267:12
    at Object.<anonymous> (/Users/mishamoroshko/job-search-panel/node_modules/react-portal/node_modules/keymaster/keymaster.js:296:3)
    at Module._compile (module.js:456:26)
    at Object.Module._extensions..js (module.js:474:10)
    at Module.load (module.js:356:32)
    at Function.Module._load (module.js:312:12)
    at Module.require (module.js:364:17)
    at require (module.js:380:17)
    at Object.<anonymous> (/Users/mishamoroshko/job-search-panel/node_modules/react-portal/portal.js:25:12)
    at Module._compile (module.js:456:26)

What is the reason behind NPM 3+ request?

Hi,

I'm wondering why preferred NPM version for the package is defined to 3+? It should not make any difference for the component if it was installed by NPM 2 or 3.

And I didn't noticed any problems using it within NPM 2 env.

Refactor as an input for using isOpened correctly

We thought about it with a colleague and we think that a small refactoring to work exactly as an input would be great.

If you do NOT use isOpened, the portal is in a non controlled state. You close it via the optional props. Nothing is different from before.

If you DO use isOpened, the Portal enter controlled state.

With the value prop of an input being equivalent to the isOpened prop of the portal and toBeDefined equivalent to onChange (or beforeClose, not sure yet).

Anything wanting to close the portal will declare its intent to close the portal, the user will catch that and be able to change its state correctly so that isOpened is false. But you do not actually edit the active state of the portal to false or do anything at all inside the portal, except declare your intent to close the portal (if using closeOnEsc and pressing escape, you will trigger the onChange equivalent, but the portal will not close unless the user re-renders with isOpened to false or unmounts the Portal).

And of course, we do not forget the warning in propTypes that isOpened MUST come with the portal equivalent onChange prop.

Thoughts ?

Portal used in iframe is transported to top-level document rather than frame document

I am using a <Portal> inside a frame, and I expected the element to be added as a child of the frame document's body. Instead, it is added as a child of the top-level document's body. I guess this could be fixed by having render return something like <noscript ref="root"> instead of null and then using this.refs.root.ownerDocument everywhere that document is used currently.

Passing styles to the wrapper div

Hi, guys!
I struggled into a problem. I have a group of inlined buttons, and one of them opens a modal window.
The problem that the react-portal wraps a button into the div, so the markup becomes broken.
Is there a way to pass styles or add an additional class to the wrapper div?
Also, is it possible to change the wrapper div to the span?

Knows if portal is active

If I have two portals with isOpened={true} and I press ESC key, the two portals have been closed.

Any idea to solve this?

Tips on testing

I'm lost on testing components that use react-portal. Mind elaborating an example?

Using the scroll bar dismisses the modal.

Not sure if this is the intended behaviour but I have a use case where we want to be able to scroll without dismissing the modal. I'm wondering if this should be configurable through props or just a default behaviour. Let me know and I can create a pull request.

Call onClose when closeOnEsc is true

I was wondering maybe it's better to call onClose when you set closeOnEsc prop, because the parent don't know the modal is close when you press esc key.
Do you want a PR for that ?

Allow to render abirtrary content outside of portal

Hi,

I have a component. I want it to render both a dom element, AND manage a portal.

Currently to do so I have to wrap it inside a div, because React can't render an array:

render() {
            return (
              <div className="wrapper">
                  {this.props.children}
                  <Portal>
                      {this.props.portalContent}
                  </Portal>
              </div>
            );
}

Would it be possible to do this?

render() {
            return (
                  <Portal rendered={this.props.children}>
                      {this.props.portalContent}
                  </Portal>
            );
}

I'd like to avoid to use the wrapper element

Animating a portal on close.

Is there a suggested strategy to animate out a portal before it's removed from the DOM? Or should this be handled by a container component?

I think this ideally would be handled by a beforeClose callback that can trigger removing the node and reseting the portal state after it's been executed.

Test specs

Every good lib should have specs! ๐Ÿ˜„

Personally, I'm actually not quite sure how to go about testing something like this. How would you get a ref to the portal?

Bug on open/close: Invariant Violation: removeComponentAsRefFrom(...)

Hello again,

I'm not sure what changed, but the code I had which was previously working no longer does. The React-Portal I have opens correctly, but it renders the modal inside of it too far down on the page. Additionally, and more importantly, when I try to click out of it or hit esc, I get this error:

Invariant Violation: removeComponentAsRefFrom(...): Only a ReactOwner can have refs. You might be removing a ref to a component that was not created inside a component's `render` method, or you have multiple copies of React loaded (details: https://fb.me/react-refs-must-have-owner).

I'm reasonably sure that I don't have two copies of React loaded. One key thing here is that I'm using React with Meteor and Webpack, so my React is coming from an "external" package. Is there a chance that react-portal is trying to instantiate its own React rather than using the provided version?

isOpened variable is not set to true on closeOnOutsideClick

I'm manually managing the state of the portal using isOpened

<span>
  {button}
  <Portal closeOnEsc closeOnOutsideClick isOpened={this.state.isOpened}>
    <div styleName='popover' ref='popover'>
      {arrow}
      {this.props.popover}
    </div>
  </Portal>
</span>

The problem is that when I click outside, the portal is closed, bu the this.state.isOpened prop is not set to false.
I'm I missing something?

onClose triggered 2 times

When we use the closePortal props passed to the children (https://github.com/tajo/react-portal/blob/master/lib/portal.js#L87) it immediately triggers the onClose props (https://github.com/tajo/react-portal/blob/master/lib/portal.js#L87).

Because we unmount the portal when we receive the onClose we actually trigger the onClose two times because of the second one coming of course from the componentWillUnmount lifecycle (https://github.com/tajo/react-portal/blob/master/lib/portal.js#L69).

Do we misuse this library or should we try to make it so that react-portal can only call the onClose callback once ?

edit: clarity of last phrase

Close button doesn't set active state properly and will not allow you to reopen the portal

Steps to reproduce issue:

  1. Go to the demo: https://miksu.cz/react-portal/
  2. Open the first portal
  3. Click on the "close this" button in the portal
  4. Try and open the first portal again

Now, for more fun, next open the second portal. It will open both portals.

If you press the esc key at any time after step 4 above it will close the first portal and "fix" the issue. You can visually see this by opening the second portal and pressing the esc key.

I haven't dug into this much but it looks like the active state is not getting set to false when you use the close button.

Stateless components

function call here returns null if the child is a stateless component. preventing close on escape.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.