@dhis2-ui/alert 10.16.1 → 10.16.3-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dhis2-ui/alert",
3
- "version": "10.16.1",
3
+ "version": "10.16.3-alpha.1",
4
4
  "description": "UI Alert",
5
5
  "repository": {
6
6
  "type": "git",
@@ -33,15 +33,16 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "@dhis2/prop-types": "^3.1.2",
36
- "@dhis2-ui/portal": "10.16.1",
37
- "@dhis2/ui-constants": "10.16.1",
38
- "@dhis2/ui-icons": "10.16.1",
36
+ "@dhis2-ui/portal": "10.16.3-alpha.1",
37
+ "@dhis2/ui-constants": "10.16.3-alpha.1",
38
+ "@dhis2/ui-icons": "10.16.3-alpha.1",
39
39
  "classnames": "^2.3.1",
40
40
  "prop-types": "^15.7.2"
41
41
  },
42
42
  "files": [
43
43
  "build",
44
- "types"
44
+ "types",
45
+ "src"
45
46
  ],
46
47
  "devDependencies": {
47
48
  "react": "^18.3.1",
@@ -0,0 +1,37 @@
1
+ import { spacers } from '@dhis2/ui-constants'
2
+ import PropTypes from 'prop-types'
3
+ import React, { Component } from 'react'
4
+
5
+ class Action extends Component {
6
+ onClick = (event) => {
7
+ this.props.onClick(event)
8
+ this.props.hide(event)
9
+ }
10
+
11
+ render() {
12
+ return (
13
+ <span onClick={this.onClick} data-test={this.props.dataTest}>
14
+ {this.props.label}
15
+ <style jsx>{`
16
+ span {
17
+ margin-inline-end: ${spacers.dp12};
18
+ text-decoration: underline;
19
+ white-space: nowrap;
20
+ }
21
+ span:hover {
22
+ cursor: pointer;
23
+ }
24
+ `}</style>
25
+ </span>
26
+ )
27
+ }
28
+ }
29
+
30
+ Action.propTypes = {
31
+ dataTest: PropTypes.string.isRequired,
32
+ hide: PropTypes.func.isRequired,
33
+ label: PropTypes.string.isRequired,
34
+ onClick: PropTypes.func.isRequired,
35
+ }
36
+
37
+ export { Action }
@@ -0,0 +1,47 @@
1
+ import { arrayWithLength } from '@dhis2/prop-types'
2
+ import { spacers } from '@dhis2/ui-constants'
3
+ import PropTypes from 'prop-types'
4
+ import React from 'react'
5
+ import { Action } from './action.js'
6
+
7
+ const Actions = ({ actions, hide, dataTest }) => {
8
+ if (!actions) {
9
+ return null
10
+ }
11
+
12
+ return (
13
+ <div>
14
+ {actions.map((action) => (
15
+ <Action
16
+ key={action.label}
17
+ {...action}
18
+ hide={hide}
19
+ dataTest={`${dataTest}-action`}
20
+ />
21
+ ))}
22
+ <style jsx>{`
23
+ div {
24
+ margin-inline-start: ${spacers.dp48};
25
+ margin-inline-end: -${spacers.dp12};
26
+ }
27
+ `}</style>
28
+ </div>
29
+ )
30
+ }
31
+
32
+ const actionsPropType = arrayWithLength(
33
+ 0,
34
+ 2,
35
+ PropTypes.shape({
36
+ label: PropTypes.string.isRequired,
37
+ onClick: PropTypes.func.isRequired,
38
+ })
39
+ )
40
+
41
+ Actions.propTypes = {
42
+ dataTest: PropTypes.string.isRequired,
43
+ hide: PropTypes.func.isRequired,
44
+ actions: actionsPropType,
45
+ }
46
+
47
+ export { Actions, actionsPropType }
@@ -0,0 +1,53 @@
1
+ import React, { useState } from 'react'
2
+ import { AlertBar } from './index.js'
3
+
4
+ window.onHidden = window.Cypress && window.Cypress.cy.stub()
5
+
6
+ export default { title: 'AlertBar' }
7
+
8
+ export const Default = () => <AlertBar>Default</AlertBar>
9
+ export const CustomDuration = () => (
10
+ <AlertBar duration={2000}>Custom duration</AlertBar>
11
+ )
12
+ export const PermanentWithActions = () => (
13
+ <AlertBar
14
+ permanent
15
+ actions={[
16
+ { label: 'Save', onClick: () => {} },
17
+ { label: 'Cancel', onClick: () => {} },
18
+ ]}
19
+ >
20
+ With Actions
21
+ </AlertBar>
22
+ )
23
+ export const DisabledIcon = () => <AlertBar icon={false}>Message</AlertBar>
24
+ export const CustomIcon = () => (
25
+ <AlertBar icon={<span>Custom icon</span>}>Message</AlertBar>
26
+ )
27
+ export const WithMessage = () => <AlertBar>With a message</AlertBar>
28
+ export const WithOnHidden = () => (
29
+ <AlertBar onHidden={window.onHidden}>Message</AlertBar>
30
+ )
31
+ export const Permanent = () => <AlertBar permanent>Message</AlertBar>
32
+ export const HiddenProp = () => {
33
+ const [hidden, setHidden] = useState(true)
34
+ const toggleVisibility = () => setHidden((prevHidden) => !prevHidden)
35
+ return (
36
+ <React.Fragment>
37
+ <button
38
+ style={{
39
+ display: 'block',
40
+ position: 'fixed',
41
+ bottom: 150,
42
+ left: 10,
43
+ }}
44
+ onClick={toggleVisibility}
45
+ >
46
+ {hidden ? 'Show' : 'Hide'}
47
+ </button>
48
+ <AlertBar permanent hidden={hidden} onHidden={window.onHidden}>
49
+ Short text
50
+ </AlertBar>
51
+ </React.Fragment>
52
+ )
53
+ }
@@ -0,0 +1,180 @@
1
+ import { mutuallyExclusive } from '@dhis2/prop-types'
2
+ import cx from 'classnames'
3
+ import PropTypes from 'prop-types'
4
+ import React, { useRef, useState, useEffect } from 'react'
5
+ import { Actions, actionsPropType } from './actions.js'
6
+ import styles, { ANIMATION_TIME } from './alert-bar.styles.js'
7
+ import { Dismiss } from './dismiss.js'
8
+ import { Icon, iconPropType } from './icon.js'
9
+ import { Message } from './message.js'
10
+
11
+ const AlertBar = ({
12
+ actions,
13
+ children,
14
+ className,
15
+ critical,
16
+ dataTest = 'dhis2-uicore-alertbar',
17
+ duration = 8000,
18
+ hidden,
19
+ icon = true,
20
+ permanent,
21
+ success,
22
+ warning,
23
+ onHidden,
24
+ }) => {
25
+ const [inViewport, setInViewport] = useState(!hidden)
26
+ const [inDOM, setInDOM] = useState(!hidden)
27
+ const alertRef = useRef(null)
28
+ const showTimeout = useRef(null)
29
+ const displayTimeout = useRef(null)
30
+ const hideTimeout = useRef(null)
31
+ const displayStartTime = useRef(null)
32
+ const displayTimeRemaining = useRef(null)
33
+ const info = !critical && !success && !warning
34
+ const shouldAutoHide = !(permanent || warning || critical)
35
+ const show = () => {
36
+ setInDOM(true)
37
+ setInViewport(true)
38
+ }
39
+ const hide = () => {
40
+ setInDOM(true)
41
+ setInViewport(false)
42
+ }
43
+ const remove = () => {
44
+ setInDOM(false)
45
+ setInViewport(false)
46
+ onHidden && onHidden({}, null)
47
+ }
48
+ const clearAllTimeouts = () => {
49
+ clearTimeout(showTimeout.current)
50
+ clearTimeout(displayTimeout.current)
51
+ clearTimeout(hideTimeout.current)
52
+ }
53
+ const runHideAnimation = () => {
54
+ clearAllTimeouts()
55
+ if (alertRef.current) {
56
+ alertRef.current.style.setProperty(
57
+ '--alert-bar-collapse',
58
+ `-${alertRef.current.offsetHeight}px`
59
+ )
60
+ }
61
+ hide()
62
+ hideTimeout.current = setTimeout(remove, ANIMATION_TIME)
63
+ }
64
+ const startDisplayTimeout = () => {
65
+ if (shouldAutoHide) {
66
+ clearAllTimeouts()
67
+ displayStartTime.current = Date.now()
68
+ displayTimeRemaining.current = duration
69
+ displayTimeout.current = setTimeout(
70
+ runHideAnimation,
71
+ displayTimeRemaining.current
72
+ )
73
+ }
74
+ }
75
+ const runShowAnimation = () => {
76
+ clearAllTimeouts()
77
+ show()
78
+ showTimeout.current = setTimeout(startDisplayTimeout, ANIMATION_TIME)
79
+ }
80
+ const pauseDisplayTimeout = () => {
81
+ if (shouldAutoHide) {
82
+ clearAllTimeouts()
83
+ const elapsedTime = Date.now() - displayStartTime.current
84
+ displayTimeRemaining.current -= elapsedTime
85
+ }
86
+ }
87
+ const resumeDisplayTimeout = () => {
88
+ if (shouldAutoHide) {
89
+ clearAllTimeouts()
90
+ displayTimeout.current = setTimeout(
91
+ runHideAnimation,
92
+ displayTimeRemaining.current
93
+ )
94
+ }
95
+ }
96
+
97
+ useEffect(() => {
98
+ // Additional check on inDOM prevents the AlertBar from briefly showing
99
+ // when it is mounted with a hidden prop set to true
100
+ if (hidden && inDOM) {
101
+ runHideAnimation()
102
+ }
103
+ if (!hidden) {
104
+ runShowAnimation()
105
+ }
106
+
107
+ return clearAllTimeouts
108
+ }, [hidden])
109
+
110
+ return !inDOM ? null : (
111
+ <div
112
+ ref={alertRef}
113
+ className={cx(className, {
114
+ info,
115
+ success,
116
+ warning,
117
+ critical,
118
+ inViewport,
119
+ })}
120
+ data-test={dataTest}
121
+ onMouseEnter={pauseDisplayTimeout}
122
+ onMouseLeave={resumeDisplayTimeout}
123
+ >
124
+ <Icon
125
+ dataTest={`${dataTest}-icon`}
126
+ icon={icon}
127
+ critical={critical}
128
+ success={success}
129
+ warning={warning}
130
+ info={info}
131
+ />
132
+ <Message>{children}</Message>
133
+ <Actions
134
+ actions={actions}
135
+ hide={runHideAnimation}
136
+ dataTest={dataTest}
137
+ />
138
+ <Dismiss
139
+ onClick={runHideAnimation}
140
+ dataTest={`${dataTest}-dismiss`}
141
+ />
142
+
143
+ <style jsx>{styles}</style>
144
+ </div>
145
+ )
146
+ }
147
+
148
+ const alertTypePropType = mutuallyExclusive(
149
+ ['success', 'warning', 'critical'],
150
+ PropTypes.bool
151
+ )
152
+
153
+ AlertBar.propTypes = {
154
+ /** An array of 0-2 action objects
155
+ `[{label: "Save", onClick: clickHandler}]`*/
156
+ actions: actionsPropType,
157
+ /** The message string for the alert */
158
+ children: PropTypes.string,
159
+ className: PropTypes.string,
160
+ /** Alert bars with `critical` will not autohide */
161
+ critical: alertTypePropType,
162
+ dataTest: PropTypes.string,
163
+ /** How long you want the notification to display, in `ms`, when it's not permanent */
164
+ duration: PropTypes.number,
165
+ /** AlertBar will be hidden on creation when this is set to true */
166
+ hidden: PropTypes.bool,
167
+ /**
168
+ * A specific icon to override the default icon in the bar.
169
+ * If `false` is provided, no icon will be shown.
170
+ */
171
+ icon: iconPropType,
172
+ /** When set, AlertBar will not autohide */
173
+ permanent: PropTypes.bool,
174
+ success: alertTypePropType,
175
+ /** Alert bars with `warning` will not autohide */
176
+ warning: alertTypePropType,
177
+ onHidden: PropTypes.func,
178
+ }
179
+
180
+ export { AlertBar }
@@ -0,0 +1,258 @@
1
+ import { IconFile16 } from '@dhis2/ui-icons'
2
+ import React, { useState } from 'react'
3
+ import { AlertBar } from './index.js'
4
+
5
+ const subtitle = `
6
+ A floating alert that informs the user about temporary information
7
+ in the context of the current screen.
8
+ `
9
+
10
+ const description = `
11
+ Alert bars notify a user of some information. There are different types of
12
+ alert bar for displaying different types of content. Use the alert bar type
13
+ that matches your content type and importance. Note that alert bar can be
14
+ ignored by the user, so they shouldn't be used for content that needs to
15
+ block an app flow, use a modal instead.
16
+
17
+ Alert bars are always displayed at centered and fixed at the bottom of the
18
+ screen. Some types of alert bar dismiss after a set time, others must be
19
+ dismissed by the user.
20
+
21
+ See specification: [Design System](https://github.com/dhis2/design-system/blob/master/molecules/alertbar.md)
22
+
23
+ \`\`\`js
24
+ import { AlertBar } from '@dhis2/ui'
25
+ \`\`\`
26
+ `
27
+
28
+ const Wrapper = (fn) => (
29
+ <div style={{ height: '260px' }}>
30
+ <div
31
+ className="alert-bars"
32
+ style={{
33
+ width: '100%',
34
+ position: 'fixed',
35
+ bottom: 0,
36
+ left: 0,
37
+ paddingLeft: 16,
38
+ }}
39
+ >
40
+ {fn()}
41
+ </div>
42
+ </div>
43
+ )
44
+
45
+ const alertTypeArgType = {
46
+ table: {
47
+ type: {
48
+ summary: 'bool',
49
+ detail: "'success', 'warning', and 'critical' are mutually exclusive props",
50
+ },
51
+ defaultValue: {
52
+ summary: false,
53
+ },
54
+ },
55
+ control: {
56
+ type: 'boolean',
57
+ },
58
+ }
59
+ const iconArgType = {
60
+ table: {
61
+ type: {
62
+ summary: 'bool | element',
63
+ },
64
+ },
65
+ }
66
+
67
+ const permanentArgType = {
68
+ table: {
69
+ type: {
70
+ summary: 'bool',
71
+ },
72
+ defaultValue: {
73
+ summary: false,
74
+ },
75
+ },
76
+ control: {
77
+ type: 'boolean',
78
+ },
79
+ }
80
+
81
+ const childrenArgType = {
82
+ table: {
83
+ type: {
84
+ summary: 'String to display in the alert bar',
85
+ },
86
+ },
87
+ }
88
+
89
+ const actionsArgType = {
90
+ table: {
91
+ type: {
92
+ summary: '[{ label: string, onClick: func }]',
93
+ },
94
+ },
95
+ }
96
+
97
+ export default {
98
+ title: 'Alert Bar',
99
+ component: AlertBar,
100
+ decorators: [Wrapper],
101
+ args: {
102
+ children: 'I will autohide',
103
+ },
104
+ parameters: {
105
+ componentSubtitle: subtitle,
106
+ docs: {
107
+ description: {
108
+ component: description,
109
+ },
110
+ },
111
+ },
112
+ argTypes: {
113
+ actions: { ...actionsArgType },
114
+ critical: { ...alertTypeArgType },
115
+ success: { ...alertTypeArgType },
116
+ warning: { ...alertTypeArgType },
117
+ permanent: { ...permanentArgType },
118
+ children: { ...childrenArgType },
119
+ icon: { ...iconArgType },
120
+ },
121
+ }
122
+
123
+ export const Default = (args) => <AlertBar {...args}></AlertBar>
124
+
125
+ export const States = () => (
126
+ <React.Fragment>
127
+ <AlertBar permanent>Default (info)</AlertBar>
128
+ <AlertBar permanent success>
129
+ Success
130
+ </AlertBar>
131
+ <AlertBar permanent warning>
132
+ Warning
133
+ </AlertBar>
134
+ <AlertBar permanent critical>
135
+ Critical
136
+ </AlertBar>
137
+ </React.Fragment>
138
+ )
139
+
140
+ export const RTL = () => (
141
+ <React.Fragment>
142
+ <div dir="rtl">
143
+ <AlertBar permanent>Default (info)</AlertBar>
144
+ <AlertBar permanent success>
145
+ Success
146
+ </AlertBar>
147
+ <AlertBar permanent warning>
148
+ Warning
149
+ </AlertBar>
150
+ <AlertBar permanent critical>
151
+ Critical
152
+ </AlertBar>
153
+ </div>
154
+ </React.Fragment>
155
+ )
156
+
157
+ export const AutoHiding = () => (
158
+ <React.Fragment>
159
+ <AlertBar permanent>Permanent never auto-hides</AlertBar>
160
+ <AlertBar warning>Warning never auto-hides</AlertBar>
161
+ <AlertBar critical>Critial never auto-hides</AlertBar>
162
+ <AlertBar
163
+ duration={2000}
164
+ onHidden={(payload, event) => {
165
+ console.log('onHidden payload', payload)
166
+ console.log('onHidden event', event)
167
+ }}
168
+ >
169
+ Custom duration, hides after 2s
170
+ </AlertBar>
171
+ <AlertBar
172
+ onHidden={(payload, event) => {
173
+ console.log('onHidden payload', payload)
174
+ console.log('onHidden event', event)
175
+ }}
176
+ >
177
+ Default auto-hides after 8s
178
+ </AlertBar>
179
+ </React.Fragment>
180
+ )
181
+ AutoHiding.storyName = 'Auto hiding'
182
+
183
+ export const WithActions = () => (
184
+ <AlertBar
185
+ permanent
186
+ actions={[
187
+ { label: 'Save', onClick: () => {} },
188
+ { label: 'Cancel', onClick: () => {} },
189
+ ]}
190
+ >
191
+ With Actions
192
+ </AlertBar>
193
+ )
194
+ WithActions.storyName = 'With actions'
195
+
196
+ export const WithActionsAndInsufficientSpace = () => (
197
+ <AlertBar
198
+ permanent
199
+ actions={[
200
+ { label: 'Long action 1', onClick: () => {} },
201
+ { label: 'Long action 2', onClick: () => {} },
202
+ ]}
203
+ >
204
+ Some text, a pretty normal amount, that conflicts with pretty long
205
+ actions
206
+ </AlertBar>
207
+ )
208
+ WithActionsAndInsufficientSpace.storyName =
209
+ 'With actions and insufficient space'
210
+
211
+ export const Icons = () => (
212
+ <React.Fragment>
213
+ <AlertBar permanent>Default icon</AlertBar>
214
+ <AlertBar permanent icon={false}>
215
+ No icon
216
+ </AlertBar>
217
+ <AlertBar permanent icon={<IconFile16 />}>
218
+ Custom icon
219
+ </AlertBar>
220
+ </React.Fragment>
221
+ )
222
+
223
+ export const TextOverflow = () => (
224
+ <React.Fragment>
225
+ <AlertBar permanent>Short text</AlertBar>
226
+ <AlertBar permanent>
227
+ If the alert bar gets a ver long text, it will grow to a maximum of
228
+ 600px and the text will overflow across several lines. If there are
229
+ multiple AlertBars in a stack, they will all grow to the size of the
230
+ widest sibling.
231
+ </AlertBar>
232
+ </React.Fragment>
233
+ )
234
+ TextOverflow.storyName = 'Text overflow'
235
+
236
+ export const HiddenProp = () => {
237
+ const [hidden, setHidden] = useState(true)
238
+ const toggleVisibility = () => setHidden((prevHidden) => !prevHidden)
239
+ return (
240
+ <React.Fragment>
241
+ <button
242
+ style={{
243
+ display: 'block',
244
+ position: 'fixed',
245
+ bottom: 150,
246
+ left: 10,
247
+ }}
248
+ onClick={toggleVisibility}
249
+ >
250
+ {hidden ? 'Show' : 'Hide'}
251
+ </button>
252
+ <AlertBar permanent hidden={hidden}>
253
+ Short text
254
+ </AlertBar>
255
+ </React.Fragment>
256
+ )
257
+ }
258
+ HiddenProp.storyName = 'Hidden prop'
@@ -0,0 +1,92 @@
1
+ import { colors, spacers, elevations } from '@dhis2/ui-constants'
2
+ import css from 'styled-jsx/css'
3
+
4
+ export const ANIMATION_TIME = 350
5
+
6
+ export default css`
7
+ div {
8
+ display: flex;
9
+ justify-content: space-between;
10
+ align-items: center;
11
+ border-radius: 4px;
12
+ box-shadow: ${elevations.e300};
13
+ padding-top: ${spacers.dp8};
14
+ padding-inline-end: ${spacers.dp8};
15
+ padding-bottom: ${spacers.dp8};
16
+ padding-inline-start: ${spacers.dp16};
17
+ margin-bottom: ${spacers.dp16};
18
+ max-width: 600px;
19
+ min-width: 300px;
20
+ font-size: 14px;
21
+ pointer-events: all;
22
+ }
23
+
24
+ /* States */
25
+
26
+ div.info {
27
+ background-color: ${colors.grey900};
28
+ color: ${colors.white};
29
+ }
30
+ div.success {
31
+ background-color: ${colors.green800};
32
+ color: ${colors.white};
33
+ }
34
+ div.warning {
35
+ background-color: ${colors.yellow300};
36
+ color: ${colors.yellow900};
37
+ }
38
+ div.critical {
39
+ background-color: ${colors.red800};
40
+ color: ${colors.white};
41
+ }
42
+
43
+ /* Animation */
44
+
45
+ @keyframes slidein {
46
+ from {
47
+ transform: translateY(${spacers.dp24});
48
+ opacity: 0;
49
+ }
50
+ to {
51
+ transform: translateY(0);
52
+ opacity: 1;
53
+ }
54
+ }
55
+
56
+ @keyframes slideout {
57
+ 0% {
58
+ transform: translateY(0);
59
+ opacity: 1;
60
+ margin-bottom: ${spacers.dp16};
61
+ }
62
+ 45% {
63
+ opacity: 0;
64
+ }
65
+ 100% {
66
+ transform: translateY(${spacers.dp8});
67
+ opacity: 0;
68
+ margin-bottom: var(--alert-bar-collapse, ${spacers.dp16});
69
+ }
70
+ }
71
+
72
+ div.inViewport {
73
+ animation-duration: ${ANIMATION_TIME}ms;
74
+ animation-name: slidein;
75
+ animation-fill-mode: forwards;
76
+ animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
77
+ }
78
+
79
+ div {
80
+ animation-duration: ${ANIMATION_TIME}ms;
81
+ animation-name: slideout;
82
+ animation-fill-mode: forwards;
83
+ animation-timing-function: cubic-bezier(0.4, 0, 1, 1);
84
+ }
85
+
86
+ @media (prefers-reduced-motion: reduce) {
87
+ div,
88
+ div.inViewport {
89
+ animation-duration: 0ms;
90
+ }
91
+ }
92
+ `
@@ -0,0 +1,39 @@
1
+ import { spacers } from '@dhis2/ui-constants'
2
+ import { IconCross24 } from '@dhis2/ui-icons'
3
+ import PropTypes from 'prop-types'
4
+ import React from 'react'
5
+
6
+ const Dismiss = ({ onClick, dataTest }) => (
7
+ <div onClick={onClick} data-test={dataTest}>
8
+ <IconCross24 />
9
+ <style jsx>{`
10
+ div {
11
+ margin-inline-start: ${spacers.dp16};
12
+ min-height: 32px;
13
+ min-width: 32px;
14
+ display: flex;
15
+ align-items: center;
16
+ justify-content: center;
17
+ border-radius: 5px;
18
+ }
19
+ div:hover {
20
+ cursor: pointer;
21
+ background: rgba(0, 0, 0, 0.15);
22
+ }
23
+ div:active {
24
+ background: rgba(0, 0, 0, 0.25);
25
+ }
26
+ div :global(svg) {
27
+ width: 18px;
28
+ height: 18px;
29
+ }
30
+ `}</style>
31
+ </div>
32
+ )
33
+
34
+ Dismiss.propTypes = {
35
+ dataTest: PropTypes.string.isRequired,
36
+ onClick: PropTypes.func.isRequired,
37
+ }
38
+
39
+ export { Dismiss }
@@ -0,0 +1,51 @@
1
+ import { Given, When, Then } from '@badeball/cypress-cucumber-preprocessor'
2
+
3
+ Given('an AlertBar with onHidden handler is rendered', () => {
4
+ cy.visitStory('AlertBar', 'With on hidden')
5
+ })
6
+
7
+ Given('an AlertBar with disabled icon is rendered', () => {
8
+ cy.visitStory('AlertBar', 'Disabled icon')
9
+ })
10
+
11
+ Given('an AlertBar with custom icon is rendered', () => {
12
+ cy.visitStory('AlertBar', 'Custom icon')
13
+ })
14
+
15
+ Given('an AlertBar with a message is rendered', () => {
16
+ cy.visitStory('AlertBar', 'With message')
17
+ })
18
+
19
+ Given('an AlertBar with permanent is rendered', () => {
20
+ cy.visitStory('AlertBar', 'Permanent')
21
+ cy.get('[data-test="dhis2-uicore-alertbar"]').should('be.visible')
22
+ })
23
+
24
+ When('the Alertbar is not rendered', () => {
25
+ cy.wait(8000)
26
+ cy.get('[data-test="dhis2-uicore-alertbar"]').should('not.exist')
27
+ })
28
+
29
+ Then('the icon will be visible', () => {
30
+ cy.get('[data-test="dhis2-uicore-alertbar-icon"]').should('be.visible')
31
+ })
32
+
33
+ Then('the icon will not be rendered', () => {
34
+ cy.get('[data-test="dhis2-uicore-alertbar-icon"]').should('not.exist')
35
+ })
36
+
37
+ Then('the custom icon will be visible', () => {
38
+ cy.get('[data-test="dhis2-uicore-alertbar-icon"]')
39
+ .contains('Custom icon')
40
+ .should('be.visible')
41
+ })
42
+
43
+ Then('the message will be visible', () => {
44
+ cy.get('[data-test="dhis2-uicore-alertbar"]')
45
+ .contains('With a message')
46
+ .should('be.visible')
47
+ })
48
+
49
+ Then('the AlertBar will be visible', () => {
50
+ cy.get('[data-test="dhis2-uicore-alertbar"]').should('be.visible')
51
+ })
@@ -0,0 +1,29 @@
1
+ Feature: AlertBar API
2
+
3
+ Scenario: AlertBar icon shows by default
4
+ Given a default AlertBar is rendered
5
+ Then the icon will be visible
6
+
7
+ Scenario: AlertBar icon can be hidden
8
+ Given an AlertBar with disabled icon is rendered
9
+ Then the icon will not be rendered
10
+
11
+ Scenario: Custom AlertBar icon
12
+ Given an AlertBar with custom icon is rendered
13
+ Then the custom icon will be visible
14
+
15
+ Scenario: Standard AlertBar with a message
16
+ Given an AlertBar with a message is rendered
17
+ Then the message will be visible
18
+
19
+ Scenario: The AlertBar renders with a permanent flag
20
+ Given an AlertBar with permanent is rendered
21
+ When the default duration has passed
22
+ Then the AlertBar will be visible
23
+
24
+ Scenario: AlertBar will call the onHidden cb when hidden
25
+ Given an AlertBar with onHidden handler is rendered
26
+ When the default duration has passed
27
+ Then the AlertBar is not rendered
28
+ Then the onHidden handler is called
29
+
@@ -0,0 +1,26 @@
1
+ import { When, Given, Then } from '@badeball/cypress-cucumber-preprocessor'
2
+
3
+ Given('a default AlertBar is rendered', () => {
4
+ cy.visitStory('AlertBar', 'Default')
5
+ cy.get('[data-test="dhis2-uicore-alertbar"]').should('be.visible')
6
+ })
7
+
8
+ Given('the AlertBar is rendered', () => {
9
+ cy.get('[data-test="dhis2-uicore-alertbar"]').should('exist')
10
+ cy.get('[data-test="dhis2-uicore-alertbar"]').should('be.visible')
11
+ })
12
+
13
+ Then('the AlertBar is not rendered', () => {
14
+ cy.get('[data-test="dhis2-uicore-alertbar"]').should('not.exist')
15
+ })
16
+
17
+ When('the default duration has passed', () => {
18
+ cy.wait(8000)
19
+ })
20
+
21
+ Then('the onHidden handler is called', () => {
22
+ cy.window().should((win) => {
23
+ expect(win.onHidden).to.be.calledOnce
24
+ expect(win.onHidden).to.be.calledWith({}, null)
25
+ })
26
+ })
@@ -0,0 +1,39 @@
1
+ import { Given, When, Then } from '@badeball/cypress-cucumber-preprocessor'
2
+
3
+ Given('a permanent AlertBar with hidden and onHidden is rendered', () => {
4
+ cy.visitStory('AlertBar', 'Hidden prop')
5
+ })
6
+
7
+ Given('an AlertBar with a custom duration is rendered', () => {
8
+ cy.visitStory('AlertBar', 'Custom duration')
9
+ cy.get('[data-test="dhis2-uicore-alertbar"]').should('be.visible')
10
+ })
11
+
12
+ Given('a permanent AlertBar with actions is rendered', () => {
13
+ cy.visitStory('AlertBar', 'Permanent with actions')
14
+ cy.get('[data-test="dhis2-uicore-alertbar"]').should('be.visible')
15
+ })
16
+
17
+ When('the user clicks on the "Cancel" button', () => {
18
+ cy.get(
19
+ '[data-test="dhis2-uicore-alertbar-action"]:contains("Cancel")'
20
+ ).click()
21
+ })
22
+
23
+ When('the user click a button which toggles the hidden prop to false', () => {
24
+ cy.get('button:contains("Show")').click()
25
+ })
26
+
27
+ When('the user click a button which toggles the hidden prop to true', () => {
28
+ cy.get('button:contains("Hide")').click()
29
+ })
30
+
31
+ When('the custom duration has passed', () => {
32
+ cy.wait(2000)
33
+ })
34
+
35
+ Then('the onHidden handler is not called', () => {
36
+ cy.window().should((win) => {
37
+ expect(win.onHidden).to.not.be.called
38
+ })
39
+ })
@@ -0,0 +1,26 @@
1
+ Feature: Hiding the AlertBar
2
+
3
+ Scenario: AlertBar hides automatically after the default time
4
+ Given a default AlertBar is rendered
5
+ When the default duration has passed
6
+ Then the AlertBar is not rendered
7
+
8
+ Scenario: AlertBars hides automatically after a custom time
9
+ Given an AlertBar with a custom duration is rendered
10
+ When the custom duration has passed
11
+ Then the AlertBar is not rendered
12
+
13
+ Scenario: The user clicks the "Cancel" button
14
+ Given a permanent AlertBar with actions is rendered
15
+ When the user clicks on the "Cancel" button
16
+ Then the AlertBar is not rendered
17
+
18
+ Scenario: The hidden prop is toggled
19
+ Given a permanent AlertBar with hidden and onHidden is rendered
20
+ Then the AlertBar is not rendered
21
+ And the onHidden handler is not called
22
+ When the user click a button which toggles the hidden prop to false
23
+ Then the AlertBar is rendered
24
+ When the user click a button which toggles the hidden prop to true
25
+ Then the AlertBar is not rendered
26
+ And the onHidden handler is called
@@ -0,0 +1,80 @@
1
+ import { mutuallyExclusive } from '@dhis2/prop-types'
2
+ import { spacers, colors } from '@dhis2/ui-constants'
3
+ import {
4
+ IconErrorFilled24,
5
+ IconInfoFilled24,
6
+ IconWarningFilled24,
7
+ IconCheckmark24,
8
+ } from '@dhis2/ui-icons'
9
+ import PropTypes from 'prop-types'
10
+ import React from 'react'
11
+
12
+ const StatusIcon = ({ error, warning, valid, info, defaultTo = null }) => {
13
+ if (error) {
14
+ return <IconErrorFilled24 color={colors.white} />
15
+ }
16
+ if (warning) {
17
+ return <IconWarningFilled24 color={colors.yellow900} />
18
+ }
19
+ if (valid) {
20
+ return <IconCheckmark24 color={colors.white} />
21
+ }
22
+ if (info) {
23
+ return <IconInfoFilled24 color={colors.white} />
24
+ }
25
+
26
+ return defaultTo
27
+ }
28
+
29
+ StatusIcon.propTypes = {
30
+ defaultTo: PropTypes.element,
31
+ error: PropTypes.bool,
32
+ info: PropTypes.bool,
33
+ valid: PropTypes.bool,
34
+ warning: PropTypes.bool,
35
+ }
36
+
37
+ const Icon = ({ icon, success, warning, critical, info, dataTest }) => {
38
+ if (icon === false) {
39
+ return null
40
+ }
41
+
42
+ return (
43
+ <div data-test={dataTest}>
44
+ {React.isValidElement(icon) ? (
45
+ icon
46
+ ) : (
47
+ <StatusIcon
48
+ valid={success}
49
+ error={critical}
50
+ warning={warning}
51
+ info={info}
52
+ />
53
+ )}
54
+ <style jsx>{`
55
+ div {
56
+ display: flex;
57
+ align-items: center;
58
+ margin-inline-end: ${spacers.dp12};
59
+ }
60
+ `}</style>
61
+ </div>
62
+ )
63
+ }
64
+
65
+ const iconPropType = PropTypes.oneOfType([PropTypes.bool, PropTypes.element])
66
+ const alertStatePropType = mutuallyExclusive(
67
+ ['success', 'warning', 'critical', 'info'],
68
+ PropTypes.bool
69
+ )
70
+
71
+ Icon.propTypes = {
72
+ dataTest: PropTypes.string.isRequired,
73
+ critical: alertStatePropType,
74
+ icon: iconPropType,
75
+ info: alertStatePropType,
76
+ success: alertStatePropType,
77
+ warning: alertStatePropType,
78
+ }
79
+
80
+ export { Icon, iconPropType }
@@ -0,0 +1 @@
1
+ export { AlertBar } from './alert-bar.js'
@@ -0,0 +1,19 @@
1
+ import PropTypes from 'prop-types'
2
+ import React from 'react'
3
+
4
+ const Message = ({ children }) => (
5
+ <div>
6
+ {children}
7
+ <style jsx>{`
8
+ div {
9
+ flex-grow: 1;
10
+ }
11
+ `}</style>
12
+ </div>
13
+ )
14
+
15
+ Message.propTypes = {
16
+ children: PropTypes.string.isRequired,
17
+ }
18
+
19
+ export { Message }
@@ -0,0 +1,13 @@
1
+ import React from 'react'
2
+ import { AlertBar } from '../alert-bar/index.js'
3
+ import { AlertStack } from './alert-stack.js'
4
+
5
+ export default { title: 'AlertStack' }
6
+
7
+ export const Default = () => (
8
+ <AlertStack>
9
+ <AlertBar permanent>Message</AlertBar>
10
+ <AlertBar permanent>Message</AlertBar>
11
+ <AlertBar permanent>Message</AlertBar>
12
+ </AlertStack>
13
+ )
@@ -0,0 +1,44 @@
1
+ import { layers } from '@dhis2/ui-constants'
2
+ import { Portal } from '@dhis2-ui/portal'
3
+ import cx from 'classnames'
4
+ import PropTypes from 'prop-types'
5
+ import React from 'react'
6
+
7
+ export const AlertStack = ({
8
+ className,
9
+ children,
10
+ dataTest = 'dhis2-uicore-alertstack',
11
+ }) => (
12
+ <Portal>
13
+ <div className={cx(className)} data-test={dataTest}>
14
+ {children}
15
+ <style jsx>{`
16
+ div {
17
+ position: fixed;
18
+ top: auto;
19
+ inset-inline-end: auto;
20
+ bottom: 0;
21
+ inset-inline-start: 50%;
22
+ transform: translateX(-50%);
23
+
24
+ z-index: ${layers.alert};
25
+
26
+ display: flex;
27
+ flex-direction: column-reverse;
28
+
29
+ pointer-events: none;
30
+ }
31
+
32
+ div:dir(rtl) {
33
+ transform: translateX(50%);
34
+ }
35
+ `}</style>
36
+ </div>
37
+ </Portal>
38
+ )
39
+
40
+ AlertStack.propTypes = {
41
+ children: PropTypes.node,
42
+ className: PropTypes.string,
43
+ dataTest: PropTypes.string,
44
+ }
@@ -0,0 +1,111 @@
1
+ import React, { useEffect, useState, useRef } from 'react'
2
+ import { AlertBar } from '../alert-bar/index.js'
3
+ import { AlertStack } from './alert-stack.js'
4
+
5
+ const VARIANTS = ['info', 'success', 'warning', 'critical']
6
+
7
+ const description = `
8
+ A container for Alert Bars.
9
+
10
+ _**Note:** The demos on this page may be slow._
11
+
12
+ \`\`\`js
13
+ import { AlertStack } from '@dhis2/ui'
14
+ \`\`\`
15
+ `
16
+
17
+ export default {
18
+ title: 'Alert Stack',
19
+ component: AlertStack,
20
+ // Use an iframe in docs to contain 'portal'
21
+ parameters: {
22
+ docs: {
23
+ inlineStories: false,
24
+ iframeHeight: '300px',
25
+ description: { component: description },
26
+ },
27
+ },
28
+ }
29
+
30
+ export const Default = (args) => (
31
+ <AlertStack {...args}>
32
+ <AlertBar permanent>First notification - I am at the bottom</AlertBar>
33
+ <AlertBar permanent critical>
34
+ Second notification
35
+ </AlertBar>
36
+ <AlertBar permanent warning>
37
+ Third notification
38
+ </AlertBar>
39
+ <AlertBar permanent success>
40
+ Fourth notification - I am at the top
41
+ </AlertBar>
42
+ </AlertStack>
43
+ )
44
+
45
+ export const Interactive = (args) => {
46
+ const [alerts, setAlerts] = useState([])
47
+ const nextId = useRef(0)
48
+ const addAlert = () => {
49
+ const id = nextId.current++
50
+ const variant = VARIANTS[id % VARIANTS.length]
51
+ setAlerts((current) => [
52
+ ...current,
53
+ { id, variant, message: `Alert #${id} (${variant})` },
54
+ ])
55
+ }
56
+ const removeAlert = (id) => {
57
+ setAlerts((current) => current.filter((a) => a.id !== id))
58
+ }
59
+ return (
60
+ <>
61
+ <button
62
+ onClick={addAlert}
63
+ style={{
64
+ padding: '8px 16px',
65
+ fontSize: '14px',
66
+ cursor: 'pointer',
67
+ }}
68
+ >
69
+ Add alert
70
+ </button>
71
+ <AlertStack {...args}>
72
+ {alerts.map(({ id, variant, message }) => (
73
+ <AlertBar
74
+ key={id}
75
+ {...{ [variant]: true }}
76
+ onHidden={() => removeAlert(id)}
77
+ >
78
+ {message}
79
+ </AlertBar>
80
+ ))}
81
+ </AlertStack>
82
+ </>
83
+ )
84
+ }
85
+
86
+ export const RTL = (args) => {
87
+ useEffect(() => {
88
+ document.body.dir = 'rtl'
89
+ return () => {
90
+ document.body.dir = 'ltr'
91
+ }
92
+ }, [])
93
+ return (
94
+ <div dir="rtl">
95
+ <AlertStack {...args}>
96
+ <AlertBar permanent>
97
+ First notification - I am at the bottom
98
+ </AlertBar>
99
+ <AlertBar permanent critical>
100
+ Second notification
101
+ </AlertBar>
102
+ <AlertBar permanent warning>
103
+ Third notification
104
+ </AlertBar>
105
+ <AlertBar permanent success>
106
+ Fourth notification - I am at the top
107
+ </AlertBar>
108
+ </AlertStack>
109
+ </div>
110
+ )
111
+ }
@@ -0,0 +1,11 @@
1
+ import { Given, Then } from '@badeball/cypress-cucumber-preprocessor'
2
+
3
+ Given('an AlertStack with multiple AlertBars is rendered', () => {
4
+ cy.visitStory('Alertstack', 'Default')
5
+ cy.get('[data-test="dhis2-uicore-alertstack"]').should('be.visible')
6
+ })
7
+
8
+ Then('the AlertBars will be visible', () => {
9
+ cy.get('[data-test="dhis2-uicore-alertbar"]').should('have.length', 3)
10
+ cy.get('[data-test="dhis2-uicore-alertbar"]').should('be.visible')
11
+ })
@@ -0,0 +1,5 @@
1
+ Feature: Shows AlertBars
2
+
3
+ Scenario: AlertStack with AlertBars
4
+ Given an AlertStack with multiple AlertBars is rendered
5
+ Then the AlertBars will be visible
@@ -0,0 +1 @@
1
+ export { AlertStack } from './alert-stack.js'
package/src/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { AlertBar } from './alert-bar/index.js'
2
+ export { AlertStack } from './alert-stack/index.js'