diff --git a/cypress/integration/NoticeBox/accepts_children.feature b/cypress/integration/NoticeBox/accepts_children.feature new file mode 100644 index 0000000000..6fca591ccd --- /dev/null +++ b/cypress/integration/NoticeBox/accepts_children.feature @@ -0,0 +1,5 @@ +Feature: The NoticeBox can render an optional message + + Scenario: A NoticeBox is provided a message + Given a NoticeBox receives a message as children + Then the message is visible diff --git a/cypress/integration/NoticeBox/accepts_children/index.js b/cypress/integration/NoticeBox/accepts_children/index.js new file mode 100644 index 0000000000..c295f86214 --- /dev/null +++ b/cypress/integration/NoticeBox/accepts_children/index.js @@ -0,0 +1,12 @@ +import { Given, Then } from 'cypress-cucumber-preprocessor/steps' + +Given('a NoticeBox receives a message as children', () => { + cy.visitStory('NoticeBox', 'With children') + cy.get('[data-test="dhis2-uicore-noticebox"]').should('be.visible') +}) + +Then('the message is visible', () => { + cy.get('[data-test="dhis2-uicore-noticebox-message"]') + .contains('The noticebox content') + .should('be.visible') +}) diff --git a/cypress/integration/NoticeBox/accepts_title.feature b/cypress/integration/NoticeBox/accepts_title.feature new file mode 100644 index 0000000000..cc3f49d09c --- /dev/null +++ b/cypress/integration/NoticeBox/accepts_title.feature @@ -0,0 +1,5 @@ +Feature: The NoticeBox can render an optional title + + Scenario: A NoticeBox is provided a title + Given a NoticeBox receives a title prop + Then the title is visible diff --git a/cypress/integration/NoticeBox/accepts_title/index.js b/cypress/integration/NoticeBox/accepts_title/index.js new file mode 100644 index 0000000000..af4a779f42 --- /dev/null +++ b/cypress/integration/NoticeBox/accepts_title/index.js @@ -0,0 +1,12 @@ +import { Given, Then } from 'cypress-cucumber-preprocessor/steps' + +Given('a NoticeBox receives a title prop', () => { + cy.visitStory('NoticeBox', 'With title') + cy.get('[data-test="dhis2-uicore-noticebox"]').should('be.visible') +}) + +Then('the title is visible', () => { + cy.get('[data-test="dhis2-uicore-noticebox-title"]') + .contains('The noticebox title') + .should('be.visible') +}) diff --git a/packages/widgets/src/NoticeBox/NoticeBox.js b/packages/widgets/src/NoticeBox/NoticeBox.js new file mode 100644 index 0000000000..353a3d846b --- /dev/null +++ b/packages/widgets/src/NoticeBox/NoticeBox.js @@ -0,0 +1,87 @@ +import React from 'react' +import cx from 'classnames' +import { spacers, colors } from '@dhis2/ui-constants' +import propTypes from '@dhis2/prop-types' +import { NoticeBoxTitle } from './NoticeBoxTitle.js' +import { NoticeBoxIcon } from './NoticeBoxIcon.js' +import { NoticeBoxMessage } from './NoticeBoxMessage.js' + +/** + * @module + * + * @param {NoticeBox.PropTypes} props + * @returns {React.Component} + * + * @example import { NoticeBox } from '@dhis2/ui-core' + * + * @see Live demo: {@link /demo/?path=/story/component-widget-noticebox--default|Storybook} + */ +export const NoticeBox = ({ + className, + children, + dataTest, + title, + warning, + error, +}) => { + const classnames = cx(className, 'root', { warning, error }) + + return ( +
+ +
+ + + {children} + +
+ + +
+ ) +} + +NoticeBox.defaultProps = { + dataTest: 'dhis2-uicore-noticebox', +} + +/** + * @typedef {Object} PropTypes + * @static + * @prop {Node} [children] + * @prop {className} [string] + * @prop {title} [string] + * @prop {string} [dataTest] + * @prop {boolean} [warning] - `warning` and `error` are mutually exclusive boolean props + * @prop {boolean} [error] + */ +NoticeBox.propTypes = { + children: propTypes.node, + className: propTypes.string, + dataTest: propTypes.string, + error: propTypes.mutuallyExclusive(['error', 'warning'], propTypes.bool), + title: propTypes.string, + warning: propTypes.mutuallyExclusive(['error', 'warning'], propTypes.bool), +} diff --git a/packages/widgets/src/NoticeBox/NoticeBox.stories.e2e.js b/packages/widgets/src/NoticeBox/NoticeBox.stories.e2e.js new file mode 100644 index 0000000000..bd834f480e --- /dev/null +++ b/packages/widgets/src/NoticeBox/NoticeBox.stories.e2e.js @@ -0,0 +1,7 @@ +import React from 'react' +import { storiesOf } from '@storybook/react' +import { NoticeBox } from './NoticeBox.js' + +storiesOf('NoticeBox', module) + .add('With children', () => The noticebox content) + .add('With title', () => ) diff --git a/packages/widgets/src/NoticeBox/NoticeBox.stories.js b/packages/widgets/src/NoticeBox/NoticeBox.stories.js new file mode 100644 index 0000000000..76650f7281 --- /dev/null +++ b/packages/widgets/src/NoticeBox/NoticeBox.stories.js @@ -0,0 +1,40 @@ +import React from 'react' +import { NoticeBox } from './NoticeBox.js' + +export default { + title: 'Component/Widget/NoticeBox', + component: NoticeBox, +} + +export const Default = () => ( + + Data shown in this dashboard may take a few hours to update. Scheduled + dashboard updates can be managed in the scheduler app. + +) + +export const Warning = () => ( + + No one will be able to access this program. Add some Organisation Units + to the access list. + +) + +export const Error = () => ( + + Data could be accessed from outside this instance. Update access rules + immediately. + +) + +const text = + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.' + + 'Ut semper interdum scelerisque. Suspendisse ut velit sed' + + 'lacus pretium convallis vitae sit amet purus. Nam ut' + + 'libero rhoncus, consectetur sem a, sollicitudin lectus.' + +export const WithALongTitle = () => ( + + The title text will wrap + +) diff --git a/packages/widgets/src/NoticeBox/NoticeBoxIcon.js b/packages/widgets/src/NoticeBox/NoticeBoxIcon.js new file mode 100644 index 0000000000..05f6a7221e --- /dev/null +++ b/packages/widgets/src/NoticeBox/NoticeBoxIcon.js @@ -0,0 +1,46 @@ +import React from 'react' +import propTypes from '@dhis2/prop-types' +import { colors, spacers } from '@dhis2/ui-constants' +import { Info, Warning, Error as ErrorIcon } from '@dhis2/ui-icons' +import css from 'styled-jsx/css' + +const getIconStyles = color => + css.resolve` + svg { + fill: ${color}; + width: 24px; + height: 24px; + margin-right: ${spacers.dp12}; + } + ` + +export const NoticeBoxIcon = ({ warning, error, dataTest }) => { + // Info is the default icon + let color = colors.blue900 + let Icon = Info + + if (warning) { + color = colors.yellow700 + Icon = Warning + } + + if (error) { + color = colors.red700 + Icon = ErrorIcon + } + + const { className, styles } = getIconStyles(color) + + return ( +
+ + {styles} +
+ ) +} + +NoticeBoxIcon.propTypes = { + dataTest: propTypes.string.isRequired, + error: propTypes.mutuallyExclusive(['error', 'warning'], propTypes.bool), + warning: propTypes.mutuallyExclusive(['error', 'warning'], propTypes.bool), +} diff --git a/packages/widgets/src/NoticeBox/NoticeBoxMessage.js b/packages/widgets/src/NoticeBox/NoticeBoxMessage.js new file mode 100644 index 0000000000..c058396211 --- /dev/null +++ b/packages/widgets/src/NoticeBox/NoticeBoxMessage.js @@ -0,0 +1,28 @@ +import React from 'react' +import { colors } from '@dhis2/ui-constants' +import propTypes from '@dhis2/prop-types' + +export const NoticeBoxMessage = ({ children, dataTest }) => { + if (!children) { + return null + } + + return ( +
+ {children} + + +
+ ) +} + +NoticeBoxMessage.propTypes = { + dataTest: propTypes.string.isRequired, + children: propTypes.node, +} diff --git a/packages/widgets/src/NoticeBox/NoticeBoxTitle.js b/packages/widgets/src/NoticeBox/NoticeBoxTitle.js new file mode 100644 index 0000000000..c151540794 --- /dev/null +++ b/packages/widgets/src/NoticeBox/NoticeBoxTitle.js @@ -0,0 +1,29 @@ +import React from 'react' +import { colors, spacers } from '@dhis2/ui-constants' +import propTypes from '@dhis2/prop-types' + +export const NoticeBoxTitle = ({ title, dataTest }) => { + if (!title) { + return null + } + + return ( +
+ {title} + +
+ ) +} + +NoticeBoxTitle.propTypes = { + dataTest: propTypes.string.isRequired, + title: propTypes.string, +} diff --git a/packages/widgets/src/NoticeBox/__test__/NoticeBoxIcon.test.js b/packages/widgets/src/NoticeBox/__test__/NoticeBoxIcon.test.js new file mode 100644 index 0000000000..f5d0480630 --- /dev/null +++ b/packages/widgets/src/NoticeBox/__test__/NoticeBoxIcon.test.js @@ -0,0 +1,54 @@ +import React from 'react' +import { shallow } from 'enzyme' +import { NoticeBoxIcon } from '../NoticeBoxIcon.js' + +describe('NoticeBoxIcon', () => { + it('should render info icon by default', () => { + const wrapper = shallow() + + expect(wrapper.find('Warning')).toHaveLength(0) + expect(wrapper.find('Error')).toHaveLength(0) + expect(wrapper.find('Info')).toHaveLength(1) + }) + + it('should log errors when both warning and error flag are set', () => { + const spy = jest + .spyOn(global.console, 'error') + .mockImplementation(() => {}) + shallow() + + expect(spy.mock.calls[0][0]).toMatchSnapshot() + expect(spy.mock.calls[1][0]).toMatchSnapshot() + + spy.mockRestore() + }) + + it('should render error icon when both warning and error flag are set', () => { + const spy = jest + .spyOn(global.console, 'error') + .mockImplementation(() => {}) + const wrapper = shallow() + + expect(wrapper.find('Warning')).toHaveLength(0) + expect(wrapper.find('Info')).toHaveLength(0) + expect(wrapper.find('Error')).toHaveLength(1) + + spy.mockRestore() + }) + + it('should render error icon when only error flag is set', () => { + const wrapper = shallow() + + expect(wrapper.find('Warning')).toHaveLength(0) + expect(wrapper.find('Info')).toHaveLength(0) + expect(wrapper.find('Error')).toHaveLength(1) + }) + + it('should render warning icon when only warning flag is set', () => { + const wrapper = shallow() + + expect(wrapper.find('Info')).toHaveLength(0) + expect(wrapper.find('Error')).toHaveLength(0) + expect(wrapper.find('Warning')).toHaveLength(1) + }) +}) diff --git a/packages/widgets/src/NoticeBox/__test__/NoticeBoxMessage.test.js b/packages/widgets/src/NoticeBox/__test__/NoticeBoxMessage.test.js new file mode 100644 index 0000000000..c17a22afa7 --- /dev/null +++ b/packages/widgets/src/NoticeBox/__test__/NoticeBoxMessage.test.js @@ -0,0 +1,21 @@ +import React from 'react' +import { shallow } from 'enzyme' +import { NoticeBoxMessage } from '../NoticeBoxMessage.js' + +describe('NoticeBoxMessage', () => { + it('should return null when there are no children', () => { + const props = { + dataTest: 'test', + } + + expect(NoticeBoxMessage(props)).toBe(null) + }) + + it('should render children', () => { + const wrapper = shallow( + children + ) + + expect(wrapper.text()).toEqual(expect.stringContaining('children')) + }) +}) diff --git a/packages/widgets/src/NoticeBox/__test__/NoticeBoxTitle.test.js b/packages/widgets/src/NoticeBox/__test__/NoticeBoxTitle.test.js new file mode 100644 index 0000000000..50eb702456 --- /dev/null +++ b/packages/widgets/src/NoticeBox/__test__/NoticeBoxTitle.test.js @@ -0,0 +1,21 @@ +import React from 'react' +import { shallow } from 'enzyme' +import { NoticeBoxTitle } from '../NoticeBoxTitle.js' + +describe('NoticeBoxTitle', () => { + it('should return null when there is no title', () => { + const props = { + dataTest: 'test', + } + + expect(NoticeBoxTitle(props)).toBe(null) + }) + + it('should render title', () => { + const wrapper = shallow( + + ) + + expect(wrapper.text()).toEqual(expect.stringContaining('title')) + }) +}) diff --git a/packages/widgets/src/NoticeBox/__test__/__snapshots__/NoticeBoxIcon.test.js.snap b/packages/widgets/src/NoticeBox/__test__/__snapshots__/NoticeBoxIcon.test.js.snap new file mode 100644 index 0000000000..1a62b61d3f --- /dev/null +++ b/packages/widgets/src/NoticeBox/__test__/__snapshots__/NoticeBoxIcon.test.js.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`NoticeBoxIcon should log errors when both warning and error flag are set 1`] = ` +"Warning: Failed prop type: Invalid prop \`error\` supplied to \`NoticeBoxIcon\`, Property 'error' is mutually exclusive with 'warning', but both have a thruthy value. + in NoticeBoxIcon" +`; + +exports[`NoticeBoxIcon should log errors when both warning and error flag are set 2`] = ` +"Warning: Failed prop type: Invalid prop \`warning\` supplied to \`NoticeBoxIcon\`, Property 'warning' is mutually exclusive with 'error', but both have a thruthy value. + in NoticeBoxIcon" +`;