@dhis2-ui/text-area 10.16.2 → 10.16.3

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/text-area",
3
- "version": "10.16.2",
3
+ "version": "10.16.3",
4
4
  "description": "UI TextArea",
5
5
  "repository": {
6
6
  "type": "git",
@@ -33,18 +33,19 @@
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/loader": "10.16.2",
39
- "@dhis2-ui/status-icon": "10.16.2",
40
- "@dhis2/ui-constants": "10.16.2",
41
- "@dhis2/ui-icons": "10.16.2",
36
+ "@dhis2-ui/box": "10.16.3",
37
+ "@dhis2-ui/field": "10.16.3",
38
+ "@dhis2-ui/loader": "10.16.3",
39
+ "@dhis2-ui/status-icon": "10.16.3",
40
+ "@dhis2/ui-constants": "10.16.3",
41
+ "@dhis2/ui-icons": "10.16.3",
42
42
  "classnames": "^2.3.1",
43
43
  "prop-types": "^15.7.2"
44
44
  },
45
45
  "files": [
46
46
  "build",
47
- "types"
47
+ "types",
48
+ "src"
48
49
  ],
49
50
  "devDependencies": {
50
51
  "react": "^18.3.1",
package/src/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { TextArea } from './text-area/index.js'
2
+ export { TextAreaField } from './text-area-field/index.js'
@@ -0,0 +1,20 @@
1
+ import { render, fireEvent, screen } from '@testing-library/react'
2
+ import React from 'react'
3
+ import { TextArea } from '../text-area.js'
4
+
5
+ describe('<TextArea>', () => {
6
+ it('should call the onKeyDown callback when provided', () => {
7
+ const onKeyDown = jest.fn()
8
+
9
+ render(<TextArea name="foo" value="bar" onKeyDown={onKeyDown} />)
10
+
11
+ fireEvent.keyDown(screen.getByRole('textbox'), {})
12
+
13
+ expect(onKeyDown).toHaveBeenCalledWith(
14
+ { name: 'foo', value: 'bar' },
15
+ expect.objectContaining({})
16
+ )
17
+
18
+ expect(onKeyDown).toHaveBeenCalledTimes(1)
19
+ })
20
+ })
@@ -0,0 +1,9 @@
1
+ import { Given, Then } from '@badeball/cypress-cucumber-preprocessor'
2
+
3
+ Given('a TextArea with initialFocus is rendered', () => {
4
+ cy.visitStory('TextArea', 'With initial focus')
5
+ })
6
+
7
+ Then('the TextArea is focused', () => {
8
+ cy.focused().parent('[data-test="dhis2-uicore-textarea"]').should('exist')
9
+ })
@@ -0,0 +1,5 @@
1
+ Feature: Focusing the TextArea on mount
2
+
3
+ Scenario: The TextArea renders with focus
4
+ Given a TextArea with initialFocus is rendered
5
+ Then the TextArea is focused
@@ -0,0 +1,18 @@
1
+ import { Given, When, Then } from '@badeball/cypress-cucumber-preprocessor'
2
+
3
+ Given('a TextArea with initialFocus and onBlur handler is rendered', () => {
4
+ cy.visitStory('TextArea', 'With initial focus and on blur')
5
+ })
6
+
7
+ When('the TextArea is blurred', () => {
8
+ cy.get('[data-test="dhis2-uicore-textarea"] textarea').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: 'textarea',
16
+ })
17
+ })
18
+ })
@@ -0,0 +1,6 @@
1
+ Feature: The TextArea has an onBlur api
2
+
3
+ Scenario: The user blurs the TextArea
4
+ Given a TextArea with initialFocus and onBlur handler is rendered
5
+ When the TextArea 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 TextArea with onChange handler is rendered', () => {
4
+ cy.visitStory('TextArea', 'With on change')
5
+ })
6
+
7
+ When('the TextArea is filled with a character', () => {
8
+ cy.get('[data-test="dhis2-uicore-textarea"]').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: 'textarea',
16
+ })
17
+ })
18
+ })
@@ -0,0 +1,6 @@
1
+ Feature: The TextArea has an onChange api
2
+
3
+ Scenario: The user types a character into the TextArea
4
+ Given a TextArea with onChange handler is rendered
5
+ When the TextArea is filled with a character
6
+ Then the onChange handler is called
@@ -0,0 +1,15 @@
1
+ import { Given, When, Then } from '@badeball/cypress-cucumber-preprocessor'
2
+
3
+ Given('a disabled TextArea is rendered', () => {
4
+ cy.visitStory('TextArea', 'With disabled')
5
+ })
6
+
7
+ When('the user clicks the TextArea', () => {
8
+ cy.get('[data-test="dhis2-uicore-textarea"] textarea').click({
9
+ force: true,
10
+ })
11
+ })
12
+
13
+ Then('the TextArea is not focused', () => {
14
+ cy.focused().should('not.exist')
15
+ })
@@ -0,0 +1,6 @@
1
+ Feature: The TextArea can be disabled
2
+
3
+ Scenario: The user clicks a disabled TextArea
4
+ Given a disabled TextArea is rendered
5
+ When the user clicks the TextArea
6
+ Then the TextArea is not focused
@@ -0,0 +1,18 @@
1
+ import { Given, When, Then } from '@badeball/cypress-cucumber-preprocessor'
2
+
3
+ Given('a TextArea with onFocus handler is rendered', () => {
4
+ cy.visitStory('TextArea', 'With on focus')
5
+ })
6
+
7
+ When('the TextArea is focused', () => {
8
+ cy.get('[data-test="dhis2-uicore-textarea"] textarea').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: 'textarea',
16
+ })
17
+ })
18
+ })
@@ -0,0 +1,6 @@
1
+ Feature: The TextArea has an onFocus api
2
+
3
+ Scenario: The user focuses the TextArea
4
+ Given a TextArea with onFocus handler is rendered
5
+ When the TextArea is focused
6
+ Then the onFocus handler is called
@@ -0,0 +1,5 @@
1
+ import { Given } from '@badeball/cypress-cucumber-preprocessor'
2
+
3
+ Given('an empty TextArea is rendered', () => {
4
+ cy.visitStory('TextArea', 'Empty')
5
+ })
@@ -0,0 +1 @@
1
+ export { TextArea } from './text-area.js'
@@ -0,0 +1,19 @@
1
+ import React from 'react'
2
+ import { TextArea } 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: 'TextArea' }
9
+ export const WithOnChange = () => (
10
+ <TextArea onChange={window.onChange} name="textarea" />
11
+ )
12
+ export const WithInitialFocusAndOnBlur = () => (
13
+ <TextArea initialFocus name="textarea" onBlur={window.onBlur} />
14
+ )
15
+ export const WithOnFocus = () => (
16
+ <TextArea name="textarea" onFocus={window.onFocus} />
17
+ )
18
+ export const WithInitialFocus = () => <TextArea name="textarea" initialFocus />
19
+ export const WithDisabled = () => <TextArea name="textarea" disabled />
@@ -0,0 +1,224 @@
1
+ import { sharedPropTypes } from '@dhis2/ui-constants'
2
+ import { StatusIcon } from '@dhis2-ui/status-icon'
3
+ import cx from 'classnames'
4
+ import PropTypes from 'prop-types'
5
+ import React, { Component } from 'react'
6
+ import { styles } from './text-area.styles.js'
7
+
8
+ export class TextArea extends Component {
9
+ textareaRef = React.createRef()
10
+ state = {
11
+ height: 'auto',
12
+ }
13
+ textareaDimensions = { width: 0, height: 0 }
14
+ userHasResized = false
15
+
16
+ componentDidMount() {
17
+ this.attachResizeListener()
18
+
19
+ if (this.props.initialFocus) {
20
+ this.textareaRef.current.focus()
21
+ }
22
+
23
+ if (this.shouldDoAutoGrow()) {
24
+ this.setHeight()
25
+ }
26
+ }
27
+
28
+ componentDidUpdate(prevProps) {
29
+ if (this.shouldDoAutoGrow() && this.props.value !== prevProps.value) {
30
+ this.setHeight()
31
+ }
32
+ }
33
+
34
+ attachResizeListener() {
35
+ const textarea = this.textareaRef.current
36
+ textarea.addEventListener('mousedown', this.setTextareaDimensions)
37
+ textarea.addEventListener('mouseup', this.hasUserResized)
38
+ }
39
+
40
+ removeResizeListener() {
41
+ const textarea = this.textareaRef.current
42
+ textarea.removeEventListener('mousedown', this.setTextareaDimensions)
43
+ textarea.removeEventListener('mouseup', this.hasUserResized)
44
+ }
45
+
46
+ setHeight() {
47
+ const textarea = this.textareaRef.current
48
+ const offset = textarea.offsetHeight - textarea.clientHeight
49
+ const height = textarea.scrollHeight + offset + 'px'
50
+ this.setState({ height })
51
+ }
52
+
53
+ setTextareaDimensions = () => {
54
+ const textarea = this.textareaRef.current
55
+ this.textareaDimensions = {
56
+ width: textarea.clientWidth,
57
+ height: textarea.clientHeight,
58
+ }
59
+ }
60
+
61
+ shouldDoAutoGrow() {
62
+ return this.props.autoGrow && !this.userHasResized
63
+ }
64
+
65
+ hasUserResized = () => {
66
+ const { width: oldWidth, height: oldHeight } = this.textareaDimensions
67
+
68
+ this.setTextareaDimensions()
69
+
70
+ const { width: newWidth, height: newHeight } = this.textareaDimensions
71
+ const userHasResized = newWidth !== oldWidth || newHeight !== oldHeight
72
+
73
+ if (userHasResized) {
74
+ this.userHasResized = true
75
+ this.removeResizeListener()
76
+ }
77
+ }
78
+
79
+ handleChange = (e) => {
80
+ if (this.props.onChange) {
81
+ this.props.onChange(this.createHandlerPayload(e), e)
82
+ }
83
+ }
84
+
85
+ handleBlur = (e) => {
86
+ if (this.props.onBlur) {
87
+ this.props.onBlur(this.createHandlerPayload(e), e)
88
+ }
89
+ }
90
+
91
+ handleFocus = (e) => {
92
+ if (this.props.onFocus) {
93
+ this.props.onFocus(this.createHandlerPayload(e), e)
94
+ }
95
+ }
96
+
97
+ handleKeyDown = (e) => {
98
+ if (this.props.onKeyDown) {
99
+ this.props.onKeyDown(this.createHandlerPayload(e), e)
100
+ }
101
+ }
102
+
103
+ createHandlerPayload(e) {
104
+ return {
105
+ value: e.target.value,
106
+ name: this.props.name,
107
+ }
108
+ }
109
+ static defaultProps = {
110
+ rows: 4,
111
+ width: '100%',
112
+ resize: 'vertical',
113
+ dataTest: 'dhis2-uicore-textarea',
114
+ }
115
+
116
+ render() {
117
+ const {
118
+ className,
119
+ dense,
120
+ disabled,
121
+ readOnly,
122
+ placeholder,
123
+ name,
124
+ valid,
125
+ error,
126
+ warning,
127
+ loading,
128
+ value,
129
+ tabIndex,
130
+ resize = 'vertical',
131
+ rows = 4,
132
+ width = '100%',
133
+ dataTest = 'dhis2-uicore-textarea',
134
+ } = this.props
135
+ const { height } = this.state
136
+
137
+ return (
138
+ <div className={cx('textarea', className)} data-test={dataTest}>
139
+ <textarea
140
+ id={name}
141
+ name={name}
142
+ placeholder={placeholder}
143
+ ref={this.textareaRef}
144
+ value={value}
145
+ disabled={disabled}
146
+ readOnly={readOnly}
147
+ tabIndex={tabIndex}
148
+ onFocus={this.handleFocus}
149
+ onKeyDown={this.handleKeyDown}
150
+ onBlur={this.handleBlur}
151
+ onChange={this.handleChange}
152
+ rows={rows}
153
+ className={cx({
154
+ dense,
155
+ disabled,
156
+ error,
157
+ valid,
158
+ warning,
159
+ 'read-only': readOnly,
160
+ })}
161
+ />
162
+ <StatusIcon
163
+ error={error}
164
+ valid={valid}
165
+ loading={loading}
166
+ warning={warning}
167
+ />
168
+
169
+ <style jsx>{styles}</style>
170
+ <style jsx>{`
171
+ textarea {
172
+ width: ${width};
173
+ height: ${height};
174
+ resize: ${resize};
175
+ }
176
+ `}</style>
177
+ </div>
178
+ )
179
+ }
180
+ }
181
+
182
+ TextArea.propTypes = {
183
+ /** Grow the text area in response to overflow instead of adding a scroll bar */
184
+ autoGrow: PropTypes.bool,
185
+ className: PropTypes.string,
186
+ dataTest: PropTypes.string,
187
+ /** Compact mode */
188
+ dense: PropTypes.bool,
189
+ /** Disables the textarea and makes in non-interactive */
190
+ disabled: PropTypes.bool,
191
+ /** Applies 'error' styles for validation feedback. Mutually exclusive with `valid` and `warning` props */
192
+ error: sharedPropTypes.statusPropType,
193
+ /** Grabs initial focus on the page */
194
+ initialFocus: PropTypes.bool,
195
+ /** Adds a loading spinner */
196
+ loading: PropTypes.bool,
197
+ /** Name associated with the text area. Passed in object argument to event handlers. */
198
+ name: PropTypes.string,
199
+ /** Placeholder text for an empty textarea */
200
+ placeholder: PropTypes.string,
201
+ /** Makes the textarea read-only */
202
+ readOnly: PropTypes.bool,
203
+ /** [Resize property](https://developer.mozilla.org/en-US/docs/Web/CSS/resize) for the textarea element */
204
+ resize: PropTypes.oneOf(['none', 'both', 'horizontal', 'vertical']),
205
+ /** Initial height of the textarea, in lines of text */
206
+ rows: PropTypes.number,
207
+ tabIndex: PropTypes.string,
208
+ /** Applies 'valid' styles for validation feedback. Mutually exclusive with `warning` and `error` props */
209
+ valid: sharedPropTypes.statusPropType,
210
+ /** Value in the textarea. Can be used to control component (recommended). Passed in object argument to event handlers. */
211
+ value: PropTypes.string,
212
+ /** Applies 'warning' styles for validation feedback. Mutually exclusive with `valid` and `error` props */
213
+ warning: sharedPropTypes.statusPropType,
214
+ /** Width of the text area. Can be any valid CSS measurement */
215
+ width: PropTypes.string,
216
+ /** Called with signature `({ name: string, value: string }, event)` */
217
+ onBlur: PropTypes.func,
218
+ /** Called with signature `({ name: string, value: string }, event)` */
219
+ onChange: PropTypes.func,
220
+ /** Called with signature `({ name: string, value: string }, event)` */
221
+ onFocus: PropTypes.func,
222
+ /** Called with signature `({ name: string, value: string }, event)` */
223
+ onKeyDown: PropTypes.func,
224
+ }
@@ -0,0 +1,219 @@
1
+ import { sharedPropTypes } from '@dhis2/ui-constants'
2
+ import React from 'react'
3
+ import { TextArea } from './index.js'
4
+
5
+ const description = `
6
+ A textarea allows multiple lines of text input. Use a textarea wherever a user needs to input a lot of information. Do not use a textarea if a short, single line of content is expected.
7
+
8
+ Options for textarea inputs are:
9
+
10
+ - Rows: the height of the input, defined by the number of rows of text
11
+ - Resizable: whether the textarea can be resized by the user or not. Can be set for both width and height.
12
+ - Autoheight: if enabled, the texarea will grow in height to adapt to the content.
13
+
14
+ \`\`\`js
15
+ import { TextArea } from '@dhis2/ui'
16
+ \`\`\`
17
+ `
18
+
19
+ window.onChange = (payload, event) => {
20
+ console.log('onChange payload', payload)
21
+ console.log('onChange event', event)
22
+ }
23
+
24
+ window.onFocus = (payload, event) => {
25
+ console.log('onFocus payload', payload)
26
+ console.log('onFocus event', event)
27
+ }
28
+
29
+ window.onBlur = (payload, event) => {
30
+ console.log('onBlur payload', payload)
31
+ console.log('onBlur event', event)
32
+ }
33
+
34
+ const onChange = (...args) => window.onChange(...args)
35
+ const onFocus = (...args) => window.onFocus(...args)
36
+ const onBlur = (...args) => window.onBlur(...args)
37
+
38
+ export default {
39
+ title: 'Text Area',
40
+ component: TextArea,
41
+ parameters: { docs: { description: { component: description } } },
42
+ argTypes: {
43
+ valid: { ...sharedPropTypes.statusArgType },
44
+ error: { ...sharedPropTypes.statusArgType },
45
+ warning: { ...sharedPropTypes.statusArgType },
46
+ },
47
+ args: {
48
+ name: 'textAreaName',
49
+ onChange,
50
+ onFocus,
51
+ onBlur,
52
+ },
53
+ }
54
+
55
+ const Template = (args) => <TextArea {...args} />
56
+
57
+ export const Default = Template.bind({})
58
+
59
+ export const PlaceholderNoValue = Template.bind({})
60
+ PlaceholderNoValue.args = { placeholder: 'Hold the place' }
61
+ PlaceholderNoValue.storyName = 'Placeholder, no value'
62
+
63
+ export const WithValue = Template.bind({})
64
+ WithValue.args = {
65
+ value: 'This is set through the value prop, which means the component is controlled.',
66
+ }
67
+ WithValue.storyName = 'With value'
68
+
69
+ export const Focus = (args) => (
70
+ <>
71
+ <TextArea {...args} initialFocus className="initially-focused" />
72
+ <TextArea {...args} className="initially-unfocused" />
73
+ </>
74
+ )
75
+ Focus.parameters = { docs: { disable: true } }
76
+
77
+ export const StatusValid = Template.bind({})
78
+ StatusValid.args = { valid: true, value: 'This value is valid' }
79
+ StatusValid.storyName = 'Status: Valid'
80
+
81
+ export const StatusWarning = Template.bind({})
82
+ StatusWarning.args = { warning: true, value: 'This value produces a warning' }
83
+ StatusWarning.storyName = 'Status: Warning'
84
+
85
+ export const StatusError = Template.bind({})
86
+ StatusError.args = {
87
+ error: true,
88
+ value: 'This value produces an error',
89
+ helpText: 'This is some help text to advise what this input actually is.',
90
+ validationText: 'This describes the error, if a message is supplied.',
91
+ }
92
+ StatusError.storyName = 'Status: Error'
93
+
94
+ export const StatusLoading = Template.bind({})
95
+ StatusLoading.args = {
96
+ loading: true,
97
+ value: 'This value produces a loadingn state',
98
+ }
99
+ StatusLoading.storyName = 'Status: Loading'
100
+
101
+ export const Disabled = Template.bind({})
102
+ Disabled.args = { disabled: true, value: 'This field is disabled' }
103
+
104
+ export const ReadOnly = Template.bind({})
105
+ ReadOnly.args = { readOnly: true, value: 'This field is readOnly' }
106
+
107
+ export const Dense = Template.bind({})
108
+ Dense.args = { dense: true, value: 'This field is dense' }
109
+
110
+ export const TextareaTextOverflow = Template.bind({})
111
+ TextareaTextOverflow.args = {
112
+ label: 'I have a scrollbar',
113
+ value: [
114
+ 'A line of text',
115
+ 'A line of text',
116
+ 'A line of text',
117
+ 'A line of text',
118
+ 'A line of text',
119
+ 'A line of text',
120
+ 'A line of text',
121
+ 'A line of text',
122
+ 'A line of text',
123
+ 'A line of text',
124
+ 'A line of text',
125
+ 'A line of text',
126
+ 'A line of text',
127
+ 'A line of text',
128
+ 'A line of text',
129
+ ].join('\n'),
130
+ }
131
+
132
+ export const Rows = Template.bind({})
133
+ Rows.args = {
134
+ rows: 8,
135
+ label: 'You can set the height with the rows prop. I have 8',
136
+ }
137
+
138
+ export const Resize = (args) => (
139
+ <>
140
+ <TextArea
141
+ {...args}
142
+ name="textarea1"
143
+ label="Resize: vertical (default)"
144
+ />
145
+ <TextArea
146
+ {...args}
147
+ name="textarea2"
148
+ label="Resize: none"
149
+ resize="none"
150
+ />
151
+ <TextArea
152
+ {...args}
153
+ name="textarea3"
154
+ label="Resize: both"
155
+ resize="both"
156
+ />
157
+ <TextArea
158
+ {...args}
159
+ name="textarea4"
160
+ label="Resize: horizontal"
161
+ resize="horizontal"
162
+ />
163
+ </>
164
+ )
165
+
166
+ export const Autogrow = (args) => (
167
+ <>
168
+ <TextArea
169
+ {...args}
170
+ name="textarea1"
171
+ label="Autogrow step 1"
172
+ autoGrow
173
+ rows={2}
174
+ value="This TextArea has a height of 2 rows"
175
+ />
176
+ <TextArea
177
+ {...args}
178
+ name="textarea2"
179
+ label="Autogrow step 2"
180
+ autoGrow
181
+ rows={2}
182
+ value={[
183
+ 'This TextArea has a height of two rows',
184
+ 'it also has autoGrow set to true so it will grow with the content',
185
+ ].join('\n')}
186
+ />
187
+ <TextArea
188
+ {...args}
189
+ name="textarea3"
190
+ label="Autogrow step 3"
191
+ autoGrow
192
+ rows={2}
193
+ value={[
194
+ 'This TextArea has a height of two rows',
195
+ 'it also has autoGrow set to true so it will grow with the content.',
196
+ 'See: rows is still 2, but I now have 3 lines.',
197
+ ].join('\n')}
198
+ />
199
+ <TextArea
200
+ {...args}
201
+ name="textarea4"
202
+ label="Autogrow step 4"
203
+ value={[
204
+ 'This TextArea has a height of two rows',
205
+ 'it also has autoGrow set to true so it will grow with the content.',
206
+ 'See: rows is still 2...',
207
+ 'And now I have 4 lines and still no scroll bar in sight.',
208
+ ].join('\n')}
209
+ />
210
+ </>
211
+ )
212
+
213
+ export const RTL = (args) => (
214
+ <div dir="rtl">
215
+ <Template {...args} />
216
+ </div>
217
+ )
218
+ RTL.args = { valid: true, value: 'This RTL text is valid' }
219
+ RTL.storyName = 'RTL: Valid'
@@ -0,0 +1,60 @@
1
+ import { colors, theme, spacers } from '@dhis2/ui-constants'
2
+ import css from 'styled-jsx/css'
3
+
4
+ export const styles = css`
5
+ .textarea {
6
+ display: flex;
7
+ gap: ${spacers.dp8};
8
+ }
9
+ textarea {
10
+ box-sizing: border-box;
11
+ padding: 8px 12px;
12
+
13
+ color: ${colors.grey900};
14
+ background-color: white;
15
+
16
+ border: 1px solid ${colors.grey500};
17
+ border-radius: 3px;
18
+ box-shadow: inset 0 0 1px 0 rgba(48, 54, 60, 0.1);
19
+ outline: 0;
20
+
21
+ font-size: 14px;
22
+ line-height: 17px;
23
+ user-select: text;
24
+ }
25
+
26
+ textarea.dense {
27
+ padding: 6px 8px;
28
+ }
29
+
30
+ textarea:focus {
31
+ outline: none;
32
+ box-shadow: inset 0 0 0 2px ${theme.focus};
33
+ border-color: ${theme.focus};
34
+ }
35
+
36
+ textarea.valid {
37
+ border-color: ${theme.valid};
38
+ }
39
+
40
+ textarea.warning {
41
+ border-color: ${theme.warning};
42
+ }
43
+
44
+ textarea.error {
45
+ border-color: ${theme.error};
46
+ }
47
+
48
+ textarea.read-only {
49
+ background-color: ${colors.grey100};
50
+ border-color: ${colors.grey500};
51
+ cursor: text;
52
+ }
53
+
54
+ textarea.disabled {
55
+ background-color: ${colors.grey100};
56
+ border-color: ${colors.grey500};
57
+ color: ${theme.disabled};
58
+ cursor: not-allowed;
59
+ }
60
+ `
@@ -0,0 +1,20 @@
1
+ import { render, fireEvent, screen } from '@testing-library/react'
2
+ import React from 'react'
3
+ import { TextAreaField } from '../text-area-field.js'
4
+
5
+ describe('<TextArea>', () => {
6
+ it('should call the onKeyDown callback when provided', () => {
7
+ const onKeyDown = jest.fn()
8
+
9
+ render(<TextAreaField name="foo" value="bar" onKeyDown={onKeyDown} />)
10
+
11
+ fireEvent.keyDown(screen.getByRole('textbox'), {})
12
+
13
+ expect(onKeyDown).toHaveBeenCalledWith(
14
+ { name: 'foo', value: 'bar' },
15
+ expect.objectContaining({})
16
+ )
17
+
18
+ expect(onKeyDown).toHaveBeenCalledTimes(1)
19
+ })
20
+ })
@@ -0,0 +1,11 @@
1
+ import { Given, Then } from '@badeball/cypress-cucumber-preprocessor'
2
+
3
+ Given('a TextAreaField with label and a required flag is rendered', () => {
4
+ cy.visitStory('TextAreaField', 'With label and required')
5
+ })
6
+
7
+ Then('the required indicator is visible', () => {
8
+ cy.get('[data-test="dhis2-uiwidgets-textareafield-label-required"]').should(
9
+ 'be.visible'
10
+ )
11
+ })
@@ -0,0 +1,5 @@
1
+ Feature: Required status for the TextAreaField
2
+
3
+ Scenario: Rendering a TextAreaField that is required
4
+ Given a TextAreaField with label and a required flag is rendered
5
+ Then the required indicator is visible
@@ -0,0 +1 @@
1
+ export { TextAreaField } from './text-area-field.js'
@@ -0,0 +1,11 @@
1
+ import React from 'react'
2
+ import { TextAreaField } from './index.js'
3
+
4
+ export default { title: 'TextAreaField' }
5
+ export const WithLabelAndRequired = () => (
6
+ <TextAreaField
7
+ name="textarea"
8
+ label="I am required and have an asterisk"
9
+ required
10
+ />
11
+ )
@@ -0,0 +1,127 @@
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 { TextArea } from '../text-area/index.js'
7
+
8
+ const TextAreaField = ({
9
+ className,
10
+ onChange,
11
+ onFocus,
12
+ onKeyDown,
13
+ onBlur,
14
+ initialFocus,
15
+ dense,
16
+ required,
17
+ label,
18
+ disabled,
19
+ placeholder,
20
+ name,
21
+ valid,
22
+ error,
23
+ warning,
24
+ loading,
25
+ value,
26
+ tabIndex,
27
+ helpText,
28
+ validationText,
29
+ autoGrow,
30
+ readOnly,
31
+ resize = 'vertical',
32
+ rows = 4,
33
+ inputWidth,
34
+ dataTest = 'dhis2-uiwidgets-textareafield',
35
+ }) => (
36
+ <Field
37
+ className={className}
38
+ dataTest={dataTest}
39
+ disabled={disabled}
40
+ required={required}
41
+ name={name}
42
+ helpText={helpText}
43
+ validationText={validationText}
44
+ error={error}
45
+ warning={warning}
46
+ valid={valid}
47
+ label={label}
48
+ >
49
+ <Box width={inputWidth} minWidth="220px">
50
+ <TextArea
51
+ onFocus={onFocus}
52
+ onKeyDown={onKeyDown}
53
+ onBlur={onBlur}
54
+ onChange={onChange}
55
+ name={name}
56
+ value={value || ''}
57
+ placeholder={placeholder}
58
+ disabled={disabled}
59
+ valid={valid}
60
+ warning={warning}
61
+ error={error}
62
+ loading={loading}
63
+ dense={dense}
64
+ tabIndex={tabIndex}
65
+ initialFocus={initialFocus}
66
+ autoGrow={autoGrow}
67
+ readOnly={readOnly}
68
+ resize={resize}
69
+ rows={rows}
70
+ />
71
+ </Box>
72
+ </Field>
73
+ )
74
+
75
+ TextAreaField.propTypes = {
76
+ /** Grow the text area in response to overflow instead of adding a scroll bar */
77
+ autoGrow: PropTypes.bool,
78
+ className: PropTypes.string,
79
+ dataTest: PropTypes.string,
80
+ /** Compact mode */
81
+ dense: PropTypes.bool,
82
+ /** Disables the textarea and makes in non-interactive */
83
+ disabled: PropTypes.bool,
84
+ /** Applies 'error' styles for validation feedback. Mutually exclusive with `valid` and `warning` props */
85
+ error: sharedPropTypes.statusPropType,
86
+ /** Adds useful help text below the textarea */
87
+ helpText: PropTypes.string,
88
+ /** Grabs initial focus on the page */
89
+ initialFocus: PropTypes.bool,
90
+ /** Sets the width of the textarea. Minimum 220px. Any valid CSS measurement can be used */
91
+ inputWidth: PropTypes.string,
92
+ /** Labels the textarea */
93
+ label: PropTypes.string,
94
+ /** Adds a loading spinner */
95
+ loading: PropTypes.bool,
96
+ /** Name associated with the text area. Passed in object argument to event handlers. */
97
+ name: PropTypes.string,
98
+ /** Placeholder text for an empty textarea */
99
+ placeholder: PropTypes.string,
100
+ /** Makes the textarea read-only */
101
+ readOnly: PropTypes.bool,
102
+ /** Adds an asterisk to the label to indicate this field is required */
103
+ required: PropTypes.bool,
104
+ /** [Resize property](https://developer.mozilla.org/en-US/docs/Web/CSS/resize) for the textarea element */
105
+ resize: PropTypes.oneOf(['none', 'both', 'horizontal', 'vertical']),
106
+ /** Initial height of the textarea, in lines of text */
107
+ rows: PropTypes.number,
108
+ tabIndex: PropTypes.string,
109
+ /** Applies 'valid' styles for validation feedback. Mutually exclusive with `warning` and `error` props */
110
+ valid: sharedPropTypes.statusPropType,
111
+ /** Validation text below the textarea to provide validation feedback. Changes appearance depending on validation status */
112
+ validationText: PropTypes.string,
113
+ /** Value in the textarea. Can be used to control component (recommended). Passed in object argument to event handlers. */
114
+ value: PropTypes.string,
115
+ /** Applies 'warning' styles for validation feedback. Mutually exclusive with `valid` and `error` props */
116
+ warning: sharedPropTypes.statusPropType,
117
+ /** Called with signature `({ name: string, value: string }, event)` */
118
+ onBlur: PropTypes.func,
119
+ /** Called with signature `({ name: string, value: string }, event)` */
120
+ onChange: PropTypes.func,
121
+ /** Called with signature `({ name: string, value: string }, event)` */
122
+ onFocus: PropTypes.func,
123
+ /** Called with signature `({ name: string, value: string }, event)` */
124
+ onKeyDown: PropTypes.func,
125
+ }
126
+
127
+ export { TextAreaField }
@@ -0,0 +1,221 @@
1
+ import { sharedPropTypes } from '@dhis2/ui-constants'
2
+ import React from 'react'
3
+ import { TextAreaField } from './index.js'
4
+
5
+ const description = `
6
+ \`TextAreaField\` wraps a \`TextArea\` component with a label, help text, validation text, and other functions.
7
+
8
+ See the regular TextArea for usage information and options.
9
+
10
+ \`\`\`js
11
+ import { TextAreaField } from '@dhis2/ui'
12
+ \`\`\`
13
+ `
14
+
15
+ export default {
16
+ title: 'Text Area Field',
17
+ component: TextAreaField,
18
+ parameters: { docs: { description: { component: description } } },
19
+ // Default args:
20
+ args: {
21
+ onChange: console.log,
22
+ name: 'textareaName',
23
+ },
24
+ argTypes: {
25
+ valid: { ...sharedPropTypes.statusArgType },
26
+ warning: { ...sharedPropTypes.statusArgType },
27
+ error: { ...sharedPropTypes.statusArgType },
28
+ },
29
+ }
30
+
31
+ const Template = (args) => <TextAreaField {...args} />
32
+
33
+ export const NoPlaceholderNoValue = Template.bind({})
34
+ NoPlaceholderNoValue.storyName = 'No placeholder, no value'
35
+
36
+ export const PlaceholderNoValue = Template.bind({})
37
+ PlaceholderNoValue.args = { placeholder: 'Hold the place' }
38
+ PlaceholderNoValue.storyName = 'Placeholder, no value'
39
+
40
+ export const WithHelpText = Template.bind({})
41
+ WithHelpText.args = {
42
+ helpText: 'With some helping text to guide the user along',
43
+ ...PlaceholderNoValue.args,
44
+ }
45
+
46
+ export const WithValue = Template.bind({})
47
+ WithValue.args = {
48
+ value: 'This is set through the value prop, which means the component is controlled.',
49
+ }
50
+
51
+ export const Focus = Template.bind({})
52
+ Focus.args = { initialFocus: true }
53
+ // Disable stories that manipulate focus on docs page
54
+ Focus.parameters = { docs: { disable: true } }
55
+
56
+ export const StatusValid = Template.bind({})
57
+ StatusValid.args = { valid: true, value: 'This value is valid' }
58
+ StatusValid.storyName = 'Status: Valid'
59
+
60
+ export const StatusWarning = Template.bind({})
61
+ StatusWarning.args = { warning: true, value: 'This value produces a warning' }
62
+ StatusWarning.storyName = 'Status: Warning'
63
+
64
+ export const StatusError = Template.bind({})
65
+ StatusError.args = {
66
+ error: true,
67
+ value: 'This value produces an error',
68
+ helpText: 'This is some help text to advise what this input actually is.',
69
+ validationText: 'This describes the error, if a message is supplied.',
70
+ }
71
+ StatusError.storyName = 'Status: Error'
72
+
73
+ export const StatusLoading = Template.bind({})
74
+ StatusLoading.args = {
75
+ loading: true,
76
+ value: 'This value produces a loadingn state',
77
+ }
78
+ StatusLoading.storyName = 'Status: Loading'
79
+
80
+ export const Disabled = Template.bind({})
81
+ Disabled.args = { disabled: true, value: 'This field is disabled' }
82
+
83
+ export const ReadOnly = Template.bind({})
84
+ ReadOnly.args = { readOnly: true, value: 'This field is readOnly' }
85
+
86
+ export const Dense = Template.bind({})
87
+ Dense.args = { dense: true, value: 'This field is dense' }
88
+
89
+ export const LabelTextOverflow = Template.bind({})
90
+ LabelTextOverflow.args = {
91
+ 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!",
92
+ }
93
+ LabelTextOverflow.storyName = 'Label text overflow'
94
+
95
+ export const TextareaTextOverflow = Template.bind({})
96
+ TextareaTextOverflow.args = {
97
+ label: 'I have a scrollbar',
98
+ value: [
99
+ 'A line of text',
100
+ 'A line of text',
101
+ 'A line of text',
102
+ 'A line of text',
103
+ 'A line of text',
104
+ 'A line of text',
105
+ 'A line of text',
106
+ 'A line of text',
107
+ 'A line of text',
108
+ 'A line of text',
109
+ 'A line of text',
110
+ 'A line of text',
111
+ 'A line of text',
112
+ 'A line of text',
113
+ 'A line of text',
114
+ ].join('\n'),
115
+ }
116
+
117
+ export const Required = () => (
118
+ <TextAreaField
119
+ onChange={() => {}}
120
+ name="textarea"
121
+ label="I am required and have an asterisk"
122
+ required
123
+ />
124
+ )
125
+ Required.args = { required: true, label: 'I am required and have an asterisk' }
126
+
127
+ export const Rows = Template.bind({})
128
+ Rows.args = {
129
+ rows: 8,
130
+ label: 'You can set the height with the rows prop. I have 8',
131
+ }
132
+
133
+ export const InputWidth = (args) => (
134
+ <>
135
+ <TextAreaField
136
+ {...args}
137
+ label="My textarea has a width of 220px (the minimum)"
138
+ inputWidth="220px"
139
+ />
140
+ <TextAreaField
141
+ {...args}
142
+ label="My textarea has a width of 400px"
143
+ inputWidth="400px"
144
+ />
145
+ </>
146
+ )
147
+
148
+ export const Resize = (args) => (
149
+ <>
150
+ <TextAreaField
151
+ {...args}
152
+ name="textarea1"
153
+ label="Resize: vertical (default)"
154
+ />
155
+ <TextAreaField
156
+ {...args}
157
+ name="textarea2"
158
+ label="Resize: none"
159
+ resize="none"
160
+ />
161
+ <TextAreaField
162
+ {...args}
163
+ name="textarea3"
164
+ label="Resize: both"
165
+ resize="both"
166
+ />
167
+ <TextAreaField
168
+ {...args}
169
+ name="textarea4"
170
+ label="Resize: horizontal"
171
+ resize="horizontal"
172
+ />
173
+ </>
174
+ )
175
+
176
+ export const Autogrow = (args) => (
177
+ <>
178
+ <TextAreaField
179
+ {...args}
180
+ name="textarea1"
181
+ label="Autogrow step 1"
182
+ autoGrow
183
+ rows={2}
184
+ value="This TextArea has a height of 2 rows"
185
+ />
186
+ <TextAreaField
187
+ {...args}
188
+ name="textarea2"
189
+ label="Autogrow step 2"
190
+ autoGrow
191
+ rows={2}
192
+ value={[
193
+ 'This TextArea has a height of two rows',
194
+ 'it also has autoGrow set to true so it will grow with the content',
195
+ ].join('\n')}
196
+ />
197
+ <TextAreaField
198
+ {...args}
199
+ name="textarea3"
200
+ label="Autogrow step 3"
201
+ autoGrow
202
+ rows={2}
203
+ value={[
204
+ 'This TextArea has a height of two rows',
205
+ 'it also has autoGrow set to true so it will grow with the content.',
206
+ 'See: rows is still 2, but I now have 3 lines.',
207
+ ].join('\n')}
208
+ />
209
+ <TextAreaField
210
+ {...args}
211
+ name="textarea4"
212
+ label="Autogrow step 4"
213
+ value={[
214
+ 'This TextArea has a height of two rows',
215
+ 'it also has autoGrow set to true so it will grow with the content.',
216
+ 'See: rows is still 2...',
217
+ 'And now I have 4 lines and still no scroll bar in sight.',
218
+ ].join('\n')}
219
+ />
220
+ </>
221
+ )