Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
29 changes: 27 additions & 2 deletions packages/components/textarea/Textarea.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
import classNames from 'classnames';
import { CloseCircleFilledIcon as TdCloseCircleFilledIcon } from 'tdesign-icons-react';
import calcTextareaHeight from '@tdesign/common-js/utils/calcTextareaHeight';
import { getCharacterLength, getUnicodeLength, limitUnicodeMaxLength } from '@tdesign/common-js/utils/helper';

Expand All @@ -9,6 +10,7 @@ import useConfig from '../hooks/useConfig';
import useControlled from '../hooks/useControlled';
import useDefaultProps from '../hooks/useDefaultProps';
import useEventCallback from '../hooks/useEventCallback';
import useGlobalIcon from '../hooks/useGlobalIcon';
import useIsomorphicLayoutEffect from '../hooks/useLayoutEffect';
import { textareaDefaultProps } from './defaultProps';

Expand Down Expand Up @@ -49,9 +51,12 @@ const Textarea = forwardRef<TextareaRefInterface, TextareaProps>((originalProps,
tips,
allowInputOverMax,
rows,
clearable,
onClear,
...otherProps
} = props;
const hasMaxcharacter = typeof maxcharacter !== 'undefined';
const readOnlyProp = props.readOnly || props.readonly;

const [value = '', setValue] = useControlled(props, 'value', props.onChange);

Expand All @@ -72,6 +77,10 @@ const Textarea = forwardRef<TextareaRefInterface, TextareaProps>((originalProps,
}, [value, allowInputOverMax, maxcharacter]);

const { classPrefix } = useConfig();
const { CloseCircleFilledIcon } = useGlobalIcon({
CloseCircleFilledIcon: TdCloseCircleFilledIcon,
});
const isShowClearIcon = clearable && value && !disabled && !readOnlyProp;

const textareaPropsNames = Object.keys(otherProps).filter(
(key) => !/^on[A-Z]/.test(key) && !OMIT_PROPS.includes(key),
Expand Down Expand Up @@ -140,6 +149,13 @@ const Textarea = forwardRef<TextareaRefInterface, TextareaProps>((originalProps,
}
}

function handleClear(e: React.MouseEvent<SVGSVGElement>) {
composingRef.current = false;
setComposingValue('');
setValue('', { e, trigger: 'clear' });
onClear?.({ e });
}

function handleCompositionStart() {
composingRef.current = true;
}
Expand Down Expand Up @@ -216,15 +232,21 @@ const Textarea = forwardRef<TextareaRefInterface, TextareaProps>((originalProps,
);

return (
<div style={style} ref={wrapperRef} className={classNames(`${classPrefix}-textarea`, className)}>
<div
style={style}
ref={wrapperRef}
className={classNames(`${classPrefix}-textarea`, className, {
[`${classPrefix}-textarea--clearable`]: clearable,
})}
>
<textarea
{...textareaProps}
{...eventProps}
rows={rows}
value={composingRef.current ? composingValue : value}
style={textareaStyle}
className={textareaClassNames}
readOnly={props.readOnly || props.readonly}
readOnly={readOnlyProp}
autoFocus={autofocus}
disabled={disabled}
onChange={inputValueChangeHandle}
Expand All @@ -235,6 +257,9 @@ const Textarea = forwardRef<TextareaRefInterface, TextareaProps>((originalProps,
onCompositionEnd={handleCompositionEnd}
ref={textareaRef}
/>
{isShowClearIcon ? (
<CloseCircleFilledIcon className={`${classPrefix}-textarea__clear`} onClick={handleClear} />
) : null}
{textTips || limitText ? (
<div
className={classNames(`${classPrefix}-textarea__info_wrapper`, {
Expand Down
39 changes: 35 additions & 4 deletions packages/components/textarea/__tests__/textarea.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,29 @@ describe('Textarea 组件测试', () => {
expect(container.querySelectorAll('.t-is-disabled')).not.toBeNull();
});

test('clearable', async () => {
const onChange = vi.fn();
const onClear = vi.fn();
const { container } = render(
<Textarea defaultValue="Hello TDesign" clearable onChange={onChange} onClear={onClear} />,
);

expect(container.querySelector('.t-textarea__clear')).not.toBeNull();

fireEvent.click(container.querySelector('.t-textarea__clear'));
expect(container.querySelector('textarea')).toHaveValue('');
expect(onChange).toHaveBeenCalledWith('', expect.objectContaining({ trigger: 'clear' }));
expect(onClear).toHaveBeenCalledTimes(1);
});

test('clearable hidden when disabled or readonly', async () => {
const { container: disabledContainer } = render(<Textarea value="Hello TDesign" clearable disabled />);
expect(disabledContainer.querySelector('.t-textarea__clear')).toBeNull();

const { container: readOnlyContainer } = render(<Textarea value="Hello TDesign" clearable readOnly />);
expect(readOnlyContainer.querySelector('.t-textarea__clear')).toBeNull();
});

// 测试输入
test('input', async () => {
render(<Textarea maxcharacter={5} />);
Expand All @@ -26,17 +49,23 @@ describe('Textarea 组件测试', () => {
fireEvent.change(document.querySelector('textarea'), { target: { value } });
expect(document.querySelector('textarea').textContent).toBe(value);

fireEvent.change(document.querySelector('textarea'), { target: { value: 'hi,tzmax' } });
fireEvent.change(document.querySelector('textarea'), {
target: { value: 'hi,tzmax' },
});
expect(document.querySelector('textarea').textContent.length).toBe(5);

const onChange = vi.fn();
const { container } = render(<Textarea maxLength={1} onChange={onChange} />);
fireEvent.compositionStart(container.querySelector('textarea'));
fireEvent.change(container.querySelector('textarea'), { target: { value: 'tian' } });
fireEvent.change(container.querySelector('textarea'), {
target: { value: 'tian' },
});
fireEvent.compositionEnd(container.querySelector('textarea'), {
currentTarget: { value: '天' },
});
fireEvent.change(container.querySelector('textarea'), { target: { value: '天' } });
fireEvent.change(container.querySelector('textarea'), {
target: { value: '天' },
});
expect(onChange).toHaveBeenLastCalledWith('天', expect.objectContaining({}));
});

Expand Down Expand Up @@ -82,7 +111,9 @@ describe('Textarea 组件测试', () => {

event = null;
changeValue = '';
fireEvent.change(document.querySelector('textarea'), { target: { value: 'hi,tzmax' } });
fireEvent.change(document.querySelector('textarea'), {
target: { value: 'hi,tzmax' },
});
expect(changeValue).not.toBeNull();
expect(event).not.toBeNull();
});
Expand Down
20 changes: 20 additions & 0 deletions packages/components/textarea/_example/clearable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React, { useState } from 'react';
import { Textarea } from 'tdesign-react';

export default function TextareaClearable() {
const [value, setValue] = useState('这是一段可以被快速清空的多行文本。');

return (
<Textarea
value={value}
clearable
placeholder="请输入内容"
onChange={(value) => {
setValue(value);
}}
onClear={() => {
console.log('onClear');
}}
/>
);
}
8 changes: 7 additions & 1 deletion packages/components/textarea/_usage/props.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@
"defaultValue": false,
"options": []
},
{
"name": "clearable",
"type": "Boolean",
"defaultValue": false,
"options": []
},
{
"name": "disabled",
"type": "Boolean",
Expand All @@ -23,4 +29,4 @@
"defaultValue": false,
"options": []
}
]
]
1 change: 1 addition & 0 deletions packages/components/textarea/defaultProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const textareaDefaultProps: TdTextareaProps = {
allowInputOverMax: false,
autofocus: false,
autosize: false,
clearable: false,
disabled: false,
placeholder: undefined,
readonly: false,
Expand Down
50 changes: 26 additions & 24 deletions packages/components/textarea/textarea.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,29 @@

### Textarea Props

name | type | default | description | required
-- | -- | -- | -- | --
className | String | - | className of component | N
style | Object | - | CSS(Cascading Style Sheets),Typescript: `React.CSSProperties` | N
allowInputOverMax | Boolean | false | Allow input after exceeding `maxlength` or `maxcharacter` | N
autofocus | Boolean | false | \- | N
autosize | Boolean / Object | false | Typescript: `boolean \| { minRows?: number; maxRows?: number }` | N
count | Boolean / Function | - | Character counter. It is enabled by default when `maxLength` or `maxCharacter` is set.。Typescript: `boolean \| ((ctx: { value: string; count: number; maxLength?: number; maxCharacter?: number }) => TNode)`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N
disabled | Boolean | false | \- | N
maxcharacter | Number | - | \- | N
maxlength | Number | - | Typescript: `number` | N
name | String | - | \- | N
placeholder | String | undefined | \- | N
readOnly | Boolean | false | \- | N
status | String | default | options:default/success/warning/error | N
tips | TNode | - | Typescript: `string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N
value | String | - | Typescript: `TextareaValue` `type TextareaValue = string`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/textarea/type.ts) | N
defaultValue | String | - | uncontrolled property。Typescript: `TextareaValue` `type TextareaValue = string`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/textarea/type.ts) | N
onBlur | Function | | Typescript: `(value: TextareaValue, context: { e: FocusEvent }) => void`<br/> | N
onChange | Function | | Typescript: `(value: TextareaValue, context?: { e?: InputEvent }) => void`<br/> | N
onFocus | Function | | Typescript: `(value: TextareaValue, context : { e: FocusEvent }) => void`<br/> | N
onKeydown | Function | | Typescript: `(value: TextareaValue, context: { e: KeyboardEvent }) => void`<br/> | N
onKeypress | Function | | Typescript: `(value: TextareaValue, context: { e: KeyboardEvent }) => void`<br/> | N
onKeyup | Function | | Typescript: `(value: TextareaValue, context: { e: KeyboardEvent }) => void`<br/> | N
| name | type | default | description | required |
| ----------------- | ------------------ | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
| className | String | - | className of component | N |
| style | Object | - | CSS(Cascading Style Sheets),Typescript: `React.CSSProperties` | N |
| allowInputOverMax | Boolean | false | Allow input after exceeding `maxlength` or `maxcharacter` | N |
| autofocus | Boolean | false | \- | N |
| autosize | Boolean / Object | false | Typescript: `boolean \| { minRows?: number; maxRows?: number }` | N |
| clearable | Boolean | false | Whether the textarea is clearable | N |
| count | Boolean / Function | - | Character counter. It is enabled by default when `maxLength` or `maxCharacter` is set.。Typescript: `boolean \| ((ctx: { value: string; count: number; maxLength?: number; maxCharacter?: number }) => TNode)`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N |
| disabled | Boolean | false | \- | N |
| maxcharacter | Number | - | \- | N |
| maxlength | Number | - | Typescript: `number` | N |
| name | String | - | \- | N |
| placeholder | String | undefined | \- | N |
| readOnly | Boolean | false | \- | N |
| status | String | default | options:default/success/warning/error | N |
| tips | TNode | - | Typescript: `string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N |
| value | String | - | Typescript: `TextareaValue` `type TextareaValue = string`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/textarea/type.ts) | N |
| defaultValue | String | - | uncontrolled property。Typescript: `TextareaValue` `type TextareaValue = string`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/textarea/type.ts) | N |
| onBlur | Function | | Typescript: `(value: TextareaValue, context: { e: FocusEvent }) => void`<br/> | N |
| onChange | Function | | Typescript: `(value: TextareaValue, context?: { e?: InputEvent \| MouseEvent; trigger?: 'input' \| 'clear' }) => void`<br/>`trigger=clear` means it is triggered by clicking the clear button | N |
| onClear | Function | | Typescript: `(context: { e: MouseEvent }) => void`<br/>Triggered when the clear button is clicked | N |
| onFocus | Function | | Typescript: `(value: TextareaValue, context : { e: FocusEvent }) => void`<br/> | N |
| onKeydown | Function | | Typescript: `(value: TextareaValue, context: { e: KeyboardEvent }) => void`<br/> | N |
| onKeypress | Function | | Typescript: `(value: TextareaValue, context: { e: KeyboardEvent }) => void`<br/> | N |
| onKeyup | Function | | Typescript: `(value: TextareaValue, context: { e: KeyboardEvent }) => void`<br/> | N |
Loading
Loading