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

feat(react): enable textarea to limit text input to set amount of words #12906

Merged
Merged
Show file tree
Hide file tree
Changes from 58 commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
afa51ee
feat(react): enable textarea to limit text input to set amount of words
matejoslav Dec 31, 2022
16b39e5
Merge branch 'main' into text-area-word-limit
matejoslav Dec 31, 2022
82bdf82
Merge branch 'main' into text-area-word-limit
tw15egan Jan 6, 2023
69ea92e
Merge branch 'main' into text-area-word-limit
tw15egan Jan 9, 2023
1c4badc
test(textArea): fix tests and component
matejoslav Jan 9, 2023
434e1bd
docs(developer handbook): change max characters for line in commit body
matejoslav Jan 9, 2023
9336819
Merge remote-tracking branch 'upstream/main' into text-area-word-limit
matejoslav Jan 19, 2023
ed98158
fix(docs): regenerated contributors to resolve merge conflict
matejoslav Jan 19, 2023
435d969
Merge branch 'main' into text-area-word-limit
matejoslav Jan 20, 2023
51b005f
Merge remote-tracking branch 'upstream/main' into text-area-word-limit
matejoslav Jan 29, 2023
5f30b35
Merge branch 'main' into text-area-word-limit
tw15egan Jan 30, 2023
6d2736f
Merge remote-tracking branch 'upstream/main' into text-area-word-limit
matejoslav Feb 15, 2023
275b6f6
Merge remote-tracking branch 'upstream/main' into text-area-word-limit
matejoslav May 3, 2023
96a6113
revert sync file changes
matejoslav May 4, 2023
48515fb
fix(TextArea): fixed eslint complaints
matejoslav May 4, 2023
38172d0
refactor(TextArea): changes to counterMode and storybook
matejoslav May 4, 2023
b5b849c
refactor(TextArea): removed unused test
matejoslav May 4, 2023
36c8dc6
Merge branch 'main' into text-area-word-limit
matejoslav May 4, 2023
6d31280
docs(README): added contributors
matejoslav May 4, 2023
87bab1b
Merge branch 'main' into text-area-word-limit
matejoslav May 8, 2023
03f63b2
revert(package.json & developer-handbook.md): reverted 90 limit to 80
matejoslav May 9, 2023
eeb9d43
Merge branch 'text-area-word-limit' of github.com:matejoslav/carbon i…
matejoslav May 9, 2023
f4c658e
Merge branch 'main' into text-area-word-limit
matejoslav May 9, 2023
7459cc4
Merge branch 'main' into text-area-word-limit
matejoslav May 9, 2023
7762731
Merge remote-tracking branch 'upstream/main' into text-area-word-limit
matejoslav May 15, 2023
16335de
chore(format): run yarn format
tw15egan May 16, 2023
da60968
fix(README): fixed the contributors formatting
matejoslav May 16, 2023
242dafc
Merge remote-tracking branch 'upstream/main' into text-area-word-limit
matejoslav May 28, 2023
4b62385
Merge remote-tracking branch 'upstream/main' into text-area-word-limit
matejoslav May 28, 2023
432aee9
refactor(TextArea): function to get initial text count
matejoslav May 28, 2023
9b6a2d9
refactor(useAnnouncer): made entityName have default value
matejoslav May 28, 2023
e8a802f
refactor(TextArea): use function to calculate text count in useEffect
matejoslav May 30, 2023
493df13
Merge branch 'main' into text-area-word-limit
matejoslav Jun 1, 2023
4c19757
Merge branch 'main' into text-area-word-limit
matejoslav Jun 2, 2023
e4e3dac
Merge branch 'main' into text-area-word-limit
matejoslav Jun 9, 2023
5755c91
Merge branch 'main' into text-area-word-limit
andreancardona Jun 20, 2023
dab7aa7
Merge branch 'main' into text-area-word-limit
matejoslav Jun 22, 2023
943eec0
Merge branch 'main' into text-area-word-limit
matejoslav Jul 2, 2023
78e17e6
Merge branch 'main' into text-area-word-limit
francinelucca Jul 5, 2023
ddc543c
Merge remote-tracking branch 'upstream/main' into text-area-word-limit
matejoslav Jul 16, 2023
5703d14
fix(TextArea): ignore max count limit when counter is disabled
matejoslav Jul 16, 2023
bc7c954
Merge remote-tracking branch 'upstream/main' into text-area-word-limit
matejoslav Aug 29, 2023
5c8d38f
fix: fixed contributors list
matejoslav Aug 29, 2023
1d05dce
chore(TextArea): trim words on paste and add missing tests
matejoslav Sep 2, 2023
aded469
Merge branch 'main' into text-area-word-limit
matejoslav Sep 2, 2023
98e598f
Merge remote-tracking branch 'upstream/main' into text-area-word-limit
matejoslav Oct 19, 2023
07a313b
chore(TextArea): remove console log
matejoslav Oct 19, 2023
0243e06
chore: Update packages/react/src/components/TextArea/__tests__/TextAr…
matejoslav Oct 19, 2023
24e9b65
chore: remove redundant event definition in test
matejoslav Oct 19, 2023
71ee273
Merge branch 'text-area-word-limit' of github.com:matejoslav/carbon i…
matejoslav Oct 19, 2023
2f4254f
Merge remote-tracking branch 'upstream/main' into text-area-word-limit
matejoslav Nov 20, 2023
8c67185
fix(TextArea): fix count not updating when switching mode
matejoslav Nov 20, 2023
09a9003
fix(TextArea): fix onPaste function
matejoslav Nov 20, 2023
b05553a
fix(TextArea): fix lint errors and tests logic
matejoslav Nov 27, 2023
8e71458
Merge remote-tracking branch 'upstream/main' into text-area-word-limit
matejoslav Nov 27, 2023
27263c4
fix(contributors): revert list to what is on main
matejoslav Nov 27, 2023
748459c
fix: add contributors
matejoslav Nov 27, 2023
573fdf5
Merge branch 'main' into text-area-word-limit
matejoslav Nov 27, 2023
42a0616
Merge branch 'main' into text-area-word-limit
matejoslav Dec 2, 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
28 changes: 28 additions & 0 deletions .all-contributorsrc
Original file line number Diff line number Diff line change
Expand Up @@ -1298,6 +1298,7 @@
"code"
]
},
{
"login": "allisonishida",
"name": "Allison Ishida",
"avatar_url": "https://avatars.githubusercontent.com/u/22247062?v=4",
Expand Down Expand Up @@ -1332,6 +1333,33 @@
"contributions": [
"code"
]
},
{
"login": "matejoslav",
"name": "Matej Ocovsky",
"avatar_url": "https://avatars.githubusercontent.com/u/8973672?v=4",
"profile": "https://github.com/matejoslav",
"contributions": [
"code"
]
},
{
"login": "SamChinellato",
"name": "SamChinellato",
"avatar_url": "https://avatars.githubusercontent.com/u/49278203?v=4",
"profile": "https://samuelechinellato.com/#/",
"contributions": [
"code"
]
},
{
"login": "stevenpatrick009",
"name": "stevenpatrick009",
"avatar_url": "https://avatars.githubusercontent.com/u/106097350?v=4",
"profile": "https://github.com/stevenpatrick009",
"contributions": [
"code"
]
}
],
"commitConvention": "none"
Expand Down
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,10 +261,13 @@ check out our [Contributing Guide](/.github/CONTRIBUTING.md) and our
<td align="center"><a href="https://github.com/Nirajsah"><img src="https://avatars.githubusercontent.com/u/51414373?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Niraj Sah</b></sub></a><br /><a href="https://github.com/carbon-design-system/carbon/commits?author=Nirajsah" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/allisonishida"><img src="https://avatars.githubusercontent.com/u/22247062?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Allison Ishida</b></sub></a><br /><a href="https://github.com/carbon-design-system/carbon/commits?author=allisonishida" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/alewitt2"><img src="https://avatars.githubusercontent.com/u/48691328?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Alex Lewitt</b></sub></a><br /><a href="https://github.com/carbon-design-system/carbon/commits?author=alewitt2" title="Code">💻</a></td>
<td align="center"><a href="https://haruki-kuriwada.netlify.app/"><img src="https://avatars.githubusercontent.com/u/62743644?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ruki</b></sub></a><br /><a href="https://github.com/carbon-design-system/carbon/commits?author=kuri-sun" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/allisonishida"><img src="https://avatars.githubusercontent.com/u/22247062?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Allison Ishida</b></sub></a><br /><a href="https://github.com/carbon-design-system/carbon/commits?author=allisonishida" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/Tresau-IBM"><img src="https://avatars.githubusercontent.com/u/148357638?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Tresau-IBM</b></sub></a><br /><a href="https://github.com/carbon-design-system/carbon/commits?author=Tresau-IBM" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/alewitt2"><img src="https://avatars.githubusercontent.com/u/48691328?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Alex Lewitt</b></sub></a><br /><a href="https://github.com/carbon-design-system/carbon/commits?author=alewitt2" title="Code">💻</a></td>
<td align="center"><a href="https://haruki-kuriwada.netlify.app/"><img src="https://avatars.githubusercontent.com/u/62743644?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ruki</b></sub></a><br /><a href="https://github.com/carbon-design-system/carbon/commits?author=kuri-sun" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/matejoslav"><img src="https://avatars.githubusercontent.com/u/8973672?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Matej Ocovsky</b></sub></a><br /><a href="https://github.com/carbon-design-system/carbon/commits?author=matejoslav" title="Code">💻</a></td>
<td align="center"><a href="https://samuelechinellato.com/#/"><img src="https://avatars.githubusercontent.com/u/49278203?v=4?s=100" width="100px;" alt=""/><br /><sub><b>SamChinellato</b></sub></a><br /><a href="https://github.com/carbon-design-system/carbon/commits?author=SamChinellato" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/stevenpatrick009"><img src="https://avatars.githubusercontent.com/u/106097350?v=4?s=100" width="100px;" alt=""/><br /><sub><b>stevenpatrick009</b></sub></a><br /><a href="https://github.com/carbon-design-system/carbon/commits?author=stevenpatrick009" title="Code">💻</a></td>
</tr>
</table>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8102,6 +8102,15 @@ Map {
"cols": Object {
"type": "number",
},
"counterMode": Object {
"args": Array [
Array [
"character",
"word",
],
],
"type": "oneOf",
},
"defaultValue": Object {
"args": Array [
Array [
Expand Down
63 changes: 55 additions & 8 deletions packages/react/src/components/TextArea/TextArea-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -214,22 +214,69 @@ describe('TextArea', () => {
).toEqual(1);
});

it('should have label and counter disabled', () => {
describe('word counter', () => {
it('should not render element with only counterMode prop passed in', () => {
render(
<TextArea
id="wordCounterTestWrapper1"
labelText="testLabel"
counterMode={'word'}
/>
);
expect(
// eslint-disable-next-line testing-library/no-node-access
screen.getByText('testLabel').closest(`${prefix}--text-area__counter`)
).toEqual(null);
});
});
});

it('should have label and counter disabled', () => {
render(
<TextArea
disabled
enableCounter
id="testing"
labelText="testLabel"
maxCount={100}
/>
);
expect(screen.getByText('testLabel')).toHaveClass(
`${prefix}--label--disabled`
);
expect(screen.getByText('0/100')).toHaveClass(`${prefix}--label--disabled`);
});
});

describe('events', () => {
describe('disabled textarea', () => {
it('should not invoke onClick when textarea is clicked', async () => {
const onClick = jest.fn();
render(
<TextArea
disabled
enableCounter
id="testing"
labelText="testLabel"
maxCount={100}
onClick={onClick}
/>
);
expect(screen.getByText('testLabel')).toHaveClass(
`${prefix}--label--disabled`
);
expect(screen.getByText('0/100')).toHaveClass(
`${prefix}--label--disabled`
await userEvent.click(screen.getByLabelText('testLabel'));
expect(onClick).not.toHaveBeenCalled();
});

it('should not invoke onChange', async () => {
const onChange = jest.fn();
render(
<TextArea
disabled
id="testing"
labelText="testLabel"
onChange={onChange}
/>
);
await userEvent.click(screen.getByLabelText('testLabel'));
await userEvent.keyboard('big blue');
expect(onChange).not.toHaveBeenCalled();
});
});

Expand Down
146 changes: 125 additions & 21 deletions packages/react/src/components/TextArea/TextArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export interface TextAreaProps
disabled?: boolean;

/**
* Specify whether to display the character counter
* Specify whether to display the counter
*/
enableCounter?: boolean;

Expand Down Expand Up @@ -88,7 +88,7 @@ export interface TextAreaProps
light?: boolean;

/**
* Max character count allowed for the textarea. This is needed in order for enableCounter to display
* Max entity count allowed for the textarea. This is needed in order for enableCounter to display
*/
maxCount?: number;

Expand Down Expand Up @@ -138,6 +138,11 @@ export interface TextAreaProps
* Provide the text that is displayed when the control is in warning state
*/
warnText?: ReactNodeLike;

/**
* Specify the method used for calculating the counter number
*/
counterMode?: 'character' | 'word';
}

const TextArea = React.forwardRef((props: TextAreaProps, forwardRef) => {
Expand All @@ -156,6 +161,7 @@ const TextArea = React.forwardRef((props: TextAreaProps, forwardRef) => {
placeholder = '',
enableCounter = false,
maxCount = undefined,
counterMode = 'character',
warn = false,
warnText = '',
rows = 4,
Expand All @@ -165,21 +171,32 @@ const TextArea = React.forwardRef((props: TextAreaProps, forwardRef) => {
const prefix = usePrefix();
const { isFluid } = useContext(FormContext);
const { defaultValue, value } = other;
const [textCount, setTextCount] = useState(
defaultValue?.toString()?.length || value?.toString()?.length || 0
);

const { current: textAreaInstanceId } = useRef(getInstanceId());

const textareaRef = useRef<HTMLTextAreaElement>(null);
const ref = useMergedRefs([forwardRef, textareaRef]) as
| React.LegacyRef<HTMLTextAreaElement>
| undefined;

function getInitialTextCount(): number {
const targetValue =
defaultValue || value || textareaRef.current?.value || '';
const strValue = targetValue.toString();

if (counterMode === 'character') {
return strValue.length;
} else {
return strValue.match(/\w+/g)?.length || 0;
}
}

const [textCount, setTextCount] = useState(getInitialTextCount());

useEffect(() => {
setTextCount(
defaultValue?.toString()?.length || value?.toString()?.length || 0
);
}, [value, defaultValue]);
setTextCount(getInitialTextCount());
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [value, defaultValue, counterMode]);
francinelucca marked this conversation as resolved.
Show resolved Hide resolved

useIsomorphicEffect(() => {
if (other.cols && textareaRef.current) {
Expand All @@ -192,19 +209,92 @@ const TextArea = React.forwardRef((props: TextAreaProps, forwardRef) => {

const textareaProps: {
id: TextAreaProps['id'];
onKeyDown: (evt: React.KeyboardEvent) => void;
onChange: TextAreaProps['onChange'];
onClick: TextAreaProps['onClick'];
maxLength?: number;
onPaste?: React.ClipboardEventHandler<HTMLTextAreaElement>;
} = {
id,
onKeyDown: (evt) => {
if (!disabled && enableCounter && counterMode === 'word') {
const key = evt.which;

if (maxCount && textCount >= maxCount && key === 32) {
evt.preventDefault();
}
}
},
onPaste: (evt) => {
if (!disabled) {
if (
counterMode === 'word' &&
enableCounter &&
typeof maxCount !== 'undefined' &&
textareaRef.current !== null
) {
const existingWords: string[] =
textareaRef.current.value.match(/\w+/g) || [];
const pastedWords: string[] =
evt.clipboardData.getData('Text').match(/\w+/g) || [];

const totalWords = existingWords.length + pastedWords.length;

if (totalWords > maxCount) {
evt.preventDefault();

const allowedWords = existingWords
.concat(pastedWords)
.slice(0, maxCount);

setTimeout(() => {
setTextCount(maxCount);
}, 0);

textareaRef.current.value = allowedWords.join(' ');
}
}
}
},
onChange: (evt) => {
if (!disabled && onChange) {
evt?.persist?.();
// 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);
if (!disabled) {
if (counterMode == 'character') {
evt?.persist?.();
// 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);
} else if (counterMode == 'word') {
if (!evt.target.value) {
setTimeout(() => {
setTextCount(0);
}, 0);

return;
}

if (
enableCounter &&
typeof maxCount !== 'undefined' &&
textareaRef.current !== null
) {
const matchedWords = evt.target?.value?.match(/\w+/g);
if (matchedWords && matchedWords.length <= maxCount) {
textareaRef.current.removeAttribute('maxLength');

setTimeout(() => {
setTextCount(matchedWords.length);
}, 0);
} else if (matchedWords && matchedWords.length > maxCount) {
setTimeout(() => {
setTextCount(matchedWords.length);
}, 0);
}
}
}
if (onChange) {
onChange(evt);
}
}
},
onClick: (evt) => {
Expand Down Expand Up @@ -248,7 +338,9 @@ const TextArea = React.forwardRef((props: TextAreaProps, forwardRef) => {
) : null;

const counter =
enableCounter && maxCount ? (
enableCounter &&
maxCount &&
(counterMode === 'character' || counterMode === 'word') ? (
<Text
as="div"
className={counterClasses}>{`${textCount}/${maxCount}`}</Text>
Expand Down Expand Up @@ -298,9 +390,16 @@ const TextArea = React.forwardRef((props: TextAreaProps, forwardRef) => {
}

if (enableCounter) {
textareaProps.maxLength = maxCount;
// handle different counter mode
if (counterMode == 'character') {
textareaProps.maxLength = maxCount;
}
}
const ariaAnnouncement = useAnnouncer(textCount, maxCount);
const ariaAnnouncement = useAnnouncer(
textCount,
maxCount,
counterMode === 'word' ? 'words' : undefined
);

const input = (
<textarea
Expand Down Expand Up @@ -369,6 +468,11 @@ TextArea.propTypes = {
*/
cols: PropTypes.number,

/**
* Specify the method used for calculating the counter number
*/
counterMode: PropTypes.oneOf(['character', 'word']),

/**
* Optionally provide the default value of the `<textarea>`
*/
Expand All @@ -380,7 +484,7 @@ TextArea.propTypes = {
disabled: PropTypes.bool,

/**
* Specify whether to display the character counter
* Specify whether to display the counter
*/
enableCounter: PropTypes.bool,

Expand Down Expand Up @@ -426,7 +530,7 @@ TextArea.propTypes = {
),

/**
* Max character count allowed for the textarea. This is needed in order for enableCounter to display
* Max entity count allowed for the textarea. This is needed in order for enableCounter to display
*/
maxCount: PropTypes.number,

Expand Down
Loading