Skip to content

Commit

Permalink
refactor(Modal, Dropdown{*}, PopperContent, Tabs): go to React Async …
Browse files Browse the repository at this point in the history
…Rendering (#1427)

The main purpose of the changes is to move forward to React Async Rendering.
The changes include:
1. remove unsafe componentWillReceiveProps and componentWillUpdate in Modal.
2. get rid of findDOMNode in Dropdown.
3. replace legacy context with new Context API in DropdownItem, DropdownToggle, Dropdown, TabContent.
4. upgrade react-popper to '1.3.3', and make the corresponding changes in  PopperContent, DropdownItem, DropdownToggle, DropdownMenu, Dropdown.

BREAKING CHANGE: using new Context API, react-popper v. '1.3.3'
  • Loading branch information
aakula-edmunds authored and TheSharpieOne committed Apr 3, 2019
1 parent 4a5a8a3 commit 1afb2c2
Show file tree
Hide file tree
Showing 19 changed files with 345 additions and 1,964 deletions.
17 changes: 6 additions & 11 deletions __mocks__/react-popper.js
@@ -1,17 +1,12 @@
import React from 'react';

export function Manager({ tag: Tag = 'div', ...props }) {
return <Tag {...props} />;
export function Manager({ children }) {
return (children);
}

export function Popper({ component: Tag = 'div', ...props }) {
return <Tag {...props} />;
export function Popper({ children, placement }) {
return children({ ref: () => {}, placement, style: {}, arrowProps: { ref: () => {}, style: {} } });
}

export function Arrow({ component: Tag = 'div', ...props }) {
return <Tag {...props} />;
}

export function Target({ component: Tag = 'div', ...props }) {
return <Tag {...props} />;
export function Reference({ children }) {
return children({ ref: () => {} });
}
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -105,7 +105,7 @@
"lodash.tonumber": "^4.0.3",
"prop-types": "^15.5.8",
"react-lifecycles-compat": "^3.0.4",
"react-popper": "^0.10.4",
"react-popper": "^1.3.3",
"react-transition-group": "^2.3.1"
},
"peerDependencies": {
Expand Down
33 changes: 19 additions & 14 deletions src/Dropdown.js
Expand Up @@ -3,9 +3,9 @@

import React from 'react';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
import { Manager } from 'react-popper';
import classNames from 'classnames';
import { DropdownContext } from './DropdownContext';
import { mapToCssModules, omit, keyCodes, deprecated, tagPropType } from './utils';

const propTypes = {
Expand Down Expand Up @@ -37,12 +37,6 @@ const defaultProps = {
setActiveFromChild: false
};

const childContextTypes = {
toggle: PropTypes.func.isRequired,
isOpen: PropTypes.bool.isRequired,
direction: PropTypes.oneOf(['up', 'down', 'left', 'right']).isRequired,
inNavbar: PropTypes.bool.isRequired,
};

class Dropdown extends React.Component {
constructor(props) {
Expand All @@ -53,9 +47,11 @@ class Dropdown extends React.Component {
this.handleKeyDown = this.handleKeyDown.bind(this);
this.removeEvents = this.removeEvents.bind(this);
this.toggle = this.toggle.bind(this);

this.containerRef = React.createRef();
}

getChildContext() {
getContextValue() {
return {
toggle: this.props.toggle,
isOpen: this.props.isOpen,
Expand All @@ -79,9 +75,7 @@ class Dropdown extends React.Component {
}

getContainer() {
if (this._$container) return this._$container;
this._$container = ReactDOM.findDOMNode(this);
return ReactDOM.findDOMNode(this);
return this.containerRef.current;
}

getMenuCtrl() {
Expand Down Expand Up @@ -206,12 +200,13 @@ class Dropdown extends React.Component {
setActiveFromChild,
active,
addonType,
tag,
...attrs
} = omit(this.props, ['toggle', 'disabled', 'inNavbar', 'direction']);

const direction = (this.props.direction === 'down' && dropup) ? 'up' : this.props.direction;

attrs.tag = attrs.tag || (nav ? 'li' : 'div');
const Tag = tag || (nav ? 'li' : 'div');

let subItemIsActive = false;
if (setActiveFromChild) {
Expand All @@ -237,12 +232,22 @@ class Dropdown extends React.Component {
}
), cssModule);

return <Manager {...attrs} className={classes} onKeyDown={this.handleKeyDown} />;
return (
<DropdownContext.Provider value={this.getContextValue()}>
<Manager>
<Tag
{...attrs}
{...{ [typeof Tag === 'string' ? 'ref' : 'innerRef']: this.containerRef }}
onKeyDown={this.handleKeyDown}
className={classes}
/>
</Manager>
</DropdownContext.Provider>
);
}
}

Dropdown.propTypes = propTypes;
Dropdown.defaultProps = defaultProps;
Dropdown.childContextTypes = childContextTypes;

export default Dropdown;
12 changes: 12 additions & 0 deletions src/DropdownContext.js
@@ -0,0 +1,12 @@
import React from 'react';

/**
* DropdownContext
* {
* toggle: PropTypes.func.isRequired,
* isOpen: PropTypes.bool.isRequired,
* direction: PropTypes.oneOf(['up', 'down', 'left', 'right']).isRequired,
* inNavbar: PropTypes.bool.isRequired,
* }
*/
export const DropdownContext = React.createContext({});
7 changes: 2 additions & 5 deletions src/DropdownItem.js
@@ -1,6 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { DropdownContext } from './DropdownContext';
import { mapToCssModules, omit, tagPropType } from './utils';

const propTypes = {
Expand All @@ -16,10 +17,6 @@ const propTypes = {
toggle: PropTypes.bool
};

const contextTypes = {
toggle: PropTypes.func
};

const defaultProps = {
tag: 'button',
toggle: true
Expand Down Expand Up @@ -104,6 +101,6 @@ class DropdownItem extends React.Component {

DropdownItem.propTypes = propTypes;
DropdownItem.defaultProps = defaultProps;
DropdownItem.contextTypes = contextTypes;
DropdownItem.contextType = DropdownContext;

export default DropdownItem;
94 changes: 55 additions & 39 deletions src/DropdownMenu.js
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { Popper } from 'react-popper';
import { DropdownContext } from './DropdownContext';
import { mapToCssModules, tagPropType } from './utils';

const propTypes = {
Expand All @@ -20,12 +21,6 @@ const defaultProps = {
flip: true,
};

const contextTypes = {
isOpen: PropTypes.bool.isRequired,
direction: PropTypes.oneOf(['up', 'down', 'left', 'right']).isRequired,
inNavbar: PropTypes.bool.isRequired,
};

const noFlipModifier = { flip: { enabled: false } };

const directionPositionMap = {
Expand All @@ -35,46 +30,67 @@ const directionPositionMap = {
down: 'bottom',
};

const DropdownMenu = (props, context) => {
const { className, cssModule, right, tag, flip, modifiers, persist, ...attrs } = props;
const classes = mapToCssModules(classNames(
className,
'dropdown-menu',
{
'dropdown-menu-right': right,
show: context.isOpen,
}
), cssModule);
class DropdownMenu extends React.Component {

let Tag = tag;
render() {
const { className, cssModule, right, tag, flip, modifiers, persist, ...attrs } = this.props;
const classes = mapToCssModules(classNames(
className,
'dropdown-menu',
{
'dropdown-menu-right': right,
show: this.context.isOpen,
}
), cssModule);

if (persist || (context.isOpen && !context.inNavbar)) {
Tag = Popper;
const Tag = tag;

const position1 = directionPositionMap[context.direction] || 'bottom';
const position2 = right ? 'end' : 'start';
attrs.placement = `${position1}-${position2}`;
attrs.component = tag;
attrs.modifiers = !flip ? {
...modifiers,
...noFlipModifier,
} : modifiers;
}
if (persist || (this.context.isOpen && !this.context.inNavbar)) {

return (
<Tag
tabIndex="-1"
role="menu"
{...attrs}
aria-hidden={!context.isOpen}
className={classes}
x-placement={attrs.placement}
/>
);
const position1 = directionPositionMap[this.context.direction] || 'bottom';
const position2 = right ? 'end' : 'start';
const poperPlacement = `${position1}-${position2}`;
const poperModifiers = !flip ? {
...modifiers,
...noFlipModifier,
} : modifiers;

return (
<Popper

This comment has been minimized.

Copy link
@Maczuga

Maczuga Apr 15, 2019

Breaks positionFixed prop on DropdownMenu - it's no longer being passed to Popper.

placement={poperPlacement}
modifiers={poperModifiers}
>
{({ ref, style, placement }) => (
<Tag
tabIndex="-1"
role="menu"
ref={ref}
style={style}
{...attrs}
aria-hidden={!this.context.isOpen}
className={classes}
x-placement={placement}
/>
)}
</Popper>
);
}

return (
<Tag
tabIndex="-1"
role="menu"
{...attrs}
aria-hidden={!this.context.isOpen}
className={classes}
x-placement={attrs.placement}
/>
);
}
};

DropdownMenu.propTypes = propTypes;
DropdownMenu.defaultProps = defaultProps;
DropdownMenu.contextTypes = contextTypes;
DropdownMenu.contextType = DropdownContext;

export default DropdownMenu;
32 changes: 16 additions & 16 deletions src/DropdownToggle.js
@@ -1,7 +1,8 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { Target } from 'react-popper';
import { Reference } from 'react-popper';
import { DropdownContext } from './DropdownContext';
import { mapToCssModules, tagPropType } from './utils';
import Button from './Button';

Expand All @@ -24,12 +25,6 @@ const defaultProps = {
color: 'secondary',
};

const contextTypes = {
isOpen: PropTypes.bool.isRequired,
toggle: PropTypes.func.isRequired,
inNavbar: PropTypes.bool.isRequired,
};

class DropdownToggle extends React.Component {
constructor(props) {
super(props);
Expand Down Expand Up @@ -93,20 +88,25 @@ class DropdownToggle extends React.Component {
}

return (
<Target
{...props}
className={classes}
component={Tag}
onClick={this.onClick}
aria-expanded={this.context.isOpen}
children={children}
/>
<Reference>
{({ ref }) => (
<Tag
{...props}
{...{ [typeof Tag === 'string' ? 'ref' : 'innerRef']: ref }}

className={classes}
onClick={this.onClick}
aria-expanded={this.context.isOpen}
children={children}
/>
)}
</Reference>
);
}
}

DropdownToggle.propTypes = propTypes;
DropdownToggle.defaultProps = defaultProps;
DropdownToggle.contextTypes = contextTypes;
DropdownToggle.contextType = DropdownContext;

export default DropdownToggle;
16 changes: 6 additions & 10 deletions src/Modal.js
Expand Up @@ -120,19 +120,15 @@ class Modal extends React.Component {
this._isMounted = true;
}

componentWillReceiveProps(nextProps) {
if (nextProps.isOpen && !this.props.isOpen) {
this.setState({ isOpen: nextProps.isOpen });
}
}

componentWillUpdate(nextProps, nextState) {
if (nextState.isOpen && !this.state.isOpen) {
componentDidUpdate(prevProps, prevState) {
if (this.props.isOpen && !prevProps.isOpen) {
this.init();
this.setState({ isOpen: true});
// let render() renders Modal Dialog first
return;
}
}

componentDidUpdate(prevProps, prevState) {
// now Modal Dialog is rendered and we can refer this._element and this._dialog
if (this.props.autoFocus && this.state.isOpen && !prevState.isOpen) {
this.setFocus();
}
Expand Down

0 comments on commit 1afb2c2

Please sign in to comment.