diff --git a/package.json b/package.json index 3ee7e811..2f4cff03 100644 --- a/package.json +++ b/package.json @@ -43,14 +43,15 @@ }, "devDependencies": { "async": "~1.5.0", + "core-js": "^2.5.1", "expect.js": "0.3.x", "jquery": "~1.11.3", "object-assign": "~4.0.1", "pre-commit": "1.x", - "rc-tools": "6.x", "rc-test": "6.x", - "react": "15.x", - "react-dom": "15.x" + "rc-tools": "6.x", + "react": "^16.0.0-rc.3", + "react-dom": "^16.0.0-rc.3" }, "pre-commit": [ "lint" diff --git a/src/Portal.js b/src/Portal.js new file mode 100644 index 00000000..1eb20d96 --- /dev/null +++ b/src/Portal.js @@ -0,0 +1,36 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { createPortal } from 'react-dom'; + +export default class Portal extends React.Component { + static propTypes = { + getContainer: PropTypes.func.isRequired, + children: PropTypes.node.isRequired, + } + + componentDidMount() { + this.createContainer(); + } + + componentWillUnmount() { + this.removeContainer(); + } + + createContainer() { + this._container = this.props.getContainer(); + this.forceUpdate(); + } + + removeContainer() { + if (this._container) { + this._container.parentNode.removeChild(this._container); + } + } + + render() { + if (this._container) { + return createPortal(this.props.children, this._container); + } + return null; + } +} diff --git a/src/index.js b/src/index.js index 1944964d..219ebab6 100644 --- a/src/index.js +++ b/src/index.js @@ -1,12 +1,13 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { findDOMNode } from 'react-dom'; +import { findDOMNode, createPortal } from 'react-dom'; import createReactClass from 'create-react-class'; import contains from 'rc-util/lib/Dom/contains'; import addEventListener from 'rc-util/lib/Dom/addEventListener'; import Popup from './Popup'; import { getAlignFromPlacement, getPopupClassNameFromAlign } from './utils'; import getContainerRenderMixin from 'rc-util/lib/getContainerRenderMixin'; +import Portal from './Portal'; function noop() { } @@ -22,6 +23,26 @@ function returnDocument() { const ALL_HANDLERS = ['onClick', 'onMouseDown', 'onTouchStart', 'onMouseEnter', 'onMouseLeave', 'onFocus', 'onBlur']; +const IS_REACT_16 = !!createPortal; + +const mixins = []; + +if (!IS_REACT_16) { + mixins.push( + getContainerRenderMixin({ + autoMount: false, + + isVisible(instance) { + return instance.state.popupVisible; + }, + + getContainer(instance) { + return instance.getContainer(); + }, + }) + ); +} + const Trigger = createReactClass({ displayName: 'Trigger', propTypes: { @@ -66,30 +87,7 @@ const Trigger = createReactClass({ maskAnimation: PropTypes.string, }, - mixins: [ - getContainerRenderMixin({ - autoMount: false, - - isVisible(instance) { - return instance.state.popupVisible; - }, - - getContainer(instance) { - const { props } = instance; - const popupContainer = document.createElement('div'); - // Make sure default popup container will never cause scrollbar appearing - // https://github.com/react-component/trigger/issues/41 - popupContainer.style.position = 'absolute'; - popupContainer.style.top = '0'; - popupContainer.style.left = '0'; - popupContainer.style.width = '100%'; - const mountNode = props.getPopupContainer ? - props.getPopupContainer(findDOMNode(instance)) : props.getDocument().body; - mountNode.appendChild(popupContainer); - return popupContainer; - }, - }), - ], + mixins, getDefaultProps() { return { @@ -154,11 +152,16 @@ const Trigger = createReactClass({ componentDidUpdate(_, prevState) { const props = this.props; const state = this.state; - this.renderComponent(null, () => { + const triggerAfterPopupVisibleChange = () => { if (prevState.popupVisible !== state.popupVisible) { props.afterPopupVisibleChange(state.popupVisible); } - }); + }; + if (!IS_REACT_16) { + this.renderComponent(null, triggerAfterPopupVisibleChange); + } else { + triggerAfterPopupVisibleChange(); + } // We must listen to `mousedown` or `touchstart`, edge case: // https://github.com/ant-design/ant-design/issues/5804 @@ -341,12 +344,28 @@ const Trigger = createReactClass({ transitionName={props.popupTransitionName} maskAnimation={props.maskAnimation} maskTransitionName={props.maskTransitionName} + ref={this.savePopup} > {typeof props.popup === 'function' ? props.popup() : props.popup} ); }, + getContainer() { + const { props } = this; + const popupContainer = document.createElement('div'); + // Make sure default popup container will never cause scrollbar appearing + // https://github.com/react-component/trigger/issues/41 + popupContainer.style.position = 'absolute'; + popupContainer.style.top = '0'; + popupContainer.style.left = '0'; + popupContainer.style.width = '100%'; + const mountNode = props.getPopupContainer ? + props.getPopupContainer(findDOMNode(this)) : props.getDocument().body; + mountNode.appendChild(popupContainer); + return popupContainer; + }, + setPopupVisible(popupVisible) { this.clearDelayTimer(); if (this.state.popupVisible !== popupVisible) { @@ -450,11 +469,18 @@ const Trigger = createReactClass({ this.setPopupVisible(false); }, + savePopup(node) { + if (IS_REACT_16) { + this._component = node; + } + }, + render() { + const { popupVisible } = this.state; const props = this.props; const children = props.children; const child = React.Children.only(children); - const newChildProps = {}; + const newChildProps = { key: 'trigger' }; if (this.isClickToHide() || this.isClickToShow()) { newChildProps.onClick = this.onClick; newChildProps.onMouseDown = this.onMouseDown; @@ -482,7 +508,29 @@ const Trigger = createReactClass({ newChildProps.onBlur = this.createTwoChains('onBlur'); } - return React.cloneElement(child, newChildProps); + const trigger = React.cloneElement(child, newChildProps); + + if (!IS_REACT_16) { + return trigger; + } + + let portal; + // prevent unmounting after it's rendered + if (popupVisible || this._component) { + portal = ( + + {this.getComponent()} + + ); + } + + return [ + trigger, + portal, + ]; }, }); diff --git a/tests/index.js b/tests/index.js index 0ac3b42d..9761b870 100644 --- a/tests/index.js +++ b/tests/index.js @@ -1,5 +1,7 @@ /* eslint no-console:0 */ +import 'core-js/es6/map'; +import 'core-js/es6/set'; import expect from 'expect.js'; import React from 'react'; import ReactDOM from 'react-dom';