@dhis2-ui/input 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/input",
3
- "version": "10.16.2",
3
+ "version": "10.16.3-alpha.1",
4
4
  "description": "UI Input",
5
5
  "repository": {
6
6
  "type": "git",
@@ -33,19 +33,20 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "@dhis2/prop-types": "^3.1.2",
36
- "@dhis2-ui/box": "10.16.2",
37
- "@dhis2-ui/field": "10.16.2",
38
- "@dhis2-ui/input": "10.16.2",
39
- "@dhis2-ui/loader": "10.16.2",
40
- "@dhis2-ui/status-icon": "10.16.2",
41
- "@dhis2/ui-constants": "10.16.2",
42
- "@dhis2/ui-icons": "10.16.2",
36
+ "@dhis2-ui/box": "10.16.3-alpha.1",
37
+ "@dhis2-ui/field": "10.16.3-alpha.1",
38
+ "@dhis2-ui/input": "10.16.3-alpha.1",
39
+ "@dhis2-ui/loader": "10.16.3-alpha.1",
40
+ "@dhis2-ui/status-icon": "10.16.3-alpha.1",
41
+ "@dhis2/ui-constants": "10.16.3-alpha.1",
42
+ "@dhis2/ui-icons": "10.16.3-alpha.1",
43
43
  "classnames": "^2.3.1",
44
44
  "prop-types": "^15.7.2"
45
45
  },
46
46
  "files": [
47
47
  "build",
48
- "types"
48
+ "types",
49
+ "src"
49
50
  ],
50
51
  "devDependencies": {
51
52
  "react": "^18.3.1",
package/src/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { Input } from './input/index.js'
2
+ export { InputField, InputFieldProps } from './input-field/index.js'
@@ -0,0 +1,29 @@
1
+ import { render, fireEvent, screen } from '@testing-library/react'
2
+ import { shallow } from 'enzyme'
3
+ import React from 'react'
4
+ import { Input } from '../input.js'
5
+
6
+ describe('<Input>', () => {
7
+ it('passes min, max, and step props as attributes to the native <input> element', () => {
8
+ const testProps = { min: '0', max: '10', step: '0.5' }
9
+ const wrapper = shallow(<Input type="number" {...testProps} />)
10
+
11
+ const inputEl = wrapper.find('input')
12
+ expect(inputEl.props()).toMatchObject(testProps)
13
+ })
14
+
15
+ it('should call the onKeyDown callback when provided', () => {
16
+ const onKeyDown = jest.fn()
17
+
18
+ render(<Input name="foo" value="bar" onKeyDown={onKeyDown} />)
19
+
20
+ fireEvent.keyDown(screen.getByRole('textbox'), {})
21
+
22
+ expect(onKeyDown).toHaveBeenCalledWith(
23
+ { name: 'foo', value: 'bar' },
24
+ expect.objectContaining({})
25
+ )
26
+
27
+ expect(onKeyDown).toHaveBeenCalledTimes(1)
28
+ })
29
+ })
@@ -0,0 +1,9 @@
1
+ import { Given, Then } from '@badeball/cypress-cucumber-preprocessor'
2
+
3
+ Given('a Input with initialFocus is rendered', () => {
4
+ cy.visitStory('Input', 'With initial focus')
5
+ })
6
+
7
+ Then('the Input is focused', () => {
8
+ cy.focused().parent('[data-test="dhis2-uicore-input"]').should('exist')
9
+ })
@@ -0,0 +1,5 @@
1
+ Feature: Focusing the Input on mount
2
+
3
+ Scenario: The Input renders with focus
4
+ Given a Input with initialFocus is rendered
5
+ Then the Input is focused
@@ -0,0 +1,18 @@
1
+ import { Given, When, Then } from '@badeball/cypress-cucumber-preprocessor'
2
+
3
+ Given('an Input with initialFocus and onBlur handler is rendered', () => {
4
+ cy.visitStory('Input', 'With initial focus and on blur')
5
+ })
6
+
7
+ When('the Input is blurred', () => {
8
+ cy.get('[data-test="dhis2-uicore-input"] 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: '',
15
+ name: 'Default',
16
+ })
17
+ })
18
+ })
@@ -0,0 +1,6 @@
1
+ Feature: The Input has an onBlur api
2
+
3
+ Scenario: The user blurs the Input
4
+ Given an Input with initialFocus and onBlur handler is rendered
5
+ When the Input is blurred
6
+ Then the onBlur handler is called
@@ -0,0 +1,18 @@
1
+ import { Given, When, Then } from '@badeball/cypress-cucumber-preprocessor'
2
+
3
+ Given('a Input with onChange handler is rendered', () => {
4
+ cy.visitStory('Input', 'With on change')
5
+ })
6
+
7
+ When('the Input is filled with a character', () => {
8
+ cy.get('[data-test="dhis2-uicore-input"]').click().type('a')
9
+ })
10
+
11
+ Then('the onChange handler is called', () => {
12
+ cy.window().should((win) => {
13
+ expect(win.onChange).to.be.calledWith({
14
+ value: 'a',
15
+ name: 'Default',
16
+ })
17
+ })
18
+ })
@@ -0,0 +1,6 @@
1
+ Feature: The Input has an onChange api
2
+
3
+ Scenario: The user types a character into the Input
4
+ Given a Input with onChange handler is rendered
5
+ When the Input is filled with a character
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 Input is rendered', () => {
4
+ cy.visitStory('Input', 'With disabled')
5
+ })
6
+
7
+ When('the user clicks the input', () => {
8
+ cy.get('[data-test="dhis2-uicore-input"] input').click({ force: true })
9
+ })
10
+
11
+ Then('the Input is not focused', () => {
12
+ cy.focused().should('not.exist')
13
+ })
@@ -0,0 +1,6 @@
1
+ Feature: The Input can be disabled
2
+
3
+ Scenario: The user clicks a disabled Input
4
+ Given a disabled Input is rendered
5
+ When the user clicks the input
6
+ Then the Input is not focused
@@ -0,0 +1,18 @@
1
+ import { Given, When, Then } from '@badeball/cypress-cucumber-preprocessor'
2
+
3
+ Given('a Input with onFocus handler is rendered', () => {
4
+ cy.visitStory('Input', 'With on focus')
5
+ })
6
+
7
+ When('the Input is focused', () => {
8
+ cy.get('[data-test="dhis2-uicore-input"] 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: '',
15
+ name: 'Default',
16
+ })
17
+ })
18
+ })
@@ -0,0 +1,6 @@
1
+ Feature: The Input has an onFocus api
2
+
3
+ Scenario: The user focuses the Input
4
+ Given a Input with onFocus handler is rendered
5
+ When the Input is focused
6
+ Then the onFocus handler is called
@@ -0,0 +1,2 @@
1
+ export { Input } from './input.js'
2
+ export { inputTypes } from './inputTypes.js'
@@ -0,0 +1,39 @@
1
+ import React from 'react'
2
+ import { Input } 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: 'Input' }
9
+ export const WithOnChange = () => (
10
+ <Input
11
+ label="Default label"
12
+ name="Default"
13
+ value=""
14
+ onChange={window.onChange}
15
+ />
16
+ )
17
+ export const WithInitialFocusAndOnBlur = () => (
18
+ <Input
19
+ label="Default label"
20
+ name="Default"
21
+ value=""
22
+ initialFocus
23
+ onBlur={window.onBlur}
24
+ />
25
+ )
26
+ export const WithOnFocus = () => (
27
+ <Input
28
+ label="Default label"
29
+ name="Default"
30
+ value=""
31
+ onFocus={window.onFocus}
32
+ />
33
+ )
34
+ export const WithInitialFocus = () => (
35
+ <Input label="Default label" name="Default" value="" initialFocus />
36
+ )
37
+ export const WithDisabled = () => (
38
+ <Input label="Default label" name="Default" value="" disabled />
39
+ )
@@ -0,0 +1,344 @@
1
+ import { theme, colors, spacers, sharedPropTypes } from '@dhis2/ui-constants'
2
+ import { IconCross16 } from '@dhis2/ui-icons'
3
+ import { StatusIcon } from '@dhis2-ui/status-icon'
4
+ import cx from 'classnames'
5
+ import PropTypes from 'prop-types'
6
+ import React, { Component } from 'react'
7
+ import css from 'styled-jsx/css'
8
+ import { inputTypes } from './inputTypes.js'
9
+
10
+ const styles = css`
11
+ .input {
12
+ display: inline-flex;
13
+ align-items: center;
14
+ position: relative;
15
+ gap: ${spacers.dp8};
16
+ }
17
+
18
+ input {
19
+ box-sizing: border-box;
20
+
21
+ font-size: 14px;
22
+ line-height: 16px;
23
+ user-select: text;
24
+
25
+ color: ${colors.grey900};
26
+ background-color: white;
27
+
28
+ padding: 11px 12px;
29
+ max-height: 40px;
30
+
31
+ outline: 0;
32
+ border: 1px solid ${colors.grey500};
33
+ border-radius: 3px;
34
+ box-shadow: inset 0 0 1px 0 rgba(48, 54, 60, 0.1);
35
+ text-overflow: ellipsis;
36
+ }
37
+
38
+ input.dense {
39
+ max-height: 32px;
40
+ padding: 7px 8px;
41
+ }
42
+
43
+ input:focus {
44
+ outline: none;
45
+ box-shadow: inset 0 0 0 2px ${theme.focus};
46
+ border-color: ${theme.focus};
47
+ }
48
+
49
+ input::placeholder {
50
+ color: ${colors.grey600};
51
+ opacity: 1;
52
+ }
53
+
54
+ input[type='date']::-webkit-inner-spin-button,
55
+ input[type='date']::-webkit-calendar-picker-indicator,
56
+ input[type='time']::-webkit-inner-spin-button,
57
+ input[type='time']::-webkit-calendar-picker-indicator,
58
+ input[type='datetime-local']::-webkit-inner-spin-button,
59
+ input[type='datetime-local']::-webkit-calendar-picker-indicator {
60
+ height: 14px;
61
+ padding-top: 1px;
62
+ padding-bottom: 1px;
63
+ }
64
+
65
+ input[type='date']::-webkit-datetime-edit-fields-wrapper,
66
+ input[type='datetime-local']::-webkit-datetime-edit-fields-wrapper,
67
+ input[type='time']::-webkit-datetime-edit-fields-wrapper {
68
+ padding: 0;
69
+ }
70
+
71
+ input.warning {
72
+ border-color: ${theme.warning};
73
+ }
74
+
75
+ input.error {
76
+ border-color: ${theme.error};
77
+ }
78
+
79
+ input.read-only {
80
+ background-color: ${colors.grey050};
81
+ border-color: ${colors.grey300};
82
+ box-shadow: none;
83
+ cursor: text;
84
+ }
85
+
86
+ input.disabled {
87
+ background-color: ${colors.grey100};
88
+ border-color: ${colors.grey500};
89
+ color: ${theme.disabled};
90
+ cursor: not-allowed;
91
+ }
92
+ `
93
+
94
+ export class Input extends Component {
95
+ static defaultProps = {
96
+ type: 'text',
97
+ dataTest: 'dhis2-uicore-input',
98
+ }
99
+
100
+ inputRef = React.createRef()
101
+
102
+ componentDidMount() {
103
+ if (this.props.initialFocus) {
104
+ this.inputRef.current.focus()
105
+ }
106
+ }
107
+
108
+ handleChange = (e) => {
109
+ if (this.props.onChange) {
110
+ this.props.onChange(this.createHandlerPayload(e), e)
111
+ }
112
+ }
113
+
114
+ handleBlur = (e) => {
115
+ if (this.props.onBlur) {
116
+ this.props.onBlur(this.createHandlerPayload(e), e)
117
+ }
118
+ }
119
+
120
+ handleFocus = (e) => {
121
+ if (this.props.onFocus) {
122
+ this.props.onFocus(this.createHandlerPayload(e), e)
123
+ }
124
+ }
125
+
126
+ handleKeyDown = (e) => {
127
+ if (this.props.onKeyDown) {
128
+ this.props.onKeyDown(this.createHandlerPayload(e), e)
129
+ }
130
+ }
131
+
132
+ handleClear = () => {
133
+ if (this.props.onChange) {
134
+ this.props.onChange({
135
+ value: '',
136
+ name: this.props.name,
137
+ })
138
+ }
139
+ }
140
+
141
+ createHandlerPayload(e) {
142
+ return {
143
+ value: e.target.value,
144
+ name: this.props.name,
145
+ }
146
+ }
147
+
148
+ render() {
149
+ const {
150
+ role,
151
+ ariaLabel,
152
+ ariaControls,
153
+ ariaHaspopup,
154
+ className,
155
+ type = 'text',
156
+ dense,
157
+ disabled,
158
+ readOnly,
159
+ placeholder,
160
+ name,
161
+ valid,
162
+ error,
163
+ warning,
164
+ loading,
165
+ value,
166
+ tabIndex,
167
+ max,
168
+ min,
169
+ step,
170
+ autoComplete,
171
+ dataTest = 'dhis2-uicore-input',
172
+ clearable,
173
+ prefixIcon,
174
+ width,
175
+ } = this.props
176
+
177
+ const statusIcon = error || loading || valid || warning
178
+ const clearButtonPadding = statusIcon ? '40px' : '10px'
179
+
180
+ return (
181
+ <div
182
+ className={cx(
183
+ 'input',
184
+ className,
185
+ { 'input-prefix-icon': prefixIcon },
186
+ { 'input-clearable': clearable }
187
+ )}
188
+ data-test={dataTest}
189
+ >
190
+ {prefixIcon && <span className="prefix">{prefixIcon}</span>}
191
+ <input
192
+ aria-label={ariaLabel}
193
+ aria-controls={ariaControls}
194
+ aria-haspopup={ariaHaspopup}
195
+ role={role}
196
+ id={name}
197
+ name={name}
198
+ placeholder={placeholder}
199
+ ref={this.inputRef}
200
+ type={type}
201
+ value={value}
202
+ max={max}
203
+ min={min}
204
+ step={step}
205
+ disabled={disabled}
206
+ readOnly={readOnly}
207
+ tabIndex={tabIndex}
208
+ autoComplete={autoComplete}
209
+ onFocus={this.handleFocus}
210
+ onBlur={this.handleBlur}
211
+ onChange={this.handleChange}
212
+ onKeyDown={this.handleKeyDown}
213
+ className={cx({
214
+ dense,
215
+ disabled,
216
+ error,
217
+ valid,
218
+ warning,
219
+ 'read-only': readOnly,
220
+ })}
221
+ />
222
+ {clearable && value?.length ? (
223
+ <button
224
+ type="button"
225
+ onClick={this.handleClear}
226
+ className="clear-button"
227
+ >
228
+ <IconCross16 color={colors.white} />
229
+ </button>
230
+ ) : null}
231
+ <StatusIcon
232
+ error={error}
233
+ valid={valid}
234
+ loading={loading}
235
+ warning={warning}
236
+ />
237
+
238
+ <style jsx>{styles}</style>
239
+ <style jsx>{`
240
+ .input {
241
+ width: ${width || `100%`};
242
+ }
243
+
244
+ input {
245
+ width: 100%;
246
+ }
247
+
248
+ .input-prefix-icon input {
249
+ padding-inline-start: 30px;
250
+ }
251
+
252
+ .input-clearable input {
253
+ padding-inline-end: 30px;
254
+ }
255
+
256
+ .prefix {
257
+ position: absolute;
258
+ display: flex;
259
+ align-items: center;
260
+ pointer-events: none;
261
+ inset-inline-start: 10px;
262
+ padding: 0;
263
+ color: ${colors.grey600};
264
+ }
265
+
266
+ .clear-button {
267
+ position: absolute;
268
+ display: flex;
269
+ align-items: center;
270
+ justify-content: center;
271
+ border: none;
272
+ cursor: pointer;
273
+ height: 16px;
274
+ width: 16px;
275
+ border-radius: 50%;
276
+ inset-inline-end: ${clearButtonPadding};
277
+ background: ${colors.grey500};
278
+ padding: 1px;
279
+ }
280
+ `}</style>
281
+ </div>
282
+ )
283
+ }
284
+ }
285
+
286
+ Input.propTypes = {
287
+ /** Add an aria-controls attribute to the input element **/
288
+ ariaControls: PropTypes.string,
289
+ /** Add an aria-haspopup attribute to the input element **/
290
+ ariaHaspopup: PropTypes.string,
291
+ /** Add an aria-label attribute to the input element **/
292
+ ariaLabel: PropTypes.string,
293
+ /** The [native `autocomplete` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-autocomplete) */
294
+ autoComplete: PropTypes.string,
295
+ className: PropTypes.string,
296
+ /** Makes the input field clearable */
297
+ clearable: PropTypes.bool,
298
+ dataTest: PropTypes.string,
299
+ /** Makes the input smaller */
300
+ dense: PropTypes.bool,
301
+ /** Disables the input */
302
+ disabled: PropTypes.bool,
303
+ /** Applies 'error' appearance for validation feedback. Mutually exclusive with `valid` and `warning` props */
304
+ error: sharedPropTypes.statusPropType,
305
+ /** The input grabs initial focus on the page */
306
+ initialFocus: PropTypes.bool,
307
+ /** Adds a loading indicator beside the input */
308
+ loading: PropTypes.bool,
309
+ /** The [native `max` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-max), for use when `type` is `'number'` */
310
+ max: PropTypes.string,
311
+ /** The [native `min` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-min), for use when `type` is `'number'` */
312
+ min: PropTypes.string,
313
+ /** Name associated with the input. Passed to event handler callbacks in object */
314
+ name: PropTypes.string,
315
+ /** Placeholder text for the input */
316
+ placeholder: PropTypes.string,
317
+ /** Add prefix icon */
318
+ prefixIcon: PropTypes.element,
319
+ /** Makes the input read-only */
320
+ readOnly: PropTypes.bool,
321
+ /** Sets a role attribute on the input */
322
+ role: PropTypes.string,
323
+ /** The [native `step` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-step), for use when `type` is `'number'` */
324
+ step: PropTypes.string,
325
+ tabIndex: PropTypes.string,
326
+ /** The native input `type` attribute */
327
+ type: PropTypes.oneOf(inputTypes),
328
+ /** Applies 'valid' appearance for validation feedback. Mutually exclusive with `error` and `warning` props */
329
+ valid: sharedPropTypes.statusPropType,
330
+ /** Value in the input. Can be used to control the component (recommended). Passed to event handler callbacks in object */
331
+ value: PropTypes.string,
332
+ /** Applies 'warning' appearance for validation feedback. Mutually exclusive with `valid` and `error` props */
333
+ warning: sharedPropTypes.statusPropType,
334
+ /** Defines the width of the input. Can be any valid CSS measurement */
335
+ width: PropTypes.string,
336
+ /** Called with signature `({ name: string, value: string }, event)` */
337
+ onBlur: PropTypes.func,
338
+ /** Called with signature `({ name: string, value: string }, event)` */
339
+ onChange: PropTypes.func,
340
+ /** Called with signature `({ name: string, value: string }, event)` */
341
+ onFocus: PropTypes.func,
342
+ /** Called with signature `({ name: string, value: string }, event)` */
343
+ onKeyDown: PropTypes.func,
344
+ }
@@ -0,0 +1,134 @@
1
+ import { sharedPropTypes } from '@dhis2/ui-constants'
2
+ import React from 'react'
3
+ import { Input } from './index.js'
4
+
5
+ const description = `
6
+ An input allows a user to enter data, usually text.
7
+
8
+ Inputs are used wherever a user needs to input standard text information. Inputs are often used as part of forms. An input can also be used to capture information outside of a form, perhaps as a 'Filter' or 'Search' field.
9
+
10
+ To use a label and validation text, consider the \`InputField\` component.
11
+
12
+ Read more about Inputs and InputFields at [Design System: Inputs](https://github.com/dhis2/design-system/blob/master/atoms/inputfield.md).
13
+
14
+ \`\`\`js
15
+ import { Input } from '@dhis/ui'
16
+ \`\`\`
17
+ `
18
+
19
+ const inputTypeArgType = {
20
+ table: { type: { summary: 'string' } },
21
+ control: {
22
+ type: 'select',
23
+ options: [
24
+ 'text',
25
+ 'number',
26
+ 'password',
27
+ 'email',
28
+ 'url',
29
+ 'tel',
30
+ 'date',
31
+ 'datetime',
32
+ 'datetime-local',
33
+ 'month',
34
+ 'week',
35
+ 'time',
36
+ 'search',
37
+ ],
38
+ },
39
+ }
40
+
41
+ const logger = ({ name, value }) =>
42
+ console.log(`Name: ${name}, value: ${value}`)
43
+
44
+ export default {
45
+ title: 'Input',
46
+ component: Input,
47
+ parameters: {
48
+ docs: { description: { component: description } },
49
+ },
50
+ args: {
51
+ name: 'defaultName',
52
+ onChange: logger,
53
+ },
54
+ argTypes: {
55
+ type: { ...inputTypeArgType },
56
+ valid: { ...sharedPropTypes.statusArgType },
57
+ warning: { ...sharedPropTypes.statusArgType },
58
+ error: { ...sharedPropTypes.statusArgType },
59
+ },
60
+ }
61
+
62
+ const Template = (args) => <Input {...args} />
63
+
64
+ export const Default = Template.bind({})
65
+
66
+ export const NoPlaceholderNoValue = Template.bind({})
67
+ NoPlaceholderNoValue.storyName = 'No placeholder, no value'
68
+
69
+ export const PlaceholderNoValue = Template.bind({})
70
+ PlaceholderNoValue.args = { placeholder: 'Hold the place' }
71
+ PlaceholderNoValue.storyName = 'Placeholder, no value'
72
+
73
+ export const WithValue = Template.bind({})
74
+ WithValue.args = {
75
+ value: 'This is set through the value prop, which means the component is controlled.',
76
+ }
77
+
78
+ export const NumberMaxMinStep = Template.bind({})
79
+ NumberMaxMinStep.args = {
80
+ type: 'number',
81
+ max: '3',
82
+ min: '0',
83
+ step: '0.5',
84
+ }
85
+
86
+ export const Focus = Template.bind({})
87
+ Focus.args = { initialFocus: true }
88
+ // Disabled initial focus stories on docs page
89
+ Focus.parameters = { docs: { disable: true } }
90
+
91
+ export const StatusValid = Template.bind({})
92
+ StatusValid.args = { valid: true, value: 'This value is valid' }
93
+ StatusValid.storyName = 'Status: Valid'
94
+
95
+ export const StatusWarning = Template.bind({})
96
+ StatusWarning.args = { warning: true, value: 'This value produces a warning' }
97
+ StatusWarning.storyName = 'Status: Warning'
98
+
99
+ export const StatusError = Template.bind({})
100
+ StatusError.args = { error: true, value: 'This value produces an error' }
101
+ StatusError.storyName = 'Status: Error'
102
+
103
+ export const StatusLoading = Template.bind({})
104
+ StatusLoading.args = {
105
+ loading: true,
106
+ value: 'This value produces a loading state',
107
+ }
108
+ StatusLoading.storyName = 'Status: Loading'
109
+
110
+ export const Disabled = Template.bind({})
111
+ Disabled.args = { disabled: true, value: 'This field is disabled' }
112
+
113
+ export const ReadOnly = Template.bind({})
114
+ ReadOnly.args = { readOnly: true, value: 'This field is read-only' }
115
+
116
+ export const Dense = Template.bind({})
117
+ Dense.args = { dense: true, value: 'This field is dense' }
118
+
119
+ export const ValueTextOverflow = Template.bind({})
120
+ ValueTextOverflow.args = {
121
+ value: "This value is too long in order to show on a single line of the input field. It should stay on one line, not in an extra line and which wouldn't look like a standard input",
122
+ dense: true,
123
+ warning: true,
124
+ }
125
+
126
+ export const RTLErrorPlaceholder = (args) => (
127
+ <div dir="rtl">
128
+ <Input {...args} />
129
+ </div>
130
+ )
131
+ RTLErrorPlaceholder.args = {
132
+ error: true,
133
+ placeholder: 'RTL placeholder',
134
+ }
@@ -0,0 +1,15 @@
1
+ export const inputTypes = [
2
+ 'text',
3
+ 'number',
4
+ 'password',
5
+ 'email',
6
+ 'url',
7
+ 'tel',
8
+ 'date',
9
+ 'datetime',
10
+ 'datetime-local',
11
+ 'month',
12
+ 'week',
13
+ 'time',
14
+ 'search',
15
+ ]
@@ -0,0 +1,27 @@
1
+ import { render, fireEvent, screen } from '@testing-library/react'
2
+ import React from 'react'
3
+ import { InputField } from '../input-field.js'
4
+
5
+ describe('<Input>', () => {
6
+ it('should call the onKeyDown callback when provided', () => {
7
+ const onKeyDown = jest.fn()
8
+
9
+ render(
10
+ <InputField
11
+ label="label"
12
+ name="foo"
13
+ value="bar"
14
+ onKeyDown={onKeyDown}
15
+ />
16
+ )
17
+
18
+ fireEvent.keyDown(screen.getByRole('textbox'), {})
19
+
20
+ expect(onKeyDown).toHaveBeenCalledWith(
21
+ { name: 'foo', value: 'bar' },
22
+ expect.objectContaining({})
23
+ )
24
+
25
+ expect(onKeyDown).toHaveBeenCalledTimes(1)
26
+ })
27
+ })
@@ -0,0 +1,11 @@
1
+ import { Given, Then } from '@badeball/cypress-cucumber-preprocessor'
2
+
3
+ Given('a InputField with label and a required flag is rendered', () => {
4
+ cy.visitStory('InputField', 'With label and required')
5
+ })
6
+
7
+ Then('the required indicator is visible', () => {
8
+ cy.get('[data-test="dhis2-uiwidgets-inputfield-label-required"]').should(
9
+ 'be.visible'
10
+ )
11
+ })
@@ -0,0 +1,5 @@
1
+ Feature: Required status for the InputField
2
+
3
+ Scenario: Rendering a InputField that is required
4
+ Given a InputField with label and a required flag is rendered
5
+ Then the required indicator is visible
@@ -0,0 +1 @@
1
+ export { InputField, InputFieldProps } from './input-field.js'
@@ -0,0 +1,7 @@
1
+ import React from 'react'
2
+ import { InputField } from './index.js'
3
+
4
+ export default { title: 'InputField' }
5
+ export const WithLabelAndRequired = () => (
6
+ <InputField label="Default label" name="Default" required />
7
+ )
@@ -0,0 +1,156 @@
1
+ import { sharedPropTypes } from '@dhis2/ui-constants'
2
+ import { Box } from '@dhis2-ui/box'
3
+ import { Field } from '@dhis2-ui/field'
4
+ import PropTypes from 'prop-types'
5
+ import React from 'react'
6
+ import { Input, inputTypes } from '../input/index.js'
7
+
8
+ class InputField extends React.Component {
9
+ static defaultProps = {
10
+ dataTest: 'dhis2-uiwidgets-inputfield',
11
+ }
12
+
13
+ render() {
14
+ const {
15
+ className,
16
+ onChange,
17
+ onFocus,
18
+ onKeyDown,
19
+ onBlur,
20
+ initialFocus,
21
+ type,
22
+ dense,
23
+ required,
24
+ label,
25
+ disabled,
26
+ readOnly,
27
+ placeholder,
28
+ name,
29
+ max,
30
+ min,
31
+ step,
32
+ valid,
33
+ error,
34
+ warning,
35
+ loading,
36
+ value,
37
+ tabIndex,
38
+ helpText,
39
+ validationText,
40
+ inputWidth,
41
+ autoComplete,
42
+ clearable,
43
+ prefixIcon,
44
+ dataTest = 'dhis2-uiwidgets-inputfield',
45
+ } = this.props
46
+
47
+ return (
48
+ <Field
49
+ className={className}
50
+ dataTest={dataTest}
51
+ error={error}
52
+ warning={warning}
53
+ valid={valid}
54
+ helpText={helpText}
55
+ validationText={validationText}
56
+ label={label}
57
+ name={name}
58
+ disabled={disabled}
59
+ required={required}
60
+ >
61
+ <Box width={inputWidth} minWidth="72px">
62
+ <Input
63
+ onFocus={onFocus}
64
+ onKeyDown={onKeyDown}
65
+ onBlur={onBlur}
66
+ onChange={onChange}
67
+ name={name}
68
+ type={type}
69
+ value={value || ''}
70
+ placeholder={placeholder}
71
+ disabled={disabled}
72
+ max={max}
73
+ min={min}
74
+ step={step}
75
+ valid={valid}
76
+ warning={warning}
77
+ error={error}
78
+ loading={loading}
79
+ dense={dense}
80
+ tabIndex={tabIndex}
81
+ initialFocus={initialFocus}
82
+ readOnly={readOnly}
83
+ autoComplete={autoComplete}
84
+ clearable={clearable}
85
+ prefixIcon={prefixIcon}
86
+ width={inputWidth}
87
+ />
88
+ </Box>
89
+ </Field>
90
+ )
91
+ }
92
+ }
93
+
94
+ const InputFieldProps = {
95
+ /** The [native `autocomplete` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-autocomplete) */
96
+ autoComplete: PropTypes.string,
97
+ className: PropTypes.string,
98
+ /** Makes the input field clearable */
99
+ clearable: PropTypes.bool,
100
+ dataTest: PropTypes.string,
101
+ /** Makes the input smaller */
102
+ dense: PropTypes.bool,
103
+ /** Disables the input */
104
+ disabled: PropTypes.bool,
105
+ /** Applies 'error' appearance for validation feedback. Mutually exclusive with `valid` and `warning` props */
106
+ error: sharedPropTypes.statusPropType,
107
+ /** Guiding text for how to use this input */
108
+ helpText: PropTypes.string,
109
+ /** The input grabs initial focus on the page */
110
+ initialFocus: PropTypes.bool,
111
+ /** Defines the width of the input. Can be any valid CSS measurement */
112
+ inputWidth: PropTypes.string,
113
+ /** Label text for the input */
114
+ label: PropTypes.string,
115
+ /** Adds a loading indicator beside the input */
116
+ loading: PropTypes.bool,
117
+ /** The [native `max` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-max), for use when `type` is `'number'` */
118
+ max: PropTypes.string,
119
+ /** The [native `min` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-min), for use when `type` is `'number'` */
120
+ min: PropTypes.string,
121
+ /** Name associated with the input. Passed to event handler callbacks in object */
122
+ name: PropTypes.string,
123
+ /** Placeholder text for the input */
124
+ placeholder: PropTypes.string,
125
+ /** Add prefix icon */
126
+ prefixIcon: PropTypes.element,
127
+ /** Makes the input read-only */
128
+ readOnly: PropTypes.bool,
129
+ /** Indicates this input is required */
130
+ required: PropTypes.bool,
131
+ /** The [native `step` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-step), for use when `type` is `'number'` */
132
+ step: PropTypes.string,
133
+ tabIndex: PropTypes.string,
134
+ /** Type of input */
135
+ type: PropTypes.oneOf(inputTypes),
136
+ /** Applies 'valid' appearance for validation feedback. Mutually exclusive with `error` and `warning` props */
137
+ valid: sharedPropTypes.statusPropType,
138
+ /** Text below input for validation feedback. Receives styles depending on validation status */
139
+ validationText: PropTypes.string,
140
+ /** Value in the input. Can be used to control the component (recommended). Passed to event handler callbacks in object */
141
+ value: PropTypes.string,
142
+ /** Applies 'warning' appearance for validation feedback. Mutually exclusive with `valid` and `error` props */
143
+ warning: sharedPropTypes.statusPropType,
144
+ /** Called with signature `({ name: string, value: string }, event)` */
145
+ onBlur: PropTypes.func,
146
+ /** Called with signature `({ name: string, value: string }, event)` */
147
+ onChange: PropTypes.func,
148
+ /** Called with signature `({ name: string, value: string }, event)` */
149
+ onFocus: PropTypes.func,
150
+ /** Called with signature `({ name: string, value: string }, event)` */
151
+ onKeyDown: PropTypes.func,
152
+ }
153
+
154
+ InputField.propTypes = InputFieldProps
155
+
156
+ export { InputField, InputFieldProps }
@@ -0,0 +1,195 @@
1
+ import { sharedPropTypes } from '@dhis2/ui-constants'
2
+ import { IconLocation16, IconSearch16 } from '@dhis2/ui-icons'
3
+ import React, { useState } from 'react'
4
+ import { InputField } from './index.js'
5
+
6
+ const subtitle = 'Allows a user to enter data, usually text'
7
+
8
+ const description = `
9
+ Inputs are used wherever a user needs to input standard text information. Inputs are often used as part of forms. An input can also be used to capture information outside of a form, perhaps as a 'Filter' or 'Search' field.
10
+
11
+ InputField wraps an Input component with a label, help text, validation text, and some other features.
12
+
13
+ Please see more about options and features of inputs at [Design System: Input Field](https://github.com/dhis2/design-system/blob/master/atoms/inputfield.md#input).
14
+
15
+ \`\`\`js
16
+ import { InputField } from '@dhis2/ui'
17
+ \`\`\`
18
+ `
19
+
20
+ const logger = ({ name, value }) =>
21
+ console.log(`Name: ${name}, value: ${value}`)
22
+
23
+ const inputTypeArgType = {
24
+ table: { type: { summary: 'string' } },
25
+ control: {
26
+ type: 'select',
27
+ options: [
28
+ 'text',
29
+ 'number',
30
+ 'password',
31
+ 'email',
32
+ 'url',
33
+ 'tel',
34
+ 'date',
35
+ 'datetime',
36
+ 'datetime-local',
37
+ 'month',
38
+ 'week',
39
+ 'time',
40
+ 'search',
41
+ ],
42
+ },
43
+ }
44
+
45
+ export default {
46
+ title: 'Input Field',
47
+ component: InputField,
48
+ parameters: {
49
+ componentSubtitle: subtitle,
50
+ docs: { description: { component: description } },
51
+ },
52
+ // Default args
53
+ args: {
54
+ label: 'Default label',
55
+ name: 'defaultName',
56
+ onChange: logger,
57
+ },
58
+ argTypes: {
59
+ type: { ...inputTypeArgType },
60
+ valid: { ...sharedPropTypes.statusArgType },
61
+ warning: { ...sharedPropTypes.statusArgType },
62
+ error: { ...sharedPropTypes.statusArgType },
63
+ },
64
+ }
65
+
66
+ const Template = (args) => <InputField {...args} />
67
+
68
+ export const Default = Template.bind({})
69
+
70
+ export const NoPlaceholderNoValue = Template.bind({})
71
+ NoPlaceholderNoValue.storyName = 'No placeholder, no value'
72
+
73
+ export const PlaceholderNoValue = Template.bind({})
74
+ PlaceholderNoValue.args = { placeholder: 'Hold the place' }
75
+ PlaceholderNoValue.storyName = 'Placeholder, no value'
76
+
77
+ export const WithHelpText = Template.bind({})
78
+ WithHelpText.args = {
79
+ ...PlaceholderNoValue.args,
80
+ helpText: 'With some helping text to guide the user along',
81
+ }
82
+
83
+ export const WithValue = Template.bind({})
84
+ WithValue.args = {
85
+ value: 'This is set through the value prop, which means the component is controlled.',
86
+ }
87
+
88
+ export const Focus = Template.bind({})
89
+ Focus.args = { initialFocus: true }
90
+ // Disabled initial focus stories on docs page
91
+ Focus.parameters = { docs: { disable: true } }
92
+
93
+ export const StatusValid = Template.bind({})
94
+ StatusValid.args = { valid: true, value: 'This value is valid' }
95
+ StatusValid.storyName = 'Status: Valid'
96
+
97
+ export const StatusWarning = Template.bind({})
98
+ StatusWarning.args = { warning: true, value: 'This value produces a warning' }
99
+ StatusWarning.storyName = 'Status: Warning'
100
+
101
+ export const StatusError = Template.bind({})
102
+ StatusError.args = {
103
+ error: true,
104
+ value: 'This value produces an error',
105
+ helpText: 'This is some help text to advise what this input actually is.',
106
+ validationText:
107
+ 'This validation text describes the error, if a message is supplied.',
108
+ }
109
+ StatusError.storyName = 'Status: Error'
110
+
111
+ export const StatusLoading = Template.bind({})
112
+ StatusLoading.args = {
113
+ loading: true,
114
+ value: 'This value produces a loading state',
115
+ }
116
+ StatusLoading.storyName = 'Status: Loading'
117
+
118
+ export const Disabled = Template.bind({})
119
+ Disabled.args = { disabled: true, value: 'This field is disabled' }
120
+
121
+ export const ReadOnly = Template.bind({})
122
+ ReadOnly.args = { readOnly: true, value: 'This field is read-only' }
123
+
124
+ export const Dense = Template.bind({})
125
+ Dense.args = { dense: true, value: 'This field is dense' }
126
+
127
+ export const InputWidth = (args) => (
128
+ <>
129
+ <InputField
130
+ {...args}
131
+ name="input1"
132
+ label="My inputField has a width of 100px"
133
+ inputWidth="100px"
134
+ />
135
+ <InputField
136
+ {...args}
137
+ name="input2"
138
+ label="My inputField has a width of 220px"
139
+ inputWidth="220px"
140
+ />
141
+ </>
142
+ )
143
+
144
+ export const LabelTextOverflow = Template.bind({})
145
+ LabelTextOverflow.args = {
146
+ dense: true,
147
+ warning: true,
148
+ label: "This label is too long to show on a single line of the input field's label. We just let it flow to the next line so the user can still read it. However, we should always aim to keep it shorter than this!",
149
+ }
150
+
151
+ export const ValueTextOverflow = Template.bind({})
152
+ ValueTextOverflow.args = {
153
+ value: "This value is too long in order to show on a single line of the input field. It should stay on one line, not in an extra line and which wouldn't look like a standard input",
154
+ dense: true,
155
+ warning: true,
156
+ }
157
+
158
+ export const Required = Template.bind({})
159
+ Required.args = { required: true }
160
+
161
+ export const InputWithPrefixIcon = (args) => (
162
+ <>
163
+ <InputField
164
+ {...args}
165
+ name="prefix-icon-input"
166
+ label="Search"
167
+ placeholder={'Search'}
168
+ prefixIcon={<IconSearch16 />}
169
+ />
170
+ <InputField
171
+ {...args}
172
+ name="prefix-icon-input"
173
+ label="Location"
174
+ placeholder={'Enter Location'}
175
+ prefixIcon={<IconLocation16 />}
176
+ inputWidth={'200px'}
177
+ />
178
+ </>
179
+ )
180
+
181
+ export const ClearableInput = (args) => {
182
+ const [value, setValue] = useState('value')
183
+ return (
184
+ <InputField
185
+ {...args}
186
+ name="clearable-input"
187
+ label="This field can be cleared"
188
+ placeholder={''}
189
+ onChange={(e) => setValue(e.value)}
190
+ clearable
191
+ clearText={() => setValue('')}
192
+ value={value}
193
+ />
194
+ )
195
+ }