Skip to content

Commit ae85de9

Browse files
TannerSfrancineluccaandreancardonakodiakhq[bot]
authored
fix(react): updated textarea counter value changes on re-render (#13449)
* fix(react): updated textarea counter value changes on re-render * fix: format * Update packages/react/src/components/TextArea/__tests__/TextArea-test.js * Update packages/react/src/components/TextArea/__tests__/TextArea-test.js * Update packages/react/src/components/TextArea/__tests__/TextArea-test.js * Update packages/react/src/components/TextArea/__tests__/TextArea-test.js Co-authored-by: Francine Lucca <40550942+francinelucca@users.noreply.github.com> * fix(TextArea): use textarea ref value instead of [value,defaultValue] * fix: format * fix(TextArea): add value to textCounter dependency array * fix(TextArea): textCount optimizations - use value,defaultValue in useEffect insead of ref - delay textCount assignation * fix: format --------- Co-authored-by: Francine Lucca <francinelucca@hotmail.com> Co-authored-by: Andrea N. Cardona <cardona.n.andrea@gmail.com> Co-authored-by: Francine Lucca <40550942+francinelucca@users.noreply.github.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
1 parent 05fa295 commit ae85de9

File tree

3 files changed

+304
-19
lines changed

3 files changed

+304
-19
lines changed

packages/react/src/components/TextArea/TextArea.stories.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,11 @@ Playground.argTypes = {
139139
},
140140
defaultValue: 'This is a warning message.',
141141
},
142+
value: {
143+
control: {
144+
type: 'text',
145+
},
146+
},
142147
};
143148

144149
Playground.args = {

packages/react/src/components/TextArea/TextArea.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
*/
77

88
import PropTypes, { ReactNodeLike } from 'prop-types';
9-
import React, { useState, useContext, useRef } from 'react';
9+
import React, { useState, useContext, useRef, useEffect } from 'react';
1010
import classNames from 'classnames';
1111
import deprecate from '../../prop-types/deprecate';
1212
import { WarningFilled, WarningAltFilled } from '@carbon/icons-react';
@@ -156,10 +156,16 @@ const TextArea = React.forwardRef((props: TextAreaProps, forwardRef) => {
156156
const { isFluid } = useContext(FormContext);
157157
const { defaultValue, value, disabled } = other;
158158
const [textCount, setTextCount] = useState(
159-
defaultValue?.toString().length || value?.toString().length || 0
159+
defaultValue?.toString()?.length || value?.toString()?.length || 0
160160
);
161161
const { current: textAreaInstanceId } = useRef(getInstanceId());
162162

163+
useEffect(() => {
164+
setTextCount(
165+
defaultValue?.toString()?.length || value?.toString()?.length || 0
166+
);
167+
}, [value, defaultValue]);
168+
163169
const textareaProps: {
164170
id: TextAreaProps['id'];
165171
onChange: TextAreaProps['onChange'];
@@ -169,7 +175,10 @@ const TextArea = React.forwardRef((props: TextAreaProps, forwardRef) => {
169175
id,
170176
onChange: (evt) => {
171177
if (!other.disabled && onChange) {
172-
setTextCount(evt.target.value?.length);
178+
// delay textCount assignation to give the textarea element value time to catch up if is a controlled input
179+
setTimeout(() => {
180+
setTextCount(evt.target.value?.length);
181+
}, 0);
173182
onChange(evt);
174183
}
175184
},

packages/react/src/components/TextArea/__tests__/TextArea-test.js

Lines changed: 287 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,30 +10,301 @@ import TextArea from '../TextArea';
1010
import userEvent from '@testing-library/user-event';
1111
import { render, screen } from '@testing-library/react';
1212

13+
const prefix = 'cds';
14+
1315
describe('TextArea', () => {
14-
describe('behaves as expected - Component API', () => {
15-
it('should respect readOnly prop', async () => {
16-
const onChange = jest.fn();
17-
const onClick = jest.fn();
16+
describe('renders as expected - Component API', () => {
17+
it('should spread extra props onto the text area element', () => {
18+
render(
19+
<TextArea
20+
data-testid="test-id"
21+
id="area-1"
22+
labelText="TextArea label"
23+
/>
24+
);
25+
26+
expect(screen.getByRole('textbox')).toHaveAttribute(
27+
'data-testid',
28+
'test-id'
29+
);
30+
});
31+
32+
it('should respect defaultValue prop', () => {
1833
render(
1934
<TextArea
20-
id="input-1"
35+
id="textarea-1"
2136
labelText="TextArea label"
22-
onClick={onClick}
23-
onChange={onChange}
24-
readOnly
25-
value="test"
37+
defaultValue="This is default text"
38+
/>
39+
);
40+
41+
expect(screen.getByText('This is default text')).toBeInTheDocument();
42+
});
43+
44+
it('should respect disabled prop', () => {
45+
render(<TextArea id="textarea-1" labelText="TextArea label" disabled />);
46+
47+
expect(screen.getByRole('textbox')).toBeDisabled();
48+
});
49+
50+
it('should respect helperText prop', () => {
51+
render(
52+
<TextArea
53+
id="textarea-1"
54+
labelText="TextArea label"
55+
helperText="This is helper text"
56+
/>
57+
);
58+
59+
expect(screen.getByText('This is helper text')).toBeInTheDocument();
60+
expect(screen.getByText('This is helper text')).toHaveClass(
61+
`${prefix}--form__helper-text`
62+
);
63+
});
64+
65+
it('should respect hideLabel prop', () => {
66+
render(<TextArea id="textarea-1" labelText="TextArea label" hideLabel />);
67+
68+
expect(screen.getByText('TextArea label')).toBeInTheDocument();
69+
expect(screen.getByText('TextArea label')).toHaveClass(
70+
`${prefix}--visually-hidden`
71+
);
72+
});
73+
74+
it('should respect id prop', () => {
75+
render(<TextArea id="textarea-1" labelText="TextArea label" />);
76+
expect(screen.getByRole('textbox')).toHaveAttribute('id', 'textarea-1');
77+
});
78+
79+
it('should respect invalid prop', () => {
80+
const { container } = render(
81+
<TextArea id="textarea-1" labelText="TextArea" invalid />
82+
);
83+
84+
const invalidIcon = container.querySelector(
85+
`svg.${prefix}--text-area__invalid-icon`
86+
);
87+
88+
expect(screen.getByRole('textbox')).toHaveClass(
89+
`${prefix}--text-area--invalid`
90+
);
91+
expect(invalidIcon).toBeInTheDocument();
92+
});
93+
94+
it('should respect invalidText prop', () => {
95+
render(
96+
<TextArea
97+
id="textarea-1"
98+
labelText="TextArea"
99+
invalid
100+
invalidText="This is invalid text"
26101
/>
27102
);
28103

29-
// Click events should fire
30-
await userEvent.click(screen.getByRole('textbox'));
31-
expect(onClick).toHaveBeenCalledTimes(1);
104+
expect(screen.getByText('This is invalid text')).toBeInTheDocument();
105+
expect(screen.getByText('This is invalid text')).toHaveClass(
106+
`${prefix}--form-requirement`
107+
);
108+
});
109+
110+
it('should respect labelText prop', () => {
111+
render(<TextArea id="textarea-1" labelText="TextArea label" />);
112+
113+
expect(screen.getByText('TextArea label')).toBeInTheDocument();
114+
expect(screen.getByText('TextArea label')).toHaveClass(
115+
`${prefix}--label`
116+
);
117+
});
118+
119+
it('should respect placeholder prop', () => {
120+
render(
121+
<TextArea
122+
id="textarea-1"
123+
labelText="TextArea label"
124+
placeholder="Placeholder text"
125+
/>
126+
);
127+
128+
expect(
129+
screen.getByPlaceholderText('Placeholder text')
130+
).toBeInTheDocument();
131+
});
132+
133+
it('should respect value prop', () => {
134+
render(
135+
<TextArea
136+
id="textarea-1"
137+
labelText="TextArea label"
138+
value="This is a test value"
139+
/>
140+
);
141+
142+
expect(screen.getByText('This is a test value')).toBeInTheDocument();
143+
});
144+
145+
it('should respect warn prop', () => {
146+
const { container } = render(
147+
<TextArea id="textarea-1" labelText="TextArea label" warn />
148+
);
149+
150+
const warnIcon = container.querySelector(
151+
`svg.${prefix}--text-area__invalid-icon--warning`
152+
);
153+
154+
expect(screen.getByRole('textbox')).toHaveClass(
155+
`${prefix}--text-area--warn`
156+
);
157+
expect(warnIcon).toBeInTheDocument();
158+
});
159+
160+
it('should respect warnText prop', () => {
161+
render(
162+
<TextArea
163+
id="textarea-1"
164+
labelText="TextArea label"
165+
warn
166+
warnText="This is warning text"
167+
/>
168+
);
169+
170+
expect(screen.getByText('This is warning text')).toBeInTheDocument();
171+
expect(screen.getByText('This is warning text')).toHaveClass(
172+
`${prefix}--form-requirement`
173+
);
174+
});
175+
176+
it('should respect rows prop', () => {
177+
render(<TextArea id="textarea-1" labelText="TextArea label" rows={25} />);
178+
expect(screen.getByRole('textbox')).toHaveAttribute('rows', '25');
179+
});
180+
181+
it('should respect enableCounter and maxCount prop', () => {
182+
render(
183+
<TextArea
184+
id="textarea-1"
185+
labelText="TextArea label"
186+
enableCounter={true}
187+
maxCount={500}
188+
/>
189+
);
190+
expect(screen.getByRole('textbox')).toHaveAttribute('maxlength', '500');
191+
expect(screen.getByText('0/500')).toBeInTheDocument();
192+
});
193+
194+
describe('behaves as expected - Component API', () => {
195+
it('should respect onChange prop', async () => {
196+
const onChange = jest.fn();
197+
render(
198+
<TextArea
199+
id="textarea-1"
200+
labelText="TextArea label"
201+
data-testid-="textarea-1"
202+
onChange={onChange}
203+
/>
204+
);
205+
206+
const component = screen.getByRole('textbox');
207+
208+
await userEvent.type(component, 'x');
209+
expect(component).toHaveValue('x');
210+
expect(onChange).toHaveBeenCalledTimes(1);
211+
expect(onChange).toHaveBeenCalledWith(
212+
expect.objectContaining({
213+
target: expect.any(Object),
214+
})
215+
);
216+
});
217+
218+
it('should respect onClick prop', async () => {
219+
const onClick = jest.fn();
220+
render(
221+
<TextArea
222+
id="textarea-1"
223+
labelText="TextArea label"
224+
data-testid-="textarea-1"
225+
onClick={onClick}
226+
/>
227+
);
228+
229+
await userEvent.click(screen.getByRole('textbox'));
230+
expect(onClick).toHaveBeenCalledTimes(1);
231+
expect(onClick).toHaveBeenCalledWith(
232+
expect.objectContaining({
233+
target: expect.any(Object),
234+
})
235+
);
236+
});
237+
238+
it('should not call `onClick` when the `<input>` is clicked but disabled', () => {
239+
const onClick = jest.fn();
240+
render(
241+
<TextArea
242+
id="textarea-1"
243+
labelText="TextArea label"
244+
onClick={onClick}
245+
disabled
246+
/>
247+
);
248+
249+
userEvent.click(screen.getByRole('textbox'));
250+
expect(onClick).not.toHaveBeenCalled();
251+
});
252+
253+
it('should respect readOnly prop', async () => {
254+
const onChange = jest.fn();
255+
const onClick = jest.fn();
256+
render(
257+
<TextArea
258+
id="textarea-1"
259+
labelText="TextArea label"
260+
onClick={onClick}
261+
onChange={onChange}
262+
readOnly
263+
/>
264+
);
265+
266+
await userEvent.click(screen.getByRole('textbox'));
267+
expect(onClick).toHaveBeenCalledTimes(1);
268+
269+
userEvent.type(screen.getByRole('textbox'), 'x');
270+
expect(screen.getByRole('textbox')).not.toHaveValue('x');
271+
expect(onChange).toHaveBeenCalledTimes(0);
272+
});
273+
274+
it('should not render counter with only enableCounter prop passed in', () => {
275+
render(
276+
<TextArea id="textarea-1" labelText="TextArea label" enableCounter />
277+
);
278+
279+
const counter = screen.queryByText('0/5');
280+
281+
expect(counter).not.toBeInTheDocument();
282+
});
283+
284+
it('should not render counter with only maxCount prop passed in', () => {
285+
render(
286+
<TextArea id="textarea-1" labelText="TextArea label" enableCounter />
287+
);
288+
289+
const counter = screen.queryByText('0/5');
290+
291+
expect(counter).not.toBeInTheDocument();
292+
});
293+
294+
it('should have the expected classes for counter', () => {
295+
render(
296+
<TextArea
297+
id="textarea-1"
298+
labelText="TextArea label"
299+
enableCounter
300+
maxCount={5}
301+
/>
302+
);
303+
304+
const counter = screen.queryByText('0/5');
32305

33-
// Change events should *not* fire
34-
await userEvent.type(screen.getByRole('textbox'), 'x');
35-
expect(screen.getByRole('textbox')).not.toHaveValue('x');
36-
expect(onChange).toHaveBeenCalledTimes(0);
306+
expect(counter).toBeInTheDocument();
307+
});
37308
});
38309
});
39310
});

0 commit comments

Comments
 (0)