@dhis2-ui/switch 10.16.2 → 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/switch",
3
- "version": "10.16.2",
3
+ "version": "10.16.3-alpha.1",
4
4
  "description": "UI Switch",
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/field": "10.16.2",
37
- "@dhis2-ui/required": "10.16.2",
38
- "@dhis2/ui-constants": "10.16.2",
36
+ "@dhis2-ui/field": "10.16.3-alpha.1",
37
+ "@dhis2-ui/required": "10.16.3-alpha.1",
38
+ "@dhis2/ui-constants": "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",
package/src/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { Switch } from './switch/index.js'
2
+ export { SwitchField } from './switch-field/index.js'
@@ -0,0 +1,44 @@
1
+ import { render, fireEvent, screen } from '@testing-library/react'
2
+ import React from 'react'
3
+ import { Switch } from '../switch.js'
4
+
5
+ describe('<Switch />', () => {
6
+ it('should call the onKeyDown callback when provided', () => {
7
+ const onKeyDown = jest.fn()
8
+
9
+ render(
10
+ <Switch
11
+ name="foo"
12
+ value="bar"
13
+ checked={false}
14
+ onKeyDown={onKeyDown}
15
+ />
16
+ )
17
+
18
+ fireEvent.keyDown(screen.getByRole('switch'), {
19
+ key: 'Enter',
20
+ code: 'Enter',
21
+ charCode: 13,
22
+ })
23
+
24
+ expect(onKeyDown).toHaveBeenCalledWith(
25
+ { name: 'foo', value: 'bar', checked: true },
26
+ expect.objectContaining({})
27
+ )
28
+
29
+ expect(onKeyDown).toHaveBeenCalledTimes(1)
30
+ })
31
+
32
+ it('renders the switch with aria label', () => {
33
+ const ariaLabel = 'test switch'
34
+ render(
35
+ <Switch
36
+ aria-label={ariaLabel}
37
+ name="foo"
38
+ value="bar"
39
+ checked={false}
40
+ />
41
+ )
42
+ expect(screen.getAllByLabelText(ariaLabel)).toHaveLength(1)
43
+ })
44
+ })
@@ -0,0 +1,9 @@
1
+ import { Given, Then } from '@badeball/cypress-cucumber-preprocessor'
2
+
3
+ Given('a Switch with initialFocus is rendered', () => {
4
+ cy.visitStory('Switch', 'With initial focus')
5
+ })
6
+
7
+ Then('the Switch is focused', () => {
8
+ cy.focused().parent('[data-test="dhis2-uicore-switch"]').should('exist')
9
+ })
@@ -0,0 +1,5 @@
1
+ Feature: Focusing the Switch on mount
2
+
3
+ Scenario: The Switch renders with focus
4
+ Given a Switch with initialFocus is rendered
5
+ Then the Switch is focused
@@ -0,0 +1,11 @@
1
+ import { Given, Then } from '@badeball/cypress-cucumber-preprocessor'
2
+
3
+ Given('a Switch with a label is rendered', () => {
4
+ cy.visitStory('Switch', 'With label')
5
+ })
6
+
7
+ Then('the label is shown', () => {
8
+ cy.get('[data-test="dhis2-uicore-switch"]')
9
+ .contains('The label')
10
+ .should('be.visible')
11
+ })
@@ -0,0 +1,5 @@
1
+ Feature: The Switch shows a label
2
+
3
+ Scenario: The Switch has a label
4
+ Given a Switch with a label is rendered
5
+ Then the label is shown
@@ -0,0 +1,19 @@
1
+ import { Given, When, Then } from '@badeball/cypress-cucumber-preprocessor'
2
+
3
+ Given('a Switch with initialFocus and onBlur handler is rendered', () => {
4
+ cy.visitStory('Switch', 'With initial focus and on blur')
5
+ })
6
+
7
+ When('the Switch is blurred', () => {
8
+ cy.get('[data-test="dhis2-uicore-switch"] input').blur()
9
+ })
10
+
11
+ Then('the onBlur handler is called', () => {
12
+ cy.window().should((win) => {
13
+ expect(win.onBlur).to.be.calledWith({
14
+ value: 'default',
15
+ name: 'Ex',
16
+ checked: true,
17
+ })
18
+ })
19
+ })
@@ -0,0 +1,6 @@
1
+ Feature: The Switch has an onBlur api
2
+
3
+ Scenario: The user blurs the Switch
4
+ Given a Switch with initialFocus and onBlur handler is rendered
5
+ When the Switch is blurred
6
+ Then the onBlur handler is called
@@ -0,0 +1,19 @@
1
+ import { Given, When, Then } from '@badeball/cypress-cucumber-preprocessor'
2
+
3
+ Given('a Switch with onChange handler is rendered', () => {
4
+ cy.visitStory('Switch', 'With on change')
5
+ })
6
+
7
+ When('the Switch is clicked', () => {
8
+ cy.get('[data-test="dhis2-uicore-switch"]').click()
9
+ })
10
+
11
+ Then('the onChange handler is called', () => {
12
+ cy.window().should((win) => {
13
+ expect(win.onChange).to.be.calledWith({
14
+ value: 'default',
15
+ name: 'Ex',
16
+ checked: true,
17
+ })
18
+ })
19
+ })
@@ -0,0 +1,6 @@
1
+ Feature: The Switch has an onChange api
2
+
3
+ Scenario: The user clicks the Switch
4
+ Given a Switch with onChange handler is rendered
5
+ When the Switch is clicked
6
+ Then the onChange handler is called
@@ -0,0 +1,13 @@
1
+ import { Given, When, Then } from '@badeball/cypress-cucumber-preprocessor'
2
+
3
+ Given('a disabled Switch is rendered', () => {
4
+ cy.visitStory('Switch', 'With disabled')
5
+ })
6
+
7
+ When('the user clicks the Switch', () => {
8
+ cy.get('[data-test="dhis2-uicore-switch"] input').click({ force: true })
9
+ })
10
+
11
+ Then('the Switch is not focused', () => {
12
+ cy.focused().should('not.exist')
13
+ })
@@ -0,0 +1,6 @@
1
+ Feature: The Switch can be disabled
2
+
3
+ Scenario: The user clicks a disabled Switch
4
+ Given a disabled Switch is rendered
5
+ When the user clicks the Switch
6
+ Then the Switch is not focused
@@ -0,0 +1,19 @@
1
+ import { Given, When, Then } from '@badeball/cypress-cucumber-preprocessor'
2
+
3
+ Given('a Switch with onFocus handler is rendered', () => {
4
+ cy.visitStory('Switch', 'With on focus')
5
+ })
6
+
7
+ When('the Switch is focused', () => {
8
+ cy.get('[data-test="dhis2-uicore-switch"] input').focus()
9
+ })
10
+
11
+ Then('the onFocus handler is called', () => {
12
+ cy.window().should((win) => {
13
+ expect(win.onFocus).to.be.calledWith({
14
+ value: 'default',
15
+ name: 'Ex',
16
+ checked: true,
17
+ })
18
+ })
19
+ })
@@ -0,0 +1,6 @@
1
+ Feature: The Switch has an onFocus api
2
+
3
+ Scenario: The user focuses the Switch
4
+ Given a Switch with onFocus handler is rendered
5
+ When the Switch is focused
6
+ Then the onFocus handler is called
@@ -0,0 +1 @@
1
+ export { Switch } from './switch.js'
@@ -0,0 +1,144 @@
1
+ import { colors } from '@dhis2/ui-constants'
2
+ import PropTypes from 'prop-types'
3
+ import React from 'react'
4
+ import css from 'styled-jsx/css'
5
+
6
+ const styles = css`
7
+ svg {
8
+ display: block;
9
+ pointer-events: none;
10
+ height: 18px;
11
+ width: 35px;
12
+ }
13
+
14
+ svg:dir(rtl) {
15
+ transform: scale(-1, 1);
16
+ }
17
+
18
+ svg[dir='rtl'] {
19
+ transform: scale(-1, 1);
20
+ }
21
+
22
+ svg.dense {
23
+ height: 14px;
24
+ width: 27px;
25
+ }
26
+
27
+ svg .background,
28
+ svg .border {
29
+ fill: ${colors.grey600};
30
+ }
31
+
32
+ svg .checkmark,
33
+ svg .cross,
34
+ svg .handle {
35
+ fill: ${colors.white};
36
+ }
37
+
38
+ svg .checkmark:dir(rtl) {
39
+ display: none;
40
+ }
41
+
42
+ svg[dir='rtl'] .checkmark {
43
+ display: none;
44
+ }
45
+
46
+ svg.checked .handle-unchecked,
47
+ svg:not(.checked) .handle-checked {
48
+ fill: none;
49
+ }
50
+
51
+ svg.checked .background {
52
+ fill: ${colors.teal400};
53
+ }
54
+ svg.valid .background {
55
+ fill: ${colors.blue600};
56
+ }
57
+ svg.warning .background {
58
+ fill: ${colors.yellow700};
59
+ }
60
+ svg.error .background {
61
+ fill: ${colors.red500};
62
+ }
63
+
64
+ svg.checked .border {
65
+ fill: ${colors.teal700};
66
+ }
67
+
68
+ svg.disabled .background {
69
+ fill: ${colors.grey300};
70
+ }
71
+ svg.disabled .border {
72
+ fill: ${colors.grey400};
73
+ }
74
+
75
+ svg.valid .border {
76
+ fill: ${colors.blue600};
77
+ }
78
+ svg.valid.checked .border,
79
+ svg.valid.indeterminate .border {
80
+ fill: ${colors.blue700};
81
+ }
82
+
83
+ svg.warning .border {
84
+ fill: ${colors.yellow700};
85
+ }
86
+ svg.warning.checked .border {
87
+ fill: ${colors.yellow800};
88
+ }
89
+
90
+ svg.error .border {
91
+ fill: ${colors.red500};
92
+ }
93
+ svg.error.checked .border {
94
+ fill: ${colors.red700};
95
+ }
96
+ `
97
+
98
+ export function SwitchRegular({ className }) {
99
+ return (
100
+ <svg
101
+ viewBox="0 0 42 22"
102
+ xmlns="http://www.w3.org/2000/svg"
103
+ className={className}
104
+ dir={document.documentElement?.dir ?? 'ltr'}
105
+ >
106
+ <path
107
+ d="M0,11 L0,11 C0,4.92486775 4.92076837,0 11.0075657,0 L30.9924343,0 C37.071745,0 42,4.923532 42,11 L42,11 C42,17.0751322 37.0792316,22 30.9924343,22 L11.0075657,22 C4.92825504,22 0,17.0791222 0,11 L0,11 Z"
108
+ className="background"
109
+ fill="red"
110
+ ></path>
111
+ <path
112
+ d="M30.9924343,0 C36.975248,0 41.8432574,4.76846989 41.99629,10.7115309 L42,11 C42,17.0751322 37.0792316,22 30.9924343,22 L11.0075657,22 C5.02475203,22 0.156742552,17.2341007 0.00370995454,11.2885915 L0,11 C0,4.92486775 4.92076837,0 11.0075657,0 L30.9924343,0 Z M30.9924343,1 L11.0075657,1 C5.47559009,1 0.99991738,5.47461611 0.99991738,10.9871457 L1.00337887,11.2628608 C1.14271146,16.6761076 5.5768313,21 11.0075657,21 L30.9924343,21 C36.5244099,21 41.0000827,16.5253839 41.0000827,11.0128598 L40.9966214,10.7372722 C40.8572703,5.32553352 36.4222391,1 30.9924343,1 Z"
113
+ className="border"
114
+ // fill="#00695C"
115
+ // fillRule="nonzero"
116
+ ></path>
117
+ <path
118
+ d="M27.7071068,7.29289322 L30,9.585 L32.2928932,7.29289322 C32.6533772,6.93240926 33.2206082,6.90467972 33.6128994,7.20970461 L33.7071068,7.29289322 C34.0976311,7.68341751 34.0976311,8.31658249 33.7071068,8.70710678 L33.7071068,8.70710678 L31.415,11 L33.7071068,13.2928932 C34.0976311,13.6834175 34.0976311,14.3165825 33.7071068,14.7071068 C33.3165825,15.0976311 32.6834175,15.0976311 32.2928932,14.7071068 L30,12.415 L27.7071068,14.7071068 C27.3466228,15.0675907 26.7793918,15.0953203 26.3871006,14.7902954 L26.2928932,14.7071068 C25.9023689,14.3165825 25.9023689,13.6834175 26.2928932,13.2928932 L26.2928932,13.2928932 L28.585,11 L26.2928932,8.70710678 C25.9023689,8.31658249 25.9023689,7.68341751 26.2928932,7.29289322 C26.6834175,6.90236893 27.3165825,6.90236893 27.7071068,7.29289322 Z"
119
+ className="cross"
120
+ fill="#FFFFFF"
121
+ ></path>
122
+ <path
123
+ d="M7.74451387,10.0285252 C7.39595738,10.1198564 7.12375034,10.3923519 7.03251575,10.7412777 C6.94128115,11.0902035 7.04521722,11.4612586 7.30437605,11.7118278 L10.2982384,14.7078028 C10.6875399,15.0973991 11.3185977,15.0973991 11.7078992,14.7078028 L16.695624,8.71585285 C16.9547828,8.46528367 17.0587189,8.09422851 16.9674843,7.74530271 C16.8762497,7.39637691 16.6040426,7.12388146 16.2554861,7.0325502 C15.9069296,6.94121893 15.5362672,7.04526513 15.2859632,7.30469855 L11.0030688,12.5910713 L8.71403676,10.3006735 C8.46373279,10.0412401 8.09307036,9.9371939 7.74451387,10.0285252 Z"
124
+ className="checkmark"
125
+ // fill="#FFFFFF"
126
+ // fillRule="nonzero"
127
+ ></path>
128
+ <path
129
+ d="M11,20 C15.9705627,20 20,15.9705627 20,11 C20,6.02943725 15.9705627,2 11,2 C6.02943725,2 2,6.02943725 2,11 C2,15.9705627 6.02943725,20 11,20 Z"
130
+ className="handle handle-unchecked"
131
+ // fill="#FFFFFF"
132
+ ></path>
133
+ <path
134
+ d="M31,20 C35.9705627,20 40,15.9705627 40,11 C40,6.02943725 35.9705627,2 31,2 C26.0294373,2 22,6.02943725 22,11 C22,15.9705627 26.0294373,20 31,20 Z"
135
+ className="handle handle-checked"
136
+ // fill="#FFFFFF"
137
+ ></path>
138
+ <style jsx>{styles}</style>
139
+ </svg>
140
+ )
141
+ }
142
+ SwitchRegular.propTypes = {
143
+ className: PropTypes.string,
144
+ }
@@ -0,0 +1,37 @@
1
+ import React from 'react'
2
+ import { Switch } from './index.js'
3
+
4
+ window.onChange = window.Cypress && window.Cypress.cy.stub()
5
+ window.onBlur = window.Cypress && window.Cypress.cy.stub()
6
+ window.onFocus = window.Cypress && window.Cypress.cy.stub()
7
+
8
+ export default { title: 'Switch' }
9
+ export const WithOnChange = () => (
10
+ <Switch
11
+ name="Ex"
12
+ label="Switch"
13
+ value="default"
14
+ onChange={window.onChange}
15
+ />
16
+ )
17
+ export const WithInitialFocusAndOnBlur = () => (
18
+ <Switch
19
+ initialFocus
20
+ name="Ex"
21
+ label="Switch"
22
+ value="default"
23
+ onBlur={window.onBlur}
24
+ />
25
+ )
26
+ export const WithOnFocus = () => (
27
+ <Switch name="Ex" label="Switch" value="default" onFocus={window.onFocus} />
28
+ )
29
+ export const WithDisabled = () => (
30
+ <Switch name="Ex" label="Switch" value="default" disabled />
31
+ )
32
+ export const WithLabel = () => (
33
+ <Switch name="Ex" label="The label" value="default" />
34
+ )
35
+ export const WithInitialFocus = () => (
36
+ <Switch name="Ex" label="The label" value="default" initialFocus />
37
+ )
@@ -0,0 +1,210 @@
1
+ import { colors, theme, sharedPropTypes } from '@dhis2/ui-constants'
2
+ import cx from 'classnames'
3
+ import PropTypes from 'prop-types'
4
+ import React, { Component, createRef } from 'react'
5
+ import { SwitchRegular } from './switch-icons.js'
6
+
7
+ class Switch extends Component {
8
+ ref = createRef()
9
+
10
+ componentDidMount() {
11
+ if (this.props.initialFocus) {
12
+ this.ref.current.focus()
13
+ }
14
+ }
15
+
16
+ handleChange = (e) => {
17
+ if (this.props.onChange) {
18
+ this.props.onChange(this.createHandlerPayload(), e)
19
+ }
20
+ }
21
+
22
+ handleBlur = (e) => {
23
+ if (this.props.onBlur) {
24
+ this.props.onBlur(this.createHandlerPayload(), e)
25
+ }
26
+ }
27
+
28
+ handleFocus = (e) => {
29
+ if (this.props.onFocus) {
30
+ this.props.onFocus(this.createHandlerPayload(), e)
31
+ }
32
+ }
33
+
34
+ handleKeyDown = (e) => {
35
+ if (this.props.onKeyDown) {
36
+ this.props.onKeyDown(this.createHandlerPayload(), e)
37
+ }
38
+ }
39
+
40
+ createHandlerPayload() {
41
+ return {
42
+ value: this.props.value,
43
+ name: this.props.name,
44
+ checked: !this.props.checked,
45
+ }
46
+ }
47
+
48
+ static defaultProps = {
49
+ checked: false,
50
+ dataTest: 'dhis2-uicore-switch',
51
+ role: 'switch',
52
+ }
53
+
54
+ render() {
55
+ const {
56
+ 'aria-label': ariaLabel,
57
+ checked = false,
58
+ className,
59
+ disabled,
60
+ error,
61
+ label,
62
+ name,
63
+ tabIndex,
64
+ valid,
65
+ value,
66
+ warning,
67
+ dense,
68
+ dataTest = 'dhis2-uicore-switch',
69
+ role = 'switch',
70
+ } = this.props
71
+
72
+ const classes = cx({
73
+ checked,
74
+ disabled,
75
+ valid,
76
+ error,
77
+ warning,
78
+ dense,
79
+ })
80
+
81
+ return (
82
+ <label
83
+ className={cx(className, {
84
+ disabled,
85
+ dense,
86
+ })}
87
+ data-test={dataTest}
88
+ >
89
+ <input
90
+ aria-label={ariaLabel}
91
+ type="checkbox"
92
+ role={role}
93
+ ref={this.ref}
94
+ name={name}
95
+ value={value}
96
+ checked={checked}
97
+ disabled={disabled}
98
+ tabIndex={tabIndex}
99
+ onChange={this.handleChange}
100
+ onFocus={this.handleFocus}
101
+ onKeyDown={this.handleKeyDown}
102
+ onBlur={this.handleBlur}
103
+ />
104
+
105
+ <div className={cx('icon', { dense })}>
106
+ <SwitchRegular className={classes} />
107
+ </div>
108
+
109
+ {label}
110
+
111
+ <style jsx>{`
112
+ label {
113
+ display: flex;
114
+ flex-direction: row;
115
+ align-items: center;
116
+ justify-content: flex-start;
117
+ color: ${colors.grey900};
118
+ font-size: 14px;
119
+ line-height: 19px;
120
+ }
121
+
122
+ label.dense {
123
+ font-size: 14px;
124
+ line-height: 16px;
125
+ }
126
+
127
+ label.disabled {
128
+ cursor: not-allowed;
129
+ color: ${theme.disabled};
130
+ }
131
+
132
+ input {
133
+ opacity: 0;
134
+ position: absolute;
135
+
136
+ /* The same size as the icon */
137
+ height: 18px;
138
+ width: 35px;
139
+
140
+ /* The same offset as the icon, 2px border, 1px padding */
141
+ margin-left: 3px;
142
+ }
143
+
144
+ label.dense input {
145
+ /* The same size as the dense icon */
146
+ height: 14px;
147
+ width: 27px;
148
+ }
149
+
150
+ .icon {
151
+ user-select: none;
152
+ margin-right: 5px;
153
+ border: 2px solid transparent;
154
+ padding: 1px;
155
+ border-radius: 14px;
156
+ }
157
+
158
+ label.dense .icon {
159
+ margin-right: 3px;
160
+ border-radius: 12px;
161
+ }
162
+
163
+ input:focus + .icon {
164
+ outline: 3px solid ${theme.focus};
165
+ outline-offset: -3px;
166
+ }
167
+ `}</style>
168
+ </label>
169
+ )
170
+ }
171
+ }
172
+
173
+ Switch.propTypes = {
174
+ /** Sets an aria-label attribute on the input */
175
+ 'aria-label': PropTypes.string,
176
+ checked: PropTypes.bool,
177
+ className: PropTypes.string,
178
+ dataTest: PropTypes.string,
179
+ /** Makes the switch smaller for information-dense layouts */
180
+ dense: PropTypes.bool,
181
+ /** Disables the switch */
182
+ disabled: PropTypes.bool,
183
+ /** Applies 'error' styles for validation feedback. Mutually exclusive with `valid` and `warning` prop types */
184
+ error: sharedPropTypes.statusPropType,
185
+ /** Grab initial focus on the page */
186
+ initialFocus: PropTypes.bool,
187
+ /** Label for the switch. Can be a string or an element, for example an image */
188
+ label: PropTypes.node,
189
+ /** Name associated with the switch. Passed to event handlers in object */
190
+ name: PropTypes.string,
191
+ /** Sets a role attribute on the input */
192
+ role: PropTypes.string,
193
+ tabIndex: PropTypes.string,
194
+ /** Applies 'valid' styles for validation feedback. Mutually exclusive with `error` and `warning` prop types */
195
+ valid: sharedPropTypes.statusPropType,
196
+ /** Value associated with the switch. Passed to event handlers in object */
197
+ value: PropTypes.string,
198
+ /** Applies 'warning' styles for validation feedback. Mutually exclusive with `valid` and `error` prop types */
199
+ warning: sharedPropTypes.statusPropType,
200
+ /** Called with signature `({ name: string, value: string, checked: bool }, event)` */
201
+ onBlur: PropTypes.func,
202
+ /** Called with signature `({ name: string, value: string, checked: bool }, event)` */
203
+ onChange: PropTypes.func,
204
+ /** Called with signature `({ name: string, value: string, checked: bool }, event)` */
205
+ onFocus: PropTypes.func,
206
+ /** Called with signature `({ name: string, value: string, checked: bool }, event)` */
207
+ onKeyDown: PropTypes.func,
208
+ }
209
+
210
+ export { Switch }
@@ -0,0 +1,157 @@
1
+ import { sharedPropTypes } from '@dhis2/ui-constants'
2
+ import React, { useEffect } from 'react'
3
+ import { Switch } from './index.js'
4
+
5
+ const subtitle = 'An input control that allows an on and an off state'
6
+
7
+ const description = `
8
+ **Switches are used sparingly in DHIS2, as they are not yet an accepted input control on the web. Users are not always used to the concept of a switch, but understanding is growing with wide adoption on mobile platforms.**
9
+
10
+ Use switches only when the user can toggle between on/off. Never use a switch for yes/no or any other states, use a checkbox instead. It is often safer to use a checkbox for things like turning options on/off, as users understand this pattern. Switches can be useful for ongoing or active processes, where turning them on/off makes more sense conceptually. An example of this may be toggling on/off 'Logging' or 'Update automatically', both processes that are ongoing.
11
+
12
+ \`\`\`js
13
+ import { Switch } from '@dhis2/ui'
14
+ \`\`\`
15
+ `
16
+
17
+ window.onChange = (payload, event) => {
18
+ console.log('onClick payload', payload)
19
+ console.log('onClick event', event)
20
+ }
21
+
22
+ window.onFocus = (payload, event) => {
23
+ console.log('onFocus payload', payload)
24
+ console.log('onFocus event', event)
25
+ }
26
+
27
+ window.onBlur = (payload, event) => {
28
+ console.log('onBlur payload', payload)
29
+ console.log('onBlur event', event)
30
+ }
31
+
32
+ const onChange = (...args) => window.onChange(...args)
33
+ const onFocus = (...args) => window.onFocus(...args)
34
+ const onBlur = (...args) => window.onBlur(...args)
35
+
36
+ export default {
37
+ title: 'Switch',
38
+ component: Switch,
39
+ parameters: {
40
+ componentSubtitle: subtitle,
41
+ docs: { description: { component: description } },
42
+ },
43
+ // Default args for all stories
44
+ args: {
45
+ name: 'exampleName',
46
+ label: 'Switch',
47
+ value: 'defaultValue',
48
+ onChange,
49
+ onFocus,
50
+ onBlur,
51
+ },
52
+ argTypes: {
53
+ valid: { ...sharedPropTypes.statusArgType },
54
+ error: { ...sharedPropTypes.statusArgType },
55
+ warning: { ...sharedPropTypes.statusArgType },
56
+ },
57
+ }
58
+
59
+ const Template = (args) => <Switch {...args} />
60
+
61
+ const CheckedUncheckedTemplate = (args) => (
62
+ <>
63
+ <Switch {...args} />
64
+ <Switch {...args} checked />
65
+ </>
66
+ )
67
+
68
+ export const Default = Template.bind({})
69
+
70
+ export const FocusedUnchecked = (args) => (
71
+ <>
72
+ <Switch {...args} initialFocus className="initially-focused" />
73
+ <Switch {...args} className="initially-unfocused" />
74
+ </>
75
+ )
76
+ // Stories with initial focus are distracting on docs page
77
+ FocusedUnchecked.parameters = { docs: { disable: true } }
78
+
79
+ export const FocusedChecked = FocusedUnchecked.bind({})
80
+ FocusedChecked.args = { checked: true }
81
+ FocusedChecked.parameters = { docs: { disable: true } }
82
+
83
+ export const Checked = Template.bind({})
84
+ Checked.args = { checked: true, value: 'checked' }
85
+
86
+ export const Disabled = CheckedUncheckedTemplate.bind({})
87
+ Disabled.args = { disabled: true, value: 'disabled' }
88
+
89
+ export const Valid = CheckedUncheckedTemplate.bind({})
90
+ Valid.args = { valid: true, value: 'valid' }
91
+
92
+ export const Warning = CheckedUncheckedTemplate.bind({})
93
+ Warning.args = { warning: true, value: 'warning' }
94
+
95
+ export const Error = CheckedUncheckedTemplate.bind({})
96
+ Error.args = { error: true, value: 'error' }
97
+
98
+ export const ImageLabel = Template.bind({})
99
+ ImageLabel.args = {
100
+ label: <img src="https://picsum.photos/id/82/200/100" />,
101
+ value: 'with-help',
102
+ }
103
+
104
+ export const DefaultDense = Template.bind({})
105
+ DefaultDense.args = { dense: true }
106
+ DefaultDense.storyName = 'Default - Dense'
107
+
108
+ export const FocusedUncheckedDense = FocusedUnchecked.bind({})
109
+ FocusedUncheckedDense.args = { ...DefaultDense.args }
110
+ FocusedUncheckedDense.storyName = 'Focused unchecked - Dense'
111
+ FocusedUncheckedDense.parameters = { docs: { disable: true } }
112
+
113
+ export const FocusedCheckedDense = FocusedUnchecked.bind({})
114
+ FocusedCheckedDense.args = { ...DefaultDense.args, checked: true }
115
+ FocusedCheckedDense.storyName = 'Focused checked - Dense'
116
+ FocusedCheckedDense.parameters = { docs: { disable: true } }
117
+
118
+ export const CheckedDense = Template.bind({})
119
+ CheckedDense.args = { ...Checked.args, ...DefaultDense.args }
120
+ CheckedDense.storyName = 'Checked - Dense'
121
+
122
+ export const DisabledDense = CheckedUncheckedTemplate.bind({})
123
+ DisabledDense.args = { ...Disabled.args, ...DefaultDense.args }
124
+ DisabledDense.storyName = 'Disabled - Dense'
125
+
126
+ export const ValidDense = CheckedUncheckedTemplate.bind({})
127
+ ValidDense.args = { ...Valid.args, ...DefaultDense.args }
128
+ ValidDense.storyName = 'Valid - Dense'
129
+
130
+ export const WarningDense = CheckedUncheckedTemplate.bind({})
131
+ WarningDense.args = { ...Warning.args, ...DefaultDense.args }
132
+ WarningDense.storyName = 'Warning - Dense'
133
+
134
+ export const ErrorDense = CheckedUncheckedTemplate.bind({})
135
+ ErrorDense.args = { ...Error.args, ...DefaultDense.args }
136
+ ErrorDense.storyName = 'Error - Dense'
137
+
138
+ export const ImageLabelDense = Template.bind({})
139
+ ImageLabelDense.args = { ...ImageLabel.args, ...DefaultDense.args }
140
+ ImageLabelDense.storyName = 'Image label - Dense'
141
+
142
+ export const RTL = (args) => {
143
+ useEffect(() => {
144
+ document.documentElement.setAttribute('dir', 'rtl')
145
+ return () => {
146
+ document.documentElement.setAttribute('dir', 'ltr')
147
+ }
148
+ }, [])
149
+
150
+ return (
151
+ <div dir="rtl">
152
+ <Template {...args} />
153
+ <Template checked={true} value="checked" />
154
+ <Template error={true} checked={true} value="error" />
155
+ </div>
156
+ )
157
+ }
@@ -0,0 +1,32 @@
1
+ import { render, fireEvent, screen } from '@testing-library/react'
2
+ import React from 'react'
3
+ import { SwitchField } from '../switch-field.js'
4
+
5
+ describe('<Switch />', () => {
6
+ it('should call the onKeyDown callback when provided', () => {
7
+ const onKeyDown = jest.fn()
8
+
9
+ render(
10
+ <SwitchField
11
+ label="label"
12
+ name="foo"
13
+ value="bar"
14
+ checked={false}
15
+ onKeyDown={onKeyDown}
16
+ />
17
+ )
18
+
19
+ fireEvent.keyDown(screen.getByRole('switch'), {
20
+ key: 'Enter',
21
+ code: 'Enter',
22
+ charCode: 13,
23
+ })
24
+
25
+ expect(onKeyDown).toHaveBeenCalledWith(
26
+ { name: 'foo', value: 'bar', checked: true },
27
+ expect.objectContaining({})
28
+ )
29
+
30
+ expect(onKeyDown).toHaveBeenCalledTimes(1)
31
+ })
32
+ })
@@ -0,0 +1,11 @@
1
+ import { Given, Then } from '@badeball/cypress-cucumber-preprocessor'
2
+
3
+ Given('a SwitchField with label and a required flag is rendered', () => {
4
+ cy.visitStory('SwitchField', 'With label and required')
5
+ })
6
+
7
+ Then('the required indicator is visible', () => {
8
+ cy.get('[data-test="dhis2-uiwidgets-switchfield-required"]').should(
9
+ 'be.visible'
10
+ )
11
+ })
@@ -0,0 +1,5 @@
1
+ Feature: Required status for the SwitchField
2
+
3
+ Scenario: Rendering a SwitchField that is required
4
+ Given a SwitchField with label and a required flag is rendered
5
+ Then the required indicator is visible
@@ -0,0 +1 @@
1
+ export { SwitchField } from './switch-field.js'
@@ -0,0 +1,7 @@
1
+ import React from 'react'
2
+ import { SwitchField } from './index.js'
3
+
4
+ export default { title: 'SwitchField' }
5
+ export const WithLabelAndRequired = () => (
6
+ <SwitchField name="Ex" label="SwitchField" required value="checked" />
7
+ )
@@ -0,0 +1,118 @@
1
+ import { sharedPropTypes } from '@dhis2/ui-constants'
2
+ import { Field } from '@dhis2-ui/field'
3
+ import { Required } from '@dhis2-ui/required'
4
+ import PropTypes from 'prop-types'
5
+ import React from 'react'
6
+ import { Switch } from '../switch/index.js'
7
+
8
+ const AddRequired = ({ label, required, dataTest }) => (
9
+ <React.Fragment>
10
+ {label}
11
+ {required && <Required dataTest={`${dataTest}-required`} />}
12
+ </React.Fragment>
13
+ )
14
+ AddRequired.propTypes = {
15
+ dataTest: PropTypes.string,
16
+ label: PropTypes.node,
17
+ required: PropTypes.bool,
18
+ }
19
+
20
+ const SwitchField = ({
21
+ value,
22
+ label,
23
+ name,
24
+ className,
25
+ tabIndex,
26
+ onChange,
27
+ onFocus,
28
+ onKeyDown,
29
+ onBlur,
30
+ checked,
31
+ disabled,
32
+ valid,
33
+ warning,
34
+ error,
35
+ dense,
36
+ initialFocus,
37
+ required,
38
+ helpText,
39
+ validationText,
40
+ dataTest = 'dhis2-uiwidgets-switchfield',
41
+ }) => (
42
+ <Field
43
+ className={className}
44
+ dataTest={dataTest}
45
+ helpText={helpText}
46
+ validationText={validationText}
47
+ error={error}
48
+ warning={warning}
49
+ valid={valid}
50
+ required={required}
51
+ name={name}
52
+ disabled={disabled}
53
+ >
54
+ <Switch
55
+ value={value}
56
+ label={
57
+ <AddRequired
58
+ label={label}
59
+ required={required}
60
+ dataTest={dataTest}
61
+ />
62
+ }
63
+ name={name}
64
+ tabIndex={tabIndex}
65
+ onChange={onChange}
66
+ onFocus={onFocus}
67
+ onKeyDown={onKeyDown}
68
+ onBlur={onBlur}
69
+ checked={checked}
70
+ disabled={disabled}
71
+ valid={valid}
72
+ warning={warning}
73
+ error={error}
74
+ dense={dense}
75
+ initialFocus={initialFocus}
76
+ />
77
+ </Field>
78
+ )
79
+
80
+ SwitchField.propTypes = {
81
+ checked: PropTypes.bool,
82
+ className: PropTypes.string,
83
+ dataTest: PropTypes.string,
84
+ /** Smaller dimensions for information-dense layouts */
85
+ dense: PropTypes.bool,
86
+ /** Disables the switch */
87
+ disabled: PropTypes.bool,
88
+ /** Applies 'error' styling to switch and validation text for feedback. Mutually exclusive with `warning` and `valid` props */
89
+ error: sharedPropTypes.statusPropType,
90
+ /** Useful instructions for the user */
91
+ helpText: PropTypes.string,
92
+ initialFocus: PropTypes.bool,
93
+ /** Labels the switch */
94
+ label: PropTypes.node,
95
+ /** Name associate with the switch. Passed in object as argument to event handlers */
96
+ name: PropTypes.string,
97
+ /** Adds an asterisk to indicate this field is required */
98
+ required: PropTypes.bool,
99
+ tabIndex: PropTypes.string,
100
+ /** Applies 'valid' styling to switch and validation text for feedback. Mutually exclusive with `warning` and `error` props */
101
+ valid: sharedPropTypes.statusPropType,
102
+ /** Adds text below the switch to provide validation feedback. Acquires styles from `valid`, `warning` and `error` statuses */
103
+ validationText: PropTypes.string,
104
+ /** Value associated with the switch. Passed in object as argument to event handlers */
105
+ value: PropTypes.string,
106
+ /** Applies 'warning' styling to switch and validation text for feedback. Mutually exclusive with `valid` and `error` props */
107
+ warning: sharedPropTypes.statusPropType,
108
+ /** Called with signature `({ name: string, value: string, checked: bool }, event)` */
109
+ onBlur: PropTypes.func,
110
+ /** Called with signature `({ name: string, value: string, checked: bool }, event)` */
111
+ onChange: PropTypes.func,
112
+ /** Called with signature `({ name: string, value: string, checked: bool }, event)` */
113
+ onFocus: PropTypes.func,
114
+ /** Called with signature `({ name: string, value: string, checked: bool }, event)` */
115
+ onKeyDown: PropTypes.func,
116
+ }
117
+
118
+ export { SwitchField }
@@ -0,0 +1,141 @@
1
+ import { sharedPropTypes } from '@dhis2/ui-constants'
2
+ import React from 'react'
3
+ import { SwitchField } from './index.js'
4
+
5
+ const description = `
6
+ A \`SwitchField\` is a Switch component wrapped with extra form utilities, including the ability to add a label, help text, and validation text. Validation styles like 'error' apply to all of these subcomponents.
7
+
8
+ See the basic Switch for usage and design system guidelines.
9
+
10
+ \`\`\`js
11
+ import { SwitchField } from '@dhis2/ui'
12
+ \`\`\`
13
+ `
14
+
15
+ const logger = ({ name, value, checked }) =>
16
+ console.log(`name: ${name}, value: ${value}, checked: ${checked}`)
17
+
18
+ export default {
19
+ title: 'Switch Field',
20
+ component: SwitchField,
21
+ parameters: { docs: { description: { component: description } } },
22
+ // Default args for stories
23
+ args: {
24
+ name: 'switchName',
25
+ label: 'Switch Field',
26
+ value: 'defaultValue',
27
+ onChange: logger,
28
+ },
29
+ argTypes: {
30
+ valid: { ...sharedPropTypes.statusArgType },
31
+ warning: { ...sharedPropTypes.statusArgType },
32
+ error: { ...sharedPropTypes.statusArgType },
33
+ },
34
+ }
35
+
36
+ const Template = (args) => <SwitchField {...args} />
37
+
38
+ const CheckedUncheckedTemplate = (args) => (
39
+ <>
40
+ <SwitchField {...args} />
41
+ <SwitchField {...args} checked />
42
+ </>
43
+ )
44
+
45
+ export const Default = Template.bind({})
46
+
47
+ export const FocusedUnchecked = Template.bind({})
48
+ FocusedUnchecked.args = { initialFocus: true }
49
+ // Disable stories on docs page that grab focus
50
+ FocusedUnchecked.parameters = { docs: { disable: true } }
51
+
52
+ export const FocusedChecked = Template.bind({})
53
+ FocusedChecked.args = { ...FocusedUnchecked.args, checked: true }
54
+ FocusedChecked.parameters = { docs: { disable: true } }
55
+
56
+ export const Checked = Template.bind({})
57
+ Checked.args = { checked: true, value: 'checkedValue' }
58
+
59
+ export const Required = Template.bind({})
60
+ Required.args = { required: true }
61
+
62
+ export const Disabled = CheckedUncheckedTemplate.bind({})
63
+ Disabled.args = { disabled: true }
64
+
65
+ export const HelpText = (args) => (
66
+ <>
67
+ <SwitchField {...args} />
68
+ <SwitchField
69
+ {...args}
70
+ error
71
+ validationText="Validation text (error state)"
72
+ />
73
+ </>
74
+ )
75
+ HelpText.args = { helpText: 'Help text' }
76
+
77
+ export const Valid = CheckedUncheckedTemplate.bind({})
78
+ Valid.args = {
79
+ valid: true,
80
+ validationText: 'I am validation text',
81
+ value: 'validValue',
82
+ }
83
+
84
+ export const Warning = CheckedUncheckedTemplate.bind({})
85
+ Warning.args = {
86
+ warning: true,
87
+ value: 'warningValue',
88
+ validationText: 'I am validation text',
89
+ }
90
+
91
+ export const Error = CheckedUncheckedTemplate.bind({})
92
+ Error.args = {
93
+ error: true,
94
+ value: 'errorValue',
95
+ validationText: 'I am validation text',
96
+ }
97
+
98
+ export const ImageLabel = Template.bind({})
99
+ ImageLabel.args = { label: <img src="https://picsum.photos/id/82/200/100" /> }
100
+
101
+ export const DefaultDense = Template.bind({})
102
+ DefaultDense.storyName = 'Default - Dense'
103
+ DefaultDense.args = { dense: true }
104
+
105
+ export const FocusedUncheckedDense = Template.bind({})
106
+ FocusedUncheckedDense.args = { ...DefaultDense.args, ...FocusedUnchecked.args }
107
+ FocusedUncheckedDense.parameters = { docs: { disable: true } }
108
+ FocusedUncheckedDense.storyName = 'Focused unchecked - Dense'
109
+
110
+ export const FocusedCheckedDense = Template.bind({})
111
+ FocusedCheckedDense.args = { ...DefaultDense.args, ...FocusedChecked.args }
112
+ FocusedCheckedDense.parameters = { docs: { disable: true } }
113
+ FocusedCheckedDense.storyName = 'Focused checked - Dense'
114
+
115
+ export const CheckedDense = Template.bind({})
116
+ CheckedDense.args = { ...DefaultDense.args, ...Checked.args }
117
+ CheckedDense.storyName = 'Checked - Dense'
118
+
119
+ export const RequiredDense = Template.bind({})
120
+ RequiredDense.args = { ...DefaultDense.args, ...Required.args }
121
+ RequiredDense.storyName = 'Required - Dense'
122
+
123
+ export const DisabledDense = CheckedUncheckedTemplate.bind({})
124
+ DisabledDense.args = { ...DefaultDense.args, ...Disabled.args }
125
+ DisabledDense.storyName = 'Disabled - Dense'
126
+
127
+ export const ValidDense = CheckedUncheckedTemplate.bind({})
128
+ ValidDense.args = { ...DefaultDense.args, ...Valid.args }
129
+ ValidDense.storyName = 'Valid - Dense'
130
+
131
+ export const WarningDense = CheckedUncheckedTemplate.bind({})
132
+ WarningDense.args = { ...DefaultDense.args, ...Warning.args }
133
+ WarningDense.storyName = 'Warning - Dense'
134
+
135
+ export const ErrorDense = CheckedUncheckedTemplate.bind({})
136
+ ErrorDense.args = { ...DefaultDense.args, ...Error.args }
137
+ ErrorDense.storyName = 'Error - Dense'
138
+
139
+ export const ImageLabelDense = Template.bind({})
140
+ ImageLabelDense.args = { ...DefaultDense.args, ...ImageLabel.args }
141
+ ImageLabelDense.storyName = 'Image label - Dense'