Skip to content

Commit 240e776

Browse files
Aaron Panchaleddywashere
authored andcommitted
feat(Alert): Add Alert component (#162)
* feat(Alert): add alert component Adds a component that renders a Bootstrap 4 alert. Supports dismissing alerts and animated fade out. * docs(Alert): add examples for standard and dismissible alerts * Rename Alert docs to Alerts * Make success default alert color * Combine Alert with transition group and use single-child pattern * Add props to Alert for configuring animation timeout - Use value of zero to not animate * Add react-addons-css-transition-group in npm install command in docs * Add properties description to Alerts documentation
1 parent 8c8c0b5 commit 240e776

11 files changed

Lines changed: 304 additions & 1 deletion

File tree

docs/lib/Components/AlertsPage.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/* eslint react/no-multi-comp: 0, react/prop-types: 0 */
2+
import React from 'react';
3+
import { PrismCode } from 'react-prism';
4+
import { Alert } from 'reactstrap';
5+
import Helmet from 'react-helmet';
6+
7+
import AlertExample from '../examples/Alert';
8+
const AlertExampleSource = require('!!raw!../examples/Alert');
9+
10+
import AlertDismissExample from '../examples/AlertDismiss';
11+
const AlertDismissExampleSource = require('!!raw!../examples/AlertDismiss');
12+
13+
export default class AlertsPage extends React.Component {
14+
render() {
15+
return (
16+
<div>
17+
<Helmet title="Alerts" />
18+
19+
<h3>Alerts</h3>
20+
<div className="docs-example">
21+
<AlertExample />
22+
</div>
23+
<pre>
24+
<PrismCode className="language-jsx">
25+
{AlertExampleSource}
26+
</PrismCode>
27+
</pre>
28+
29+
<h3>Properties</h3>
30+
<pre>
31+
<PrismCode className="language-jsx">
32+
{`Alert.propTypes = {
33+
className: PropTypes.any,
34+
color: PropTypes.string, // default: 'success'
35+
isOpen: PropTypes.bool, // default: true
36+
toggle: PropTypes.func,
37+
tag: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
38+
39+
// Set any of the timeouts to 0 to disable animation
40+
transitionAppearTimeout: PropTypes.number,
41+
transitionEnterTimeout: PropTypes.number,
42+
transitionLeaveTimeout: PropTypes.number
43+
}`}
44+
</PrismCode>
45+
</pre>
46+
47+
<h3>Dismissing</h3>
48+
<div className="docs-example">
49+
<AlertDismissExample />
50+
</div>
51+
<pre>
52+
<PrismCode className="language-jsx">
53+
{AlertDismissExampleSource}
54+
</PrismCode>
55+
</pre>
56+
</div>
57+
);
58+
}
59+
}

docs/lib/Components/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@ class Components extends React.Component {
104104
{
105105
name: 'Jumbotron',
106106
to: '/components/jumbotron/'
107+
},
108+
{
109+
name: 'Alerts',
110+
to: '/components/alerts/'
107111
}
108112
]
109113
};

docs/lib/Home/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export default () => {
3636
<h3>NPM</h3>
3737
<p>Install reactstrap and peer dependencies via NPM</p>
3838
<pre>
39-
<PrismCode className="language-bash">npm install --save reactstrap react-addons-transition-group react react-dom</PrismCode>
39+
<PrismCode className="language-bash">npm install --save reactstrap react-addons-transition-group react-addons-css-transition-group react react-dom</PrismCode>
4040
</pre>
4141
<p>ES6 - import the components you need</p>
4242
<div className="docs-example">

docs/lib/examples/Alert.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import React from 'react';
2+
import { Alert } from 'reactstrap';
3+
4+
const Example = (props) => {
5+
return (
6+
<div>
7+
<Alert color="success">
8+
<strong>Well done!</strong> You successfully read this important alert message.
9+
</Alert>
10+
<Alert color="info">
11+
<strong>Heads up!</strong> This alert needs your attention, but it's not super important.
12+
</Alert>
13+
<Alert color="warning">
14+
<strong>Warning!</strong> Better check yourself, you're not looking too good.
15+
</Alert>
16+
<Alert color="danger">
17+
<strong>Oh snap!</strong> Change a few things up and try submitting again.
18+
</Alert>
19+
</div>
20+
);
21+
};
22+
23+
export default Example;

docs/lib/examples/AlertDismiss.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import React from 'react';
2+
import { Alert } from 'reactstrap';
3+
4+
class AlertExample extends React.Component {
5+
constructor(props) {
6+
super(props);
7+
8+
this.state = {
9+
visible: true
10+
}
11+
}
12+
13+
onDismiss = () => {
14+
this.setState({ visible: false });
15+
}
16+
17+
render() {
18+
return (
19+
<Alert color="info" isOpen={this.state.visible} toggle={this.onDismiss}>
20+
I am an alert and I can be dismissed!
21+
</Alert>
22+
);
23+
}
24+
}
25+
26+
export default AlertExample;

docs/lib/routes.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import TablesPage from './Components/TablesPage';
2222
import PaginationPage from './Components/PaginationPage';
2323
import TabsPage from './Components/TabsPage';
2424
import JumbotronPage from './Components/JumbotronPage';
25+
import AlertsPage from './Components/AlertsPage';
2526
import NotFound from './NotFound';
2627
import Components from './Components';
2728
import UI from './UI';
@@ -51,6 +52,7 @@ const routes = (
5152
<Route path="media/" component={MediaPage} />
5253
<Route path="pagination/" component={PaginationPage} />
5354
<Route path="tabs/" component={TabsPage} />
55+
<Route path="alerts/" component={AlertsPage} />
5456
<Route path="jumbotron/" component={JumbotronPage} />
5557
</Route>
5658
<Route path="*" component={NotFound} />

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
"peerDependencies": {
5454
"react": "^0.14.0 || ^15.0.0",
5555
"react-addons-transition-group": "^0.14.0 || ^15.0.0",
56+
"react-addons-css-transition-group": "^0.14.0 || ^15.0.0",
5657
"react-dom": "^0.14.0 || ^15.0.0"
5758
},
5859
"devDependencies": {
@@ -84,6 +85,7 @@
8485
"json-loader": "^0.5.4",
8586
"raw-loader": "^0.5.1",
8687
"react": "^15.3.0",
88+
"react-addons-css-transition-group": "^15.3.2",
8789
"react-addons-test-utils": "^15.3.0",
8890
"react-addons-transition-group": "^15.3.0",
8991
"react-dom": "^15.3.0",

src/Alert.js

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import React, { PropTypes } from 'react';
2+
import classNames from 'classnames';
3+
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
4+
5+
const FirstChild = ({ children }) => (
6+
React.Children.toArray(children)[0] || null
7+
);
8+
9+
const propTypes = {
10+
className: PropTypes.any,
11+
color: PropTypes.string,
12+
isOpen: PropTypes.bool,
13+
toggle: PropTypes.func,
14+
tag: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
15+
transitionAppearTimeout: PropTypes.number,
16+
transitionEnterTimeout: PropTypes.number,
17+
transitionLeaveTimeout: PropTypes.number
18+
};
19+
20+
const defaultProps = {
21+
color: 'success',
22+
isOpen: true,
23+
tag: 'div',
24+
transitionAppearTimeout: 150,
25+
transitionEnterTimeout: 150,
26+
transitionLeaveTimeout: 150
27+
};
28+
29+
const Alert = (props) => {
30+
const {
31+
className,
32+
tag: Tag,
33+
color,
34+
isOpen,
35+
toggle,
36+
children,
37+
transitionAppearTimeout,
38+
transitionEnterTimeout,
39+
transitionLeaveTimeout,
40+
...attributes
41+
} = props;
42+
43+
const classes = classNames(
44+
className,
45+
'alert',
46+
`alert-${color}`,
47+
{ 'alert-dismissible': toggle }
48+
);
49+
50+
const alert = (
51+
<Tag {...attributes} className={classes} role="alert">
52+
{ toggle ?
53+
<button type="button" className="close" aria-label="Close" onClick={toggle}>
54+
<span aria-hidden="true">&times;</span>
55+
</button>
56+
: null }
57+
{ children }
58+
</Tag>
59+
);
60+
61+
return (
62+
<ReactCSSTransitionGroup
63+
component={FirstChild}
64+
transitionName={{
65+
appear: 'fade',
66+
appearActive: 'in',
67+
enter: 'fade',
68+
enterActive: 'in',
69+
leave: 'fade',
70+
leaveActive: 'out'
71+
}}
72+
transitionAppear={transitionAppearTimeout > 0}
73+
transitionAppearTimeout={transitionAppearTimeout}
74+
transitionEnter={transitionEnterTimeout > 0}
75+
transitionEnterTimeout={transitionEnterTimeout}
76+
transitionLeave={transitionLeaveTimeout > 0}
77+
transitionLeaveTimeout={transitionLeaveTimeout}
78+
>
79+
{isOpen ? alert : null}
80+
</ReactCSSTransitionGroup>
81+
);
82+
}
83+
84+
Alert.propTypes = propTypes;
85+
Alert.defaultProps = defaultProps;
86+
87+
export default Alert;

src/__tests__/Alert.spec.js

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import React from 'react';
2+
import { shallow } from 'enzyme';
3+
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
4+
import Alert from '../Alert';
5+
6+
describe('Alert', () => {
7+
it('should render children', () => {
8+
const alert = shallow(<Alert>Yo!</Alert>).find('div');
9+
expect(alert.text()).toBe('Yo!');
10+
});
11+
12+
it('should transition on appear, enter, and leave using fade', () => {
13+
const alert = shallow(<Alert>Yo!</Alert>);
14+
expect(alert.prop('transitionName')).toEqual({
15+
appear: 'fade',
16+
appearActive: 'in',
17+
enter: 'fade',
18+
enterActive: 'in',
19+
leave: 'fade',
20+
leaveActive: 'out'
21+
});
22+
});
23+
24+
it('should have default transitionTimeouts', () => {
25+
const alert = shallow(<Alert>Yo!</Alert>);
26+
27+
expect(alert.prop('transitionAppearTimeout')).toBe(150);
28+
expect(alert.prop('transitionAppear')).toBe(true);
29+
expect(alert.prop('transitionEnterTimeout')).toBe(150);
30+
expect(alert.prop('transitionEnter')).toBe(true);
31+
expect(alert.prop('transitionLeaveTimeout')).toBe(150);
32+
expect(alert.prop('transitionLeave')).toBe(true);
33+
});
34+
35+
it('should have support configurable transitionTimeouts', () => {
36+
const alert = shallow(
37+
<Alert
38+
transitionAppearTimeout={0}
39+
transitionEnterTimeout={0}
40+
transitionLeaveTimeout={0}>
41+
Yo!
42+
</Alert>
43+
);
44+
45+
expect(alert.prop('transitionAppearTimeout')).toBe(0);
46+
expect(alert.prop('transitionAppear')).toBe(false);
47+
expect(alert.prop('transitionEnterTimeout')).toBe(0);
48+
expect(alert.prop('transitionEnter')).toBe(false);
49+
expect(alert.prop('transitionLeaveTimeout')).toBe(0);
50+
expect(alert.prop('transitionLeave')).toBe(false);
51+
});
52+
53+
it('should have "success" as default color', () => {
54+
const alert = shallow(<Alert>Yo!</Alert>).find('div');
55+
expect(alert.hasClass('alert-success')).toBe(true);
56+
});
57+
58+
it('should accept color prop', () => {
59+
const alert = shallow(<Alert color="warning">Yo!</Alert>).find('div');
60+
expect(alert.hasClass('alert-warning')).toBe(true);
61+
});
62+
63+
it('should use a div tag by default', () => {
64+
const alert = shallow(<Alert>Yo!</Alert>).children().first();
65+
expect(alert.type()).toBe('div');
66+
});
67+
68+
it('should be non dismissible by default', () => {
69+
const alert = shallow(<Alert>Yo!</Alert>).find('div');
70+
expect(alert.find('button').length).toEqual(0);
71+
expect(alert.hasClass('alert-dismissible')).toBe(false);
72+
});
73+
74+
it('should show dismiss button if passed toggle', () => {
75+
const alert = shallow(<Alert color="danger" toggle={() => {}}>Yo!</Alert>).find('div');
76+
expect(alert.find('button').length).toEqual(1);
77+
expect(alert.hasClass('alert-dismissible')).toBe(true);
78+
});
79+
80+
it('should support custom tag', () => {
81+
const alert = shallow(<Alert tag="p">Yo!</Alert>).children().first();
82+
expect(alert.type()).toBe('p');
83+
});
84+
85+
it('should be empty if not isOpen', () => {
86+
const alert = shallow(<Alert isOpen={false}>Yo!</Alert>);
87+
expect(alert.html()).toBe('');
88+
});
89+
90+
it('should be dismissible', () => {
91+
const onClick = jasmine.createSpy('onClick');
92+
const alert = shallow(<Alert color="danger" toggle={onClick}>Yo!</Alert>);
93+
94+
alert.find('button').simulate('click');
95+
expect(onClick).toHaveBeenCalled();
96+
});
97+
});

src/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,10 @@ import PaginationLink from './PaginationLink';
6161
import TabContent from './TabContent';
6262
import TabPane from './TabPane';
6363
import Jumbotron from './Jumbotron';
64+
import Alert from './Alert';
6465

6566
export {
67+
Alert,
6668
Container,
6769
Row,
6870
Col,

0 commit comments

Comments
 (0)