Skip to content

Commit 7bf5d0a

Browse files
TheSharpieOneeddywashere
authored andcommitted
feat(modal): add backdrop and keyboard options (#135)
Closes #134 Add backdrop prop to modal with the following options - `true` show backdrop, close on click (default) - `false` hide backdrop - `"static"` show backdrop, do not close on click Add keyboard prop to modal with the following options - `true` close on escape (default) - `false` do not close on escape Add tests and docs
1 parent 74e9d31 commit 7bf5d0a

5 files changed

Lines changed: 203 additions & 5 deletions

File tree

docs/lib/Components/ModalsPage.js

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import Helmet from 'react-helmet';
55
import ModalExample from '../examples/Modal';
66
const ModalExampleSource = require('!!raw!../examples/Modal');
77

8+
import ModalBackdropExample from '../examples/ModalBackdrop';
9+
const ModalBackdropExampleSource = require('!!raw!../examples/ModalBackdrop');
10+
811
export default class ModalsPage extends React.Component {
912
render() {
1013
return (
@@ -37,10 +40,30 @@ export default class ModalsPage extends React.Component {
3740
// boolean to control the state of the popover
3841
toggle: PropTypes.func,
3942
// callback for toggling isOpen in the controlling component
40-
size: PropTypes.string
43+
size: PropTypes.string,
44+
// control backdrop, see http://v4-alpha.getbootstrap.com/components/modal/#options
45+
backdrop: PropTypes.oneOfType([
46+
PropTypes.bool,
47+
PropTypes.oneOf(['static'])
48+
]),
49+
keyboard: PropTypes.bool
4150
}`}
4251
</PrismCode>
4352
</pre>
53+
54+
<h4>Backdrop</h4>
55+
<div className="docs-example">
56+
<div className="btn-group">
57+
<div className="btn">
58+
<ModalBackdropExample buttonLabel="Launch Modal" />
59+
</div>
60+
</div>
61+
</div>
62+
<pre>
63+
<PrismCode className="language-jsx">
64+
{ModalBackdropExampleSource}
65+
</PrismCode>
66+
</pre>
4467
</div>
4568
);
4669
}

docs/lib/examples/ModalBackdrop.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/* eslint react/no-multi-comp: 0, react/prop-types: 0 */
2+
3+
import React from 'react';
4+
import { Button, Modal, ModalHeader, ModalBody, ModalFooter, Input, Label, Form, FormGroup } from 'reactstrap';
5+
6+
class ModalExample extends React.Component {
7+
constructor(props) {
8+
super(props);
9+
this.state = {
10+
modal: false,
11+
backdrop: true
12+
};
13+
14+
this.toggle = this.toggle.bind(this);
15+
this.changeBackdrop = this.changeBackdrop.bind(this);
16+
}
17+
18+
toggle() {
19+
this.setState({
20+
modal: !this.state.modal
21+
});
22+
}
23+
24+
changeBackdrop(e) {
25+
let value = e.target.value;
26+
if (value !== 'static') {
27+
value = JSON.parse(value);
28+
}
29+
this.setState({ backdrop: value });
30+
}
31+
32+
render() {
33+
return (
34+
<div>
35+
<Form inline onSubmit={(e) => e.preventDefault()}>
36+
<FormGroup>
37+
<Label for="backdrop">Backdrop value</Label>{' '}
38+
<Input type="select" name="backdrop" id="backdrop" onChange={this.changeBackdrop}>
39+
<option value="true">true</option>
40+
<option value="false">false</option>
41+
<option value="static">"static"</option>
42+
</Input>
43+
</FormGroup>
44+
{' '}
45+
<Button color="danger" onClick={this.toggle}>{this.props.buttonLabel}</Button>
46+
</Form>
47+
<Modal isOpen={this.state.modal} toggle={this.toggle} className={this.props.className} backdrop={this.state.backdrop}>
48+
<ModalHeader toggle={this.toggle}>Modal title</ModalHeader>
49+
<ModalBody>
50+
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.
51+
</ModalBody>
52+
<ModalFooter>
53+
<Button color="primary" onClick={this.toggle}>Do Something</Button>
54+
<Button color="secondary" onClick={this.toggle}>Cancel</Button>
55+
</ModalFooter>
56+
</Modal>
57+
</div>
58+
);
59+
}
60+
}
61+
62+
export default ModalExample;

src/Modal.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,21 @@ const propTypes = {
88
isOpen: PropTypes.bool,
99
size: PropTypes.string,
1010
toggle: PropTypes.func.isRequired,
11+
keyboard: PropTypes.bool,
12+
backdrop: PropTypes.oneOfType([
13+
PropTypes.bool,
14+
PropTypes.oneOf(['static'])
15+
]),
1116
onEnter: PropTypes.func,
1217
onExit: PropTypes.func,
1318
children: PropTypes.node,
1419
className: PropTypes.any
1520
};
1621

1722
const defaultProps = {
18-
isOpen: false
23+
isOpen: false,
24+
backdrop: true,
25+
keyboard: true
1926
};
2027

2128
class Modal extends React.Component {
@@ -64,12 +71,14 @@ class Modal extends React.Component {
6471
}
6572

6673
handleEscape(e) {
67-
if (e.keyCode === 27) {
74+
if (this.props.keyboard && e.keyCode === 27) {
6875
this.props.toggle();
6976
}
7077
}
7178

7279
handleBackdropClick(e) {
80+
if (this.props.backdrop !== true) return;
81+
7382
const container = this._dialog;
7483

7584
if (e.target && !container.contains(e.target)) {
@@ -168,7 +177,7 @@ class Modal extends React.Component {
168177
</div>
169178
</Fade>
170179
)}
171-
{this.props.isOpen && (
180+
{this.props.isOpen && this.props.backdrop && (
172181
<Fade
173182
key="modal-backdrop"
174183
transitionAppearTimeout={150}

src/TabContent.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export default class TabContent extends Component {
3434
const classes = classnames('tab-content', this.props.className);
3535
return (
3636
<div className={classes}>
37-
{ this.props.children }
37+
{this.props.children}
3838
</div>
3939
);
4040
}

test/Modal.spec.js

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,49 @@ describe('Modal', () => {
3333
wrapper.unmount();
3434
});
3535

36+
it('should render with the backdrop with the class "modal-backdrop" by default', () => {
37+
isOpen = true;
38+
const wrapper = mount(
39+
<Modal isOpen={isOpen} toggle={toggle}>
40+
Yo!
41+
</Modal>
42+
);
43+
44+
jasmine.clock().tick(300);
45+
expect(wrapper.children().length).toBe(0);
46+
expect(document.getElementsByClassName('modal-backdrop').length).toBe(1);
47+
wrapper.unmount();
48+
});
49+
50+
it('should render with the backdrop with the class "modal-backdrop" when backdrop is "static"', () => {
51+
isOpen = true;
52+
const wrapper = mount(
53+
<Modal isOpen={isOpen} toggle={toggle} backdrop="static">
54+
Yo!
55+
</Modal>
56+
);
57+
58+
jasmine.clock().tick(300);
59+
expect(wrapper.children().length).toBe(0);
60+
expect(document.getElementsByClassName('modal-backdrop').length).toBe(1);
61+
wrapper.unmount();
62+
});
63+
64+
it('should not render with the backdrop with the class "modal-backdrop" when backdrop is "false"', () => {
65+
isOpen = true;
66+
const wrapper = mount(
67+
<Modal isOpen={isOpen} toggle={toggle} backdrop={false}>
68+
Yo!
69+
</Modal>
70+
);
71+
72+
jasmine.clock().tick(300);
73+
expect(wrapper.children().length).toBe(0);
74+
expect(document.getElementsByClassName('modal-dialog').length).toBe(1);
75+
expect(document.getElementsByClassName('modal-backdrop').length).toBe(0);
76+
wrapper.unmount();
77+
});
78+
3679
it('should render with class "modal-dialog" and have custom class name if provided', () => {
3780
isOpen = true;
3881
const wrapper = mount(
@@ -248,6 +291,41 @@ describe('Modal', () => {
248291
wrapper.unmount();
249292
});
250293

294+
it('should not close modal when escape key pressed when keyboard is false', () => {
295+
isOpen = true;
296+
const wrapper = mount(
297+
<Modal isOpen={isOpen} toggle={toggle} keyboard={false}>
298+
Yo!
299+
</Modal>
300+
);
301+
const instance = wrapper.instance();
302+
303+
jasmine.clock().tick(300);
304+
305+
expect(isOpen).toBe(true);
306+
expect(document.getElementsByClassName('modal').length).toBe(1);
307+
308+
instance.handleEscape({ keyCode: 13 });
309+
jasmine.clock().tick(300);
310+
311+
expect(isOpen).toBe(true);
312+
expect(document.getElementsByClassName('modal').length).toBe(1);
313+
314+
instance.handleEscape({ keyCode: 27 });
315+
jasmine.clock().tick(300);
316+
317+
expect(isOpen).toBe(true);
318+
319+
wrapper.setProps({
320+
isOpen: isOpen
321+
});
322+
jasmine.clock().tick(300);
323+
324+
expect(document.getElementsByClassName('modal').length).toBe(1);
325+
326+
wrapper.unmount();
327+
});
328+
251329
it('should close modal when clicking backdrop', () => {
252330
isOpen = true;
253331
const wrapper = mount(
@@ -274,6 +352,32 @@ describe('Modal', () => {
274352
wrapper.unmount();
275353
});
276354

355+
it('should not close modal when clicking backdrop and backdrop is "static"', () => {
356+
isOpen = true;
357+
const wrapper = mount(
358+
<Modal isOpen={isOpen} toggle={toggle} backdrop="static">
359+
<button id="clicker">Does Nothing</button>
360+
</Modal>
361+
);
362+
363+
jasmine.clock().tick(300);
364+
365+
expect(isOpen).toBe(true);
366+
expect(document.getElementsByClassName('modal').length).toBe(1);
367+
368+
document.getElementById('clicker').click();
369+
jasmine.clock().tick(300);
370+
371+
expect(isOpen).toBe(true);
372+
373+
document.getElementsByClassName('modal-backdrop')[0].click();
374+
jasmine.clock().tick(300);
375+
376+
expect(isOpen).toBe(true);
377+
378+
wrapper.unmount();
379+
});
380+
277381
it('should destroy this._element', () => {
278382
isOpen = true;
279383
const wrapper = mount(

0 commit comments

Comments
 (0)