Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(react): updated textarea counter value changes on re-render #13449

Merged
merged 18 commits into from
May 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
823b465
fix(react): updated textarea counter value changes on re-render
TannerS Apr 1, 2023
28c5f68
fix: format
francinelucca Apr 3, 2023
dc7df39
Update packages/react/src/components/TextArea/__tests__/TextArea-test.js
andreancardona Apr 4, 2023
a6389b3
Update packages/react/src/components/TextArea/__tests__/TextArea-test.js
andreancardona Apr 4, 2023
3870f1e
Update packages/react/src/components/TextArea/__tests__/TextArea-test.js
andreancardona Apr 4, 2023
e36c628
Update packages/react/src/components/TextArea/__tests__/TextArea-test.js
TannerS Apr 15, 2023
3576aa0
fix(react): fixed textarea test with updated rtl
TannerS Apr 15, 2023
23b7b9e
Merge remote-tracking branch 'origin/tanner_fixTextAreaCounter' into …
TannerS Apr 15, 2023
4c4b04d
Merge branch 'main' into tanner_fixTextAreaCounter
TannerS May 3, 2023
bedec93
Merge branch 'main' into tanner_fixTextAreaCounter
andreancardona May 11, 2023
382b013
fix(TextArea): use textarea ref value instead of [value,defaultValue]
francinelucca May 15, 2023
57f9ca9
fix: format
francinelucca May 15, 2023
4dbacdd
Merge branch 'main' of github.com:carbon-design-system/carbon into ta…
francinelucca May 15, 2023
b0947ec
fix(TextArea): add value to textCounter dependency array
francinelucca May 15, 2023
7361200
fix(TextArea): textCount optimizations
francinelucca May 15, 2023
54b7c90
fix: format
francinelucca May 15, 2023
e169547
Merge branch 'main' into tanner_fixTextAreaCounter
andreancardona May 16, 2023
4f43820
Merge branch 'main' into tanner_fixTextAreaCounter
kodiakhq[bot] May 16, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/react/src/components/TextArea/TextArea.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,11 @@ Playground.argTypes = {
},
defaultValue: 'This is a warning message.',
},
value: {
control: {
type: 'text',
},
},
};

Playground.args = {
Expand Down
15 changes: 12 additions & 3 deletions packages/react/src/components/TextArea/TextArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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'];
Expand All @@ -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);
}
},
Expand Down
303 changes: 287 additions & 16 deletions packages/react/src/components/TextArea/__tests__/TextArea-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<TextArea
data-testid="test-id"
id="area-1"
labelText="TextArea label"
/>
);

expect(screen.getByRole('textbox')).toHaveAttribute(
'data-testid',
'test-id'
);
});

it('should respect defaultValue prop', () => {
render(
<TextArea
id="input-1"
id="textarea-1"
labelText="TextArea label"
onClick={onClick}
onChange={onChange}
readOnly
value="test"
defaultValue="This is default text"
/>
);

expect(screen.getByText('This is default text')).toBeInTheDocument();
});

it('should respect disabled prop', () => {
render(<TextArea id="textarea-1" labelText="TextArea label" disabled />);

expect(screen.getByRole('textbox')).toBeDisabled();
});

it('should respect helperText prop', () => {
render(
<TextArea
id="textarea-1"
labelText="TextArea label"
helperText="This is helper text"
/>
);

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(<TextArea id="textarea-1" labelText="TextArea label" hideLabel />);

expect(screen.getByText('TextArea label')).toBeInTheDocument();
expect(screen.getByText('TextArea label')).toHaveClass(
`${prefix}--visually-hidden`
);
});

it('should respect id prop', () => {
render(<TextArea id="textarea-1" labelText="TextArea label" />);
expect(screen.getByRole('textbox')).toHaveAttribute('id', 'textarea-1');
});

it('should respect invalid prop', () => {
const { container } = render(
<TextArea id="textarea-1" labelText="TextArea" invalid />
);

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(
<TextArea
id="textarea-1"
labelText="TextArea"
invalid
invalidText="This is invalid text"
/>
);

// 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(<TextArea id="textarea-1" labelText="TextArea label" />);

expect(screen.getByText('TextArea label')).toBeInTheDocument();
expect(screen.getByText('TextArea label')).toHaveClass(
`${prefix}--label`
);
});

it('should respect placeholder prop', () => {
render(
<TextArea
id="textarea-1"
labelText="TextArea label"
placeholder="Placeholder text"
/>
);

expect(
screen.getByPlaceholderText('Placeholder text')
).toBeInTheDocument();
});

it('should respect value prop', () => {
render(
<TextArea
id="textarea-1"
labelText="TextArea label"
value="This is a test value"
/>
);

expect(screen.getByText('This is a test value')).toBeInTheDocument();
});

it('should respect warn prop', () => {
const { container } = render(
<TextArea id="textarea-1" labelText="TextArea label" warn />
);

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(
<TextArea
id="textarea-1"
labelText="TextArea label"
warn
warnText="This is warning text"
/>
);

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(<TextArea id="textarea-1" labelText="TextArea label" rows={25} />);
expect(screen.getByRole('textbox')).toHaveAttribute('rows', '25');
});

it('should respect enableCounter and maxCount prop', () => {
render(
<TextArea
id="textarea-1"
labelText="TextArea label"
enableCounter={true}
maxCount={500}
/>
);
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(
<TextArea
id="textarea-1"
labelText="TextArea label"
data-testid-="textarea-1"
onChange={onChange}
/>
);

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(
<TextArea
id="textarea-1"
labelText="TextArea label"
data-testid-="textarea-1"
onClick={onClick}
/>
);

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 `<input>` is clicked but disabled', () => {
const onClick = jest.fn();
render(
<TextArea
id="textarea-1"
labelText="TextArea label"
onClick={onClick}
disabled
/>
);

userEvent.click(screen.getByRole('textbox'));
expect(onClick).not.toHaveBeenCalled();
});

it('should respect readOnly prop', async () => {
const onChange = jest.fn();
const onClick = jest.fn();
render(
<TextArea
id="textarea-1"
labelText="TextArea label"
onClick={onClick}
onChange={onChange}
readOnly
/>
);

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(
<TextArea id="textarea-1" labelText="TextArea label" enableCounter />
);

const counter = screen.queryByText('0/5');

expect(counter).not.toBeInTheDocument();
});

it('should not render counter with only maxCount prop passed in', () => {
render(
<TextArea id="textarea-1" labelText="TextArea label" enableCounter />
);

const counter = screen.queryByText('0/5');

expect(counter).not.toBeInTheDocument();
});

it('should have the expected classes for counter', () => {
render(
<TextArea
id="textarea-1"
labelText="TextArea label"
enableCounter
maxCount={5}
/>
);

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();
});
});
});
});