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`] = ` + +`; + +exports[`Storyshots StatusAlert alert invoked via a button 1`] = ` +
+ + +
+`; + +exports[`Storyshots StatusAlert alert with a link 1`] = ` + +`; + +exports[`Storyshots StatusAlert basic usage 1`] = ` + +`; + +exports[`Storyshots StatusAlert danger alert 1`] = ` + +`; + +exports[`Storyshots StatusAlert informational alert 1`] = ` + +`; + +exports[`Storyshots StatusAlert success alert 1`] = ` + +`; + 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 {
{this.props.title}
+ ); + } +} + +StatusAlertWrapper.propTypes = { + alertType: PropTypes.string, + dialog: PropTypes.string.isRequired, + +}; + +StatusAlertWrapper.defaultProps = { + alertType: 'warning', +}; + +storiesOf('StatusAlert', module) + .add('basic usage', () => ( + {}} + open + /> + )) + .add('success alert', () => ( + {}} + open + /> + )) + .add('danger alert', () => ( + {}} + open + /> + )) + .add('informational alert', () => ( + {}} + open + /> + )) + .add('Non-dismissible alert', () => ( + + )) + .add('alert invoked via a button', () => ( + + )) + .add('alert with a link', () => ( + + Love cats? + + Click me! + + + )} + 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) ? ( +