Skip to content

Commit babee0f

Browse files
Anne Johnsoneddywashere
authored andcommitted
feat(Modal): Support fade and timeout props in the Modal component to allow configuring + disabling of the fade effect (#339)
* Support a fade=false prop in the Modal component - if fade=false, the following happens: - no 'fade' class is applied - the modal + backdrop transition timeouts become 0 - the transitionAppear, etc. booleans passed to Fade are false * Update fadeless-modal to not use the Fade component - This feels like a better design. If we are not doing a fade effect, why use a Fade component? - This also gets rid of the usage of TransitionGroup when fade={false}. - Ensure onEnter and onExit fire when expected. * DRY up shared code between fade-modal and fadeless-modal - Extract modalDialog rendering into a separate method - Simplify the variable setting in #renderChildren following the movement of several of them into #modalDialog * Make modalTransitionTimeout and backdropTransitionTimeout configurable as props - Update docs - Add test * Fix warning about missing context in DropdownMenu spec * Allow configuring appear v. enter v. leave timeouts separately
1 parent f6ef088 commit babee0f

5 files changed

Lines changed: 337 additions & 42 deletions

File tree

docs/lib/Components/ModalsPage.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ const ModalBackdropExampleSource = require('!!raw!../examples/ModalBackdrop');
1111
import ModalNestedExample from '../examples/ModalNested';
1212
const ModalNestedExampleSource = require('!!raw!../examples/ModalNested');
1313

14+
import ModalCustomTimeoutExample from '../examples/ModalCustomTimeout';
15+
const ModalCustomTimeoutExampleSource = require('!!raw!../examples/ModalCustomTimeout');
16+
17+
import ModalFadelessExample from '../examples/ModalFadeless';
18+
const ModalFadelessExampleSource = require('!!raw!../examples/ModalFadeless');
19+
1420
export default class ModalsPage extends React.Component {
1521
render() {
1622
return (
@@ -60,6 +66,22 @@ export default class ModalsPage extends React.Component {
6066
modalClassName: PropTypes.string,
6167
backdropClassName: PropTypes.string,
6268
contentClassName: PropTypes.string,
69+
// boolean to control whether the fade transition occurs (default: true)
70+
fade: PropTypes.bool,
71+
// modalTransitionTimeout - controls appear, enter, and leave (default: 300)
72+
// If you need different values for appear v. enter v. leave, use the more
73+
// specific props like modalTransitionAppearTimeout.
74+
modalTransitionTimeout: PropTypes.number,
75+
modalTransitionAppearTimeout: PropTypes.number,
76+
modalTransitionEnterTimeout: PropTypes.number,
77+
modalTransitionLeaveTimeout: PropTypes.number,
78+
// backdropTransitionTimeout - controls appear, enter, and leave (default: 150)
79+
// If you need different values for appear v. enter v. leave, use the more
80+
// specific props like backdropTransitionAppearTimeout.
81+
backdropTransitionTimeout: PropTypes.number
82+
backdropTransitionAppearTimeout: PropTypes.number,
83+
backdropTransitionEnterTimeout: PropTypes.number,
84+
backdropTransitionLeaveTimeout: PropTypes.number,
6385
}`}
6486
</PrismCode>
6587
</pre>
@@ -91,6 +113,34 @@ export default class ModalsPage extends React.Component {
91113
{ModalNestedExampleSource}
92114
</PrismCode>
93115
</pre>
116+
117+
<h4>Modals with Custom Transition Timeouts</h4>
118+
<div className="docs-example">
119+
<div className="btn-group">
120+
<div className="btn">
121+
<ModalCustomTimeoutExample buttonLabel="Launch Modal with Custom Transition Timeouts Example" />
122+
</div>
123+
</div>
124+
</div>
125+
<pre>
126+
<PrismCode className="language-jsx">
127+
{ModalCustomTimeoutExampleSource}
128+
</PrismCode>
129+
</pre>
130+
131+
<h4>Modals without Fade Effect</h4>
132+
<div className="docs-example">
133+
<div className="btn-group">
134+
<div className="btn">
135+
<ModalFadelessExample buttonLabel="Launch Modal without Fade Effect Example" />
136+
</div>
137+
</div>
138+
</div>
139+
<pre>
140+
<PrismCode className="language-jsx">
141+
{ModalFadelessExampleSource}
142+
</PrismCode>
143+
</pre>
94144
</div>
95145
);
96146
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/* eslint react/no-multi-comp: 0, react/prop-types: 0 */
2+
3+
import React from 'react';
4+
import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap';
5+
6+
class ModalExample extends React.Component {
7+
constructor(props) {
8+
super(props);
9+
this.state = {
10+
modal: false
11+
};
12+
13+
this.toggle = this.toggle.bind(this);
14+
}
15+
16+
toggle() {
17+
this.setState({
18+
modal: !this.state.modal
19+
});
20+
}
21+
22+
render() {
23+
return (
24+
<div>
25+
<Button color="danger" onClick={this.toggle}>{this.props.buttonLabel}</Button>
26+
<Modal isOpen={this.state.modal} modalTransitionTimeout={20} backdropTransitionTimeout={10} toggle={this.toggle} className={this.props.className}>
27+
<ModalHeader toggle={this.toggle}>Modal title</ModalHeader>
28+
<ModalBody>
29+
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
30+
</ModalBody>
31+
<ModalFooter>
32+
<Button color="primary" onClick={this.toggle}>Do Something</Button>{' '}
33+
<Button color="secondary" onClick={this.toggle}>Cancel</Button>
34+
</ModalFooter>
35+
</Modal>
36+
</div>
37+
);
38+
}
39+
}
40+
41+
export default ModalExample;

docs/lib/examples/ModalFadeless.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/* eslint react/no-multi-comp: 0, react/prop-types: 0 */
2+
3+
import React from 'react';
4+
import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap';
5+
6+
class ModalExample extends React.Component {
7+
constructor(props) {
8+
super(props);
9+
this.state = {
10+
modal: false
11+
};
12+
13+
this.toggle = this.toggle.bind(this);
14+
}
15+
16+
toggle() {
17+
this.setState({
18+
modal: !this.state.modal
19+
});
20+
}
21+
22+
render() {
23+
return (
24+
<div>
25+
<Button color="danger" onClick={this.toggle}>{this.props.buttonLabel}</Button>
26+
<Modal isOpen={this.state.modal} fade={false} toggle={this.toggle} className={this.props.className}>
27+
<ModalHeader toggle={this.toggle}>Modal title</ModalHeader>
28+
<ModalBody>
29+
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
30+
</ModalBody>
31+
<ModalFooter>
32+
<Button color="primary" onClick={this.toggle}>Do Something</Button>{' '}
33+
<Button color="secondary" onClick={this.toggle}>Cancel</Button>
34+
</ModalFooter>
35+
</Modal>
36+
</div>
37+
);
38+
}
39+
}
40+
41+
export default ModalExample;

src/Modal.js

Lines changed: 122 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import React from 'react';
22
import ReactDOM from 'react-dom';
33
import classNames from 'classnames';
4-
import omit from 'lodash.omit';
54
import TransitionGroup from 'react-addons-transition-group';
65
import Fade from './Fade';
76
import {
@@ -29,18 +28,30 @@ const propTypes = {
2928
modalClassName: PropTypes.string,
3029
backdropClassName: PropTypes.string,
3130
contentClassName: PropTypes.string,
31+
fade: PropTypes.bool,
3232
cssModule: PropTypes.object,
3333
zIndex: PropTypes.oneOfType([
3434
PropTypes.number,
3535
PropTypes.string,
3636
]),
37+
backdropTransitionTimeout: PropTypes.number,
38+
backdropTransitionAppearTimeout: PropTypes.number,
39+
backdropTransitionEnterTimeout: PropTypes.number,
40+
backdropTransitionLeaveTimeout: PropTypes.number,
41+
modalTransitionTimeout: PropTypes.number,
42+
modalTransitionAppearTimeout: PropTypes.number,
43+
modalTransitionEnterTimeout: PropTypes.number,
44+
modalTransitionLeaveTimeout: PropTypes.number,
3745
};
3846

3947
const defaultProps = {
4048
isOpen: false,
4149
backdrop: true,
4250
keyboard: true,
4351
zIndex: 1050,
52+
fade: true,
53+
modalTransitionTimeout: 300,
54+
backdropTransitionTimeout: 150,
4455
};
4556

4657
class Modal extends React.Component {
@@ -106,12 +117,26 @@ class Modal extends React.Component {
106117
}
107118
}
108119

120+
hasTransition() {
121+
if (this.props.fade === false) {
122+
return false;
123+
}
124+
125+
return this.props.modalTransitionTimeout > 0;
126+
}
127+
109128
togglePortal() {
110129
if (this.props.isOpen) {
111130
this._focus = true;
112131
this.show();
132+
if (!this.hasTransition()) {
133+
this.onEnter();
134+
}
113135
} else {
114136
this.hide();
137+
if (!this.hasTransition()) {
138+
this.onExit();
139+
}
115140
}
116141
}
117142

@@ -151,6 +176,27 @@ class Modal extends React.Component {
151176
this.renderIntoSubtree();
152177
}
153178

179+
renderModalDialog() {
180+
return (
181+
<div
182+
className={mapToCssModules(classNames('modal-dialog', this.props.className, {
183+
[`modal-${this.props.size}`]: this.props.size
184+
}), this.props.cssModule)}
185+
role="document"
186+
ref={(c) => (this._dialog = c)}
187+
>
188+
<div
189+
className={mapToCssModules(
190+
classNames('modal-content', this.props.contentClassName),
191+
this.props.cssModule
192+
)}
193+
>
194+
{this.props.children}
195+
</div>
196+
</div>
197+
);
198+
}
199+
154200
renderIntoSubtree() {
155201
ReactDOM.unstable_renderSubtreeIntoContainer(
156202
this,
@@ -167,61 +213,95 @@ class Modal extends React.Component {
167213

168214
renderChildren() {
169215
const {
170-
className,
171216
wrapClassName,
172217
modalClassName,
173218
backdropClassName,
174-
contentClassName,
175219
cssModule,
176220
isOpen,
177-
size,
178221
backdrop,
179-
children,
180-
...attributes
181-
} = omit(this.props, ['toggle', 'keyboard', 'onEnter', 'onExit', 'zIndex']);
222+
modalTransitionTimeout,
223+
backdropTransitionTimeout
224+
} = this.props;
225+
226+
const modalAttributes = {
227+
onClickCapture: this.handleBackdropClick,
228+
onKeyUp: this.handleEscape,
229+
style: { display: 'block' },
230+
tabIndex: '-1'
231+
};
232+
233+
if (this.hasTransition()) {
234+
return (
235+
<TransitionGroup component="div" className={mapToCssModules(wrapClassName)}>
236+
{isOpen && (
237+
<Fade
238+
key="modal-dialog"
239+
onEnter={this.onEnter}
240+
onLeave={this.onExit}
241+
transitionAppearTimeout={
242+
typeof this.props.modalTransitionAppearTimeout === 'number'
243+
? this.props.modalTransitionAppearTimeout
244+
: modalTransitionTimeout
245+
}
246+
transitionEnterTimeout={
247+
typeof this.props.modalTransitionEnterTimeout === 'number'
248+
? this.props.modalTransitionEnterTimeout
249+
: modalTransitionTimeout
250+
}
251+
transitionLeaveTimeout={
252+
typeof this.props.modalTransitionLeaveTimeout === 'number'
253+
? this.props.modalTransitionLeaveTimeout
254+
: modalTransitionTimeout
255+
}
256+
cssModule={cssModule}
257+
className={mapToCssModules(classNames('modal', modalClassName), cssModule)}
258+
{...modalAttributes}
259+
>
260+
{this.renderModalDialog()}
261+
</Fade>
262+
)}
263+
{isOpen && backdrop && (
264+
<Fade
265+
key="modal-backdrop"
266+
transitionAppearTimeout={
267+
typeof this.props.backdropTransitionAppearTimeout === 'number'
268+
? this.props.backdropTransitionAppearTimeout
269+
: backdropTransitionTimeout
270+
}
271+
transitionEnterTimeout={
272+
typeof this.props.backdropTransitionEnterTimeout === 'number'
273+
? this.props.backdropTransitionEnterTimeout
274+
: backdropTransitionTimeout
275+
}
276+
transitionLeaveTimeout={
277+
typeof this.props.backdropTransitionLeaveTimeout === 'number'
278+
? this.props.backdropTransitionLeaveTimeout
279+
: backdropTransitionTimeout
280+
}
281+
cssModule={cssModule}
282+
className={mapToCssModules(classNames('modal-backdrop', backdropClassName), cssModule)}
283+
/>
284+
)}
285+
</TransitionGroup>
286+
);
287+
}
182288

183289
return (
184-
<TransitionGroup component="div" className={mapToCssModules(wrapClassName)}>
290+
<div className={mapToCssModules(wrapClassName)}>
185291
{isOpen && (
186-
<Fade
187-
key="modal-dialog"
188-
onEnter={this.onEnter}
189-
onLeave={this.onExit}
190-
transitionAppearTimeout={300}
191-
transitionEnterTimeout={300}
192-
transitionLeaveTimeout={300}
193-
onClickCapture={this.handleBackdropClick}
194-
onKeyUp={this.handleEscape}
195-
cssModule={cssModule}
196-
className={mapToCssModules(classNames('modal', modalClassName), cssModule)}
197-
style={{ display: 'block' }}
198-
tabIndex="-1"
292+
<div
293+
className={mapToCssModules(classNames('modal', 'show', modalClassName), cssModule)}
294+
{...modalAttributes}
199295
>
200-
<div
201-
className={mapToCssModules(classNames('modal-dialog', className, {
202-
[`modal-${size}`]: size
203-
}), cssModule)}
204-
role="document"
205-
ref={(c) => (this._dialog = c)}
206-
{...attributes}
207-
>
208-
<div className={mapToCssModules(classNames('modal-content', contentClassName), cssModule)}>
209-
{children}
210-
</div>
211-
</div>
212-
</Fade>
296+
{this.renderModalDialog()}
297+
</div>
213298
)}
214299
{isOpen && backdrop && (
215-
<Fade
216-
key="modal-backdrop"
217-
transitionAppearTimeout={150}
218-
transitionEnterTimeout={150}
219-
transitionLeaveTimeout={150}
220-
cssModule={cssModule}
221-
className={mapToCssModules(classNames('modal-backdrop', backdropClassName), cssModule)}
300+
<div
301+
className={mapToCssModules(classNames('modal-backdrop', 'show', backdropClassName), cssModule)}
222302
/>
223303
)}
224-
</TransitionGroup>
304+
</div>
225305
);
226306
}
227307

0 commit comments

Comments
 (0)