Skip to content

Commit

Permalink
Feat(Modal): add unmountOnClose functionality (#1381)
Browse files Browse the repository at this point in the history
  • Loading branch information
EndiM authored and TheSharpieOne committed Jan 30, 2019
1 parent e8a2945 commit 42a32c7
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 9 deletions.
18 changes: 18 additions & 0 deletions docs/lib/Components/ModalsPage.js
Expand Up @@ -11,6 +11,7 @@ import ModalFadelessExample from '../examples/ModalFadeless';
import ModalExternalExample from '../examples/ModalExternal';
import ModalCustomCloseIconExample from '../examples/ModalCustomCloseIcon';
import ModalCustomCloseButtonExample from '../examples/ModalCustomCloseButton';
import ModalDestructuringExample from '../examples/ModalDestructuring';

const ModalBackdropExampleSource = require('!!raw-loader!../examples/ModalBackdrop');
const ModalCustomCloseButtonExampleSource = require('!!raw-loader!../examples/ModalCustomCloseButton');
Expand All @@ -20,6 +21,7 @@ const ModalExampleSource = require('!!raw-loader!../examples/Modal');
const ModalExternalExampleSource = require('!!raw-loader!../examples/ModalExternal');
const ModalFadelessExampleSource = require('!!raw-loader!../examples/ModalFadeless');
const ModalNestedExampleSource = require('!!raw-loader!../examples/ModalNested');
const ModalDestructuringExampleSource = require('!!raw-loader!../examples/ModalDestructuring');

const ModalsPage = () => {
return (
Expand Down Expand Up @@ -97,6 +99,8 @@ const ModalsPage = () => {
// see [Fade](/components/fade/) for more details
modalTransition: PropTypes.shape(Fade.propTypes),
innerRef: PropTypes.object,
// if modal should be destructed/removed from DOM after closing
unmountOnClose: PropTypes.bool // defaults to true
}`}
</PrismCode>
</pre>
Expand Down Expand Up @@ -197,6 +201,20 @@ const ModalsPage = () => {
{ModalCustomCloseButtonExampleSource}
</PrismCode>
</pre>

<h4>Destructuring</h4>
<div className="docs-example">
<div className="btn-group">
<div className="btn">
<ModalDestructuringExample buttonLabel="Launch Modal" />
</div>
</div>
</div>
<pre>
<PrismCode className="language-jsx">
{ModalDestructuringExampleSource}
</PrismCode>
</pre>
</div>
);
};
Expand Down
58 changes: 58 additions & 0 deletions docs/lib/examples/ModalDestructuring.js
@@ -0,0 +1,58 @@
/* eslint react/no-multi-comp: 0, react/prop-types: 0 */

import React from 'react';
import { Button, Modal, ModalHeader, ModalBody, ModalFooter, Input, Label, Form, FormGroup } from 'reactstrap';

class ModalExample extends React.Component {
constructor(props) {
super(props);
this.state = {
modal: false,
unmountOnClose: true
};

this.toggle = this.toggle.bind(this);
this.changeUnmountOnClose = this.changeUnmountOnClose.bind(this);
}

toggle() {
this.setState({
modal: !this.state.modal
});
}

changeUnmountOnClose(e) {
let value = e.target.value;
this.setState({ unmountOnClose: JSON.parse(value) });
}

render() {
return (
<div>
<Form inline onSubmit={(e) => e.preventDefault()}>
<FormGroup>
<Label for="unmountOnClose">UnmountOnClose value</Label>{' '}
<Input type="select" name="unmountOnClose" id="unmountOnClose" onChange={this.changeUnmountOnClose}>
<option value="true">true</option>
<option value="false">false</option>
</Input>
</FormGroup>
{' '}
<Button color="danger" onClick={this.toggle}>{this.props.buttonLabel}</Button>
</Form>
<Modal isOpen={this.state.modal} toggle={this.toggle} className={this.props.className} unmountOnClose={this.state.unmountOnClose}>
<ModalHeader toggle={this.toggle}>Modal title</ModalHeader>
<ModalBody>
<Input type="textarea" placeholder="Write something (data should remain in modal if unmountOnClose is set to false)" rows={5} />
</ModalBody>
<ModalFooter>
<Button color="primary" onClick={this.toggle}>Do Something</Button>{' '}
<Button color="secondary" onClick={this.toggle}>Cancel</Button>
</ModalFooter>
</Modal>
</div>
);
}
}

export default ModalExample;
43 changes: 34 additions & 9 deletions src/Modal.js
Expand Up @@ -54,6 +54,7 @@ const propTypes = {
PropTypes.string,
PropTypes.func,
]),
unmountOnClose: PropTypes.bool
};

const propsToOmit = Object.keys(propTypes);
Expand All @@ -76,6 +77,7 @@ const defaultProps = {
mountOnEnter: true,
timeout: TransitionTimeouts.Fade, // uses standard fade transition
},
unmountOnClose: true
};

class Modal extends React.Component {
Expand Down Expand Up @@ -140,8 +142,11 @@ class Modal extends React.Component {
this.props.onExit();
}

if (this.state.isOpen) {
if (this._element) {
this.destroy();
if (this.state.isOpen) {
this.close();
}
}

this._isMounted = false;
Expand All @@ -153,10 +158,15 @@ class Modal extends React.Component {
}

onClosed(node) {
const { unmountOnClose } = this.props;
// so all methods get called before it is unmounted
this.props.onClosed();
(this.props.modalTransition.onExited || noop)(node);
this.destroy();

if (unmountOnClose) {
this.destroy();
}
this.close();

if (this._isMounted) {
this.setState({ isOpen: false });
Expand Down Expand Up @@ -242,21 +252,25 @@ class Modal extends React.Component {
} catch (err) {
this._triggeringElement = null;
}
this._element = document.createElement('div');
this._element.setAttribute('tabindex', '-1');
this._element.style.position = 'relative';
this._element.style.zIndex = this.props.zIndex;
this._originalBodyPadding = getOriginalBodyPadding();

if (!this._element) {
this._element = document.createElement('div');
this._element.setAttribute('tabindex', '-1');
this._element.style.position = 'relative';
this._element.style.zIndex = this.props.zIndex;
document.body.appendChild(this._element);
}

this._originalBodyPadding = getOriginalBodyPadding();
conditionallyUpdateScrollbar();

document.body.appendChild(this._element);
if (Modal.openCount === 0) {
document.body.className = classNames(
document.body.className,
mapToCssModules('modal-open', this.props.cssModule)
);
}

Modal.openCount += 1;
}

Expand All @@ -270,13 +284,16 @@ class Modal extends React.Component {
if (this._triggeringElement.focus) this._triggeringElement.focus();
this._triggeringElement = null;
}
}

close() {
if (Modal.openCount <= 1) {
const modalOpenClassName = mapToCssModules('modal-open', this.props.cssModule);
// Use regex to prevent matching `modal-open` as part of a different class, e.g. `my-modal-opened`
const modalOpenClassNameRegex = new RegExp(`(^| )${modalOpenClassName}( |$)`);
document.body.className = document.body.className.replace(modalOpenClassNameRegex, ' ').trim();
}

Modal.openCount = Math.max(0, Modal.openCount - 1);

setScrollbarWidth(this._originalBodyPadding);
Expand Down Expand Up @@ -311,7 +328,15 @@ class Modal extends React.Component {
}

render() {
if (this.state.isOpen) {
const {
unmountOnClose
} = this.props;

if (!!this._element && (this.state.isOpen || !unmountOnClose)) {

const isModalHidden = !!this._element && !this.state.isOpen && !unmountOnClose;
this._element.style.display = isModalHidden ? 'none' : 'block';

const {
wrapClassName,
modalClassName,
Expand Down
48 changes: 48 additions & 0 deletions src/__tests__/Modal.spec.js
Expand Up @@ -633,6 +633,54 @@ describe('Modal', () => {
wrapper.unmount();
});

it('should destroy this._element when unmountOnClose prop set to true', () => {
isOpen = true;
const wrapper = mount(
<Modal isOpen={isOpen} toggle={toggle} unmountOnClose={true}>
<button id="clicker">Does Nothing</button>
</Modal>
);
const instance = wrapper.instance();

jest.runTimersToTime(300);
expect(instance._element).toBeTruthy();

toggle();
wrapper.setProps({
isOpen: isOpen
});
jest.runTimersToTime(300);

expect(isOpen).toBe(false);
expect(instance._element).toBe(null);

wrapper.unmount();
});

it('should not destroy this._element when unmountOnClose prop set to false', () => {
isOpen = true;
const wrapper = mount(
<Modal isOpen={isOpen} toggle={toggle} unmountOnClose={false}>
<button id="clicker">Does Nothing</button>
</Modal>
);
const instance = wrapper.instance();

jest.runTimersToTime(300);
expect(instance._element).toBeTruthy();

toggle();
wrapper.setProps({
isOpen: isOpen
});
jest.runTimersToTime(300);

expect(isOpen).toBe(false);
expect(instance._element).toBeTruthy();

wrapper.unmount();
});

it('should destroy this._element on unmount', () => {
isOpen = true;
const wrapper = mount(
Expand Down

0 comments on commit 42a32c7

Please sign in to comment.