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"
+`;