diff --git a/.storybook/__snapshots__/Storyshots.test.js.snap b/.storybook/__snapshots__/Storyshots.test.js.snap
index 0164df0219..e7e89dc164 100644
--- a/.storybook/__snapshots__/Storyshots.test.js.snap
+++ b/.storybook/__snapshots__/Storyshots.test.js.snap
@@ -476,7 +476,11 @@ exports[`Storyshots Modal basic usage 1`] = `
onKeyDown={[Function]}
type="button"
>
- ×
+
+ ×
+
`;
+exports[`Storyshots StatusAlert Non-dismissible alert 1`] = `
+
+
+ You can't get rid of me!
+
+
+`;
+
+exports[`Storyshots StatusAlert alert invoked via a button 1`] = `
+
+
+
+
+ Success! You triggered the alert!
+
+
+
+
+`;
+
+exports[`Storyshots StatusAlert alert with a link 1`] = `
+
+`;
+
+exports[`Storyshots StatusAlert basic usage 1`] = `
+
+
+
+ You have a status alert!
+
+
+`;
+
+exports[`Storyshots StatusAlert danger alert 1`] = `
+
+
+
+ Error!
+
+
+`;
+
+exports[`Storyshots StatusAlert informational alert 1`] = `
+
+
+
+ Get some info here!
+
+
+`;
+
+exports[`Storyshots StatusAlert success alert 1`] = `
+
+
+
+ Success!
+
+
+`;
+
exports[`Storyshots Table default heading 1`] = `
{},
+ isClose: false,
onBlur: () => {},
onClick: () => {},
onKeyDown: () => {},
diff --git a/src/Modal/index.jsx b/src/Modal/index.jsx
index 8f2580f215..21b157eb80 100644
--- a/src/Modal/index.jsx
+++ b/src/Modal/index.jsx
@@ -96,7 +96,7 @@ class Modal extends React.Component {
+ )}
+ onClose={() => {}}
+ open
+ />
+ ));
diff --git a/src/StatusAlert/StatusAlert.test.jsx b/src/StatusAlert/StatusAlert.test.jsx
new file mode 100644
index 0000000000..eeeb35c984
--- /dev/null
+++ b/src/StatusAlert/StatusAlert.test.jsx
@@ -0,0 +1,148 @@
+import React from 'react';
+import { mount } from 'enzyme';
+import StatusAlert from './index';
+
+
+const statusAlertOpen = (isOpen, wrapper) => {
+ expect(wrapper.hasClass('show')).toEqual(isOpen);
+ expect(wrapper.state('open')).toEqual(isOpen);
+};
+const dialog = 'Status Alert dialog';
+const defaultProps = {
+ dialog,
+ onClose: () => {},
+ open: true,
+};
+
+let wrapper;
+
+describe('', () => {
+ describe('correct rendering', () => {
+ it('renders default view', () => {
+ wrapper = mount(
+ ,
+ );
+ const statusAlertDialog = wrapper.find('.alert-dialog');
+
+ expect(statusAlertDialog.text()).toEqual(dialog);
+ expect(wrapper.find('button')).toHaveLength(1);
+ });
+
+ it('renders non-dismissible view', () => {
+ wrapper = mount(
+ ,
+ );
+ const statusAlertDialog = wrapper.find('.alert-dialog');
+
+ expect(statusAlertDialog.text()).toEqual(dialog);
+ expect(wrapper.find('button')).toHaveLength(0);
+ });
+ });
+
+ describe('props received correctly', () => {
+ it('component receives props', () => {
+ wrapper = mount(
+ {}}
+ />,
+ );
+
+ statusAlertOpen(false, wrapper);
+ wrapper.setProps({ open: true });
+ statusAlertOpen(true, wrapper);
+ });
+
+ it('component receives props and ignores prop change', () => {
+ wrapper = mount(
+ ,
+ );
+
+ statusAlertOpen(true, wrapper);
+ wrapper.setProps({ dialog: 'Changed alert dialog' });
+ statusAlertOpen(true, wrapper);
+ });
+ });
+
+ describe('close functions properly', () => {
+ beforeEach(() => {
+ wrapper = mount(
+ ,
+ );
+ });
+
+ it('closes when x button pressed', () => {
+ statusAlertOpen(true, wrapper);
+ wrapper.find('button').at(0).simulate('click');
+ statusAlertOpen(false, wrapper);
+ });
+
+ it('closes when Enter key pressed', () => {
+ statusAlertOpen(true, wrapper);
+ wrapper.find('button').at(0).simulate('keyDown', { key: 'Enter' });
+ statusAlertOpen(false, wrapper);
+ });
+
+ it('closes when Escape key pressed', () => {
+ statusAlertOpen(true, wrapper);
+ wrapper.find('button').at(0).simulate('keyDown', { key: 'Escape' });
+ statusAlertOpen(false, wrapper);
+ });
+
+ it('calls callback function on close', () => {
+ const spy = jest.fn();
+
+ wrapper = mount(
+ ,
+ );
+
+ expect(spy).toHaveBeenCalledTimes(0);
+
+ // press X button
+ wrapper.find('button').at(0).simulate('click');
+ expect(spy).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('invalid keystrokes do nothing', () => {
+ beforeEach(() => {
+ wrapper = mount(
+ ,
+ );
+ });
+
+ it('does nothing on invalid keystroke q', () => {
+ const buttons = wrapper.find('button');
+
+ expect(buttons.at(0).matchesElement(document.activeElement)).toEqual(true);
+ statusAlertOpen(true, wrapper);
+ buttons.at(0).simulate('keyDown', { key: 'q' });
+ expect(buttons.at(0).matchesElement(document.activeElement)).toEqual(true);
+ statusAlertOpen(true, wrapper);
+ });
+
+ it('does nothing on invalid keystroke + ctrl', () => {
+ const buttons = wrapper.find('button');
+
+ expect(buttons.at(0).matchesElement(document.activeElement)).toEqual(true);
+ statusAlertOpen(true, wrapper);
+ buttons.at(0).simulate('keyDown', { key: 'Tab', ctrlKey: true });
+ expect(buttons.at(0).matchesElement(document.activeElement)).toEqual(true);
+ statusAlertOpen(true, wrapper);
+ });
+ });
+});
diff --git a/src/StatusAlert/index.jsx b/src/StatusAlert/index.jsx
new file mode 100644
index 0000000000..040a9082dc
--- /dev/null
+++ b/src/StatusAlert/index.jsx
@@ -0,0 +1,120 @@
+import React from 'react';
+import classNames from 'classnames';
+import PropTypes from 'prop-types';
+import isRequiredIf from 'react-proptype-conditional-require';
+
+import styles from './StatusAlert.scss';
+import Button from '../Button';
+
+class StatusAlert extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.close = this.close.bind(this);
+ this.handleKeyDown = this.handleKeyDown.bind(this);
+ this.renderDialog = this.renderDialog.bind(this);
+
+ this.state = {
+ open: props.open,
+ };
+ }
+
+ componentDidMount() {
+ if (this.xButton) {
+ this.xButton.focus();
+ }
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (nextProps.open !== this.props.open) {
+ this.setState({ open: nextProps.open });
+ }
+ }
+
+ componentDidUpdate(prevState) {
+ if (this.state.open && !prevState.open) {
+ this.xButton.focus();
+ }
+ }
+
+ close() {
+ this.setState({ open: false });
+ this.props.onClose();
+ }
+
+ handleKeyDown(e) {
+ if (e.key === 'Enter' || e.key === 'Escape') {
+ e.preventDefault();
+ this.close();
+ }
+ }
+
+ renderDialog() {
+ const { dialog } = this.props;
+
+ return (
+
+ { dialog }
+
+ );
+ }
+
+ renderDismissible() {
+ const { dismissible } = this.props;
+
+ return (dismissible) ? (
+ { this.xButton = input; }}
+ onClick={this.close}
+ onKeyDown={this.handleKeyDown}
+ display={×}
+ isClose
+ />
+ ) : null;
+ }
+
+ render() {
+ const { alertType, className, dismissible } = this.props;
+
+ return (
+
+ {this.renderDismissible()}
+ {this.renderDialog()}
+
+ );
+ }
+}
+
+StatusAlert.propTypes = {
+ alertType: PropTypes.string,
+ className: PropTypes.arrayOf(PropTypes.string),
+ dialog: PropTypes.oneOfType([PropTypes.string, PropTypes.element]).isRequired,
+ dismissible: PropTypes.bool,
+ /* eslint-disable react/require-default-props */
+ onClose: isRequiredIf(PropTypes.func, props => props.dismissible),
+ open: PropTypes.bool,
+};
+
+StatusAlert.defaultProps = {
+ alertType: 'warning',
+ className: [],
+ dismissible: true,
+ open: false,
+};
+
+export default StatusAlert;