diff --git a/packages/react/src/components/TextArea/TextArea.stories.js b/packages/react/src/components/TextArea/TextArea.stories.js
index 14e595505a09..530e6fd7255a 100644
--- a/packages/react/src/components/TextArea/TextArea.stories.js
+++ b/packages/react/src/components/TextArea/TextArea.stories.js
@@ -139,6 +139,11 @@ Playground.argTypes = {
},
defaultValue: 'This is a warning message.',
},
+ value: {
+ control: {
+ type: 'text',
+ },
+ },
};
Playground.args = {
diff --git a/packages/react/src/components/TextArea/TextArea.tsx b/packages/react/src/components/TextArea/TextArea.tsx
index 334ac7f65e2f..9cdbdaf46a76 100644
--- a/packages/react/src/components/TextArea/TextArea.tsx
+++ b/packages/react/src/components/TextArea/TextArea.tsx
@@ -6,7 +6,7 @@
*/
import PropTypes, { ReactNodeLike } from 'prop-types';
-import React, { useState, useContext, useRef } from 'react';
+import React, { useState, useContext, useRef, useEffect } from 'react';
import classNames from 'classnames';
import deprecate from '../../prop-types/deprecate';
import { WarningFilled, WarningAltFilled } from '@carbon/icons-react';
@@ -156,10 +156,16 @@ const TextArea = React.forwardRef((props: TextAreaProps, forwardRef) => {
const { isFluid } = useContext(FormContext);
const { defaultValue, value, disabled } = other;
const [textCount, setTextCount] = useState(
- defaultValue?.toString().length || value?.toString().length || 0
+ defaultValue?.toString()?.length || value?.toString()?.length || 0
);
const { current: textAreaInstanceId } = useRef(getInstanceId());
+ useEffect(() => {
+ setTextCount(
+ defaultValue?.toString()?.length || value?.toString()?.length || 0
+ );
+ }, [value, defaultValue]);
+
const textareaProps: {
id: TextAreaProps['id'];
onChange: TextAreaProps['onChange'];
@@ -169,7 +175,10 @@ const TextArea = React.forwardRef((props: TextAreaProps, forwardRef) => {
id,
onChange: (evt) => {
if (!other.disabled && onChange) {
- setTextCount(evt.target.value?.length);
+ // delay textCount assignation to give the textarea element value time to catch up if is a controlled input
+ setTimeout(() => {
+ setTextCount(evt.target.value?.length);
+ }, 0);
onChange(evt);
}
},
diff --git a/packages/react/src/components/TextArea/__tests__/TextArea-test.js b/packages/react/src/components/TextArea/__tests__/TextArea-test.js
index 0ab1d2f38148..2060df91aee9 100644
--- a/packages/react/src/components/TextArea/__tests__/TextArea-test.js
+++ b/packages/react/src/components/TextArea/__tests__/TextArea-test.js
@@ -10,30 +10,301 @@ import TextArea from '../TextArea';
import userEvent from '@testing-library/user-event';
import { render, screen } from '@testing-library/react';
+const prefix = 'cds';
+
describe('TextArea', () => {
- describe('behaves as expected - Component API', () => {
- it('should respect readOnly prop', async () => {
- const onChange = jest.fn();
- const onClick = jest.fn();
+ describe('renders as expected - Component API', () => {
+ it('should spread extra props onto the text area element', () => {
+ render(
+
+ );
+
+ expect(screen.getByRole('textbox')).toHaveAttribute(
+ 'data-testid',
+ 'test-id'
+ );
+ });
+
+ it('should respect defaultValue prop', () => {
render(
+ );
+
+ expect(screen.getByText('This is default text')).toBeInTheDocument();
+ });
+
+ it('should respect disabled prop', () => {
+ render();
+
+ expect(screen.getByRole('textbox')).toBeDisabled();
+ });
+
+ it('should respect helperText prop', () => {
+ render(
+
+ );
+
+ expect(screen.getByText('This is helper text')).toBeInTheDocument();
+ expect(screen.getByText('This is helper text')).toHaveClass(
+ `${prefix}--form__helper-text`
+ );
+ });
+
+ it('should respect hideLabel prop', () => {
+ render();
+
+ expect(screen.getByText('TextArea label')).toBeInTheDocument();
+ expect(screen.getByText('TextArea label')).toHaveClass(
+ `${prefix}--visually-hidden`
+ );
+ });
+
+ it('should respect id prop', () => {
+ render();
+ expect(screen.getByRole('textbox')).toHaveAttribute('id', 'textarea-1');
+ });
+
+ it('should respect invalid prop', () => {
+ const { container } = render(
+
+ );
+
+ const invalidIcon = container.querySelector(
+ `svg.${prefix}--text-area__invalid-icon`
+ );
+
+ expect(screen.getByRole('textbox')).toHaveClass(
+ `${prefix}--text-area--invalid`
+ );
+ expect(invalidIcon).toBeInTheDocument();
+ });
+
+ it('should respect invalidText prop', () => {
+ render(
+
);
- // Click events should fire
- await userEvent.click(screen.getByRole('textbox'));
- expect(onClick).toHaveBeenCalledTimes(1);
+ expect(screen.getByText('This is invalid text')).toBeInTheDocument();
+ expect(screen.getByText('This is invalid text')).toHaveClass(
+ `${prefix}--form-requirement`
+ );
+ });
+
+ it('should respect labelText prop', () => {
+ render();
+
+ expect(screen.getByText('TextArea label')).toBeInTheDocument();
+ expect(screen.getByText('TextArea label')).toHaveClass(
+ `${prefix}--label`
+ );
+ });
+
+ it('should respect placeholder prop', () => {
+ render(
+
+ );
+
+ expect(
+ screen.getByPlaceholderText('Placeholder text')
+ ).toBeInTheDocument();
+ });
+
+ it('should respect value prop', () => {
+ render(
+
+ );
+
+ expect(screen.getByText('This is a test value')).toBeInTheDocument();
+ });
+
+ it('should respect warn prop', () => {
+ const { container } = render(
+
+ );
+
+ const warnIcon = container.querySelector(
+ `svg.${prefix}--text-area__invalid-icon--warning`
+ );
+
+ expect(screen.getByRole('textbox')).toHaveClass(
+ `${prefix}--text-area--warn`
+ );
+ expect(warnIcon).toBeInTheDocument();
+ });
+
+ it('should respect warnText prop', () => {
+ render(
+
+ );
+
+ expect(screen.getByText('This is warning text')).toBeInTheDocument();
+ expect(screen.getByText('This is warning text')).toHaveClass(
+ `${prefix}--form-requirement`
+ );
+ });
+
+ it('should respect rows prop', () => {
+ render();
+ expect(screen.getByRole('textbox')).toHaveAttribute('rows', '25');
+ });
+
+ it('should respect enableCounter and maxCount prop', () => {
+ render(
+
+ );
+ expect(screen.getByRole('textbox')).toHaveAttribute('maxlength', '500');
+ expect(screen.getByText('0/500')).toBeInTheDocument();
+ });
+
+ describe('behaves as expected - Component API', () => {
+ it('should respect onChange prop', async () => {
+ const onChange = jest.fn();
+ render(
+
+ );
+
+ const component = screen.getByRole('textbox');
+
+ await userEvent.type(component, 'x');
+ expect(component).toHaveValue('x');
+ expect(onChange).toHaveBeenCalledTimes(1);
+ expect(onChange).toHaveBeenCalledWith(
+ expect.objectContaining({
+ target: expect.any(Object),
+ })
+ );
+ });
+
+ it('should respect onClick prop', async () => {
+ const onClick = jest.fn();
+ render(
+
+ );
+
+ await userEvent.click(screen.getByRole('textbox'));
+ expect(onClick).toHaveBeenCalledTimes(1);
+ expect(onClick).toHaveBeenCalledWith(
+ expect.objectContaining({
+ target: expect.any(Object),
+ })
+ );
+ });
+
+ it('should not call `onClick` when the `` is clicked but disabled', () => {
+ const onClick = jest.fn();
+ render(
+
+ );
+
+ userEvent.click(screen.getByRole('textbox'));
+ expect(onClick).not.toHaveBeenCalled();
+ });
+
+ it('should respect readOnly prop', async () => {
+ const onChange = jest.fn();
+ const onClick = jest.fn();
+ render(
+
+ );
+
+ await userEvent.click(screen.getByRole('textbox'));
+ expect(onClick).toHaveBeenCalledTimes(1);
+
+ userEvent.type(screen.getByRole('textbox'), 'x');
+ expect(screen.getByRole('textbox')).not.toHaveValue('x');
+ expect(onChange).toHaveBeenCalledTimes(0);
+ });
+
+ it('should not render counter with only enableCounter prop passed in', () => {
+ render(
+
+ );
+
+ const counter = screen.queryByText('0/5');
+
+ expect(counter).not.toBeInTheDocument();
+ });
+
+ it('should not render counter with only maxCount prop passed in', () => {
+ render(
+
+ );
+
+ const counter = screen.queryByText('0/5');
+
+ expect(counter).not.toBeInTheDocument();
+ });
+
+ it('should have the expected classes for counter', () => {
+ render(
+
+ );
+
+ const counter = screen.queryByText('0/5');
- // Change events should *not* fire
- await userEvent.type(screen.getByRole('textbox'), 'x');
- expect(screen.getByRole('textbox')).not.toHaveValue('x');
- expect(onChange).toHaveBeenCalledTimes(0);
+ expect(counter).toBeInTheDocument();
+ });
});
});
});