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
46 changes: 46 additions & 0 deletions packages/components/textarea/__tests__/textarea.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,37 @@ describe('Textarea', () => {
value.value = '123456';
expect(textarea.element.value).toBe('12345');
});

it(':clearable', async () => {
const wrapper = mount(() => <Textarea value="text" clearable />);
expect(wrapper.find('.t-textarea__clear').exists()).toBeTruthy();
expect(wrapper.find('.t-textarea__clear--visible').exists()).toBeFalsy();

await wrapper.trigger('mouseenter');
expect(wrapper.find('.t-textarea__clear--visible').exists()).toBeTruthy();
});

it(':clearable should show when value is number zero', async () => {
const wrapper = mount(() => <Textarea value={0} clearable />);

await wrapper.trigger('mouseenter');

expect(wrapper.find('.t-textarea__clear--visible').exists()).toBeTruthy();
});

it(':clearable should not show when empty, disabled or readonly', async () => {
const emptyWrapper = mount(() => <Textarea value="" clearable />);
await emptyWrapper.trigger('mouseenter');
expect(emptyWrapper.find('.t-textarea__clear--visible').exists()).toBeFalsy();

const disabledWrapper = mount(() => <Textarea value="text" clearable disabled />);
await disabledWrapper.trigger('mouseenter');
expect(disabledWrapper.find('.t-textarea__clear').exists()).toBeFalsy();

const readonlyWrapper = mount(() => <Textarea value="text" clearable readonly />);
await readonlyWrapper.trigger('mouseenter');
expect(readonlyWrapper.find('.t-textarea__clear').exists()).toBeFalsy();
});
});

describe(':events', () => {
Expand Down Expand Up @@ -149,5 +180,20 @@ describe('Textarea', () => {
await textarea.trigger('keyup');
expect(fn).toBeCalled();
});

it(':onClear', async () => {
const onClear = vi.fn();
const onChange = vi.fn();
const wrapper = mount(() => <Textarea defaultValue="text" clearable onClear={onClear} onChange={onChange} />);
const textarea = wrapper.find('textarea');

await wrapper.trigger('mouseenter');
await wrapper.find('.t-textarea__clear').trigger('click');

expect(onClear).toHaveBeenCalledTimes(1);
expect(onClear.mock.calls[0][0].e.type).toBe('click');
expect(onChange).toHaveBeenCalledWith('', expect.objectContaining({ trigger: 'clear' }));
expect(textarea.element.value).toBe('');
});
});
});
16 changes: 16 additions & 0 deletions packages/components/textarea/_example/clearable.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<template>
<div style="width: 100%; max-width: 500px">
<t-textarea v-model="value" clearable placeholder="请输入" @clear="onClear" />
</div>
</template>

<script lang="ts" setup>
import { ref } from 'vue';
import type { TextareaProps } from 'tdesign-vue-next';

const value = ref('Hello TDesign');

const onClear: TextareaProps['onClear'] = () => {
console.log('clear');
};
</script>
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": []
}
]
]
4 changes: 4 additions & 0 deletions packages/components/textarea/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export default {
type: [Boolean, Object] as PropType<TdTextareaProps['autosize']>,
default: false as TdTextareaProps['autosize'],
},
/** 是否可清空 */
clearable: Boolean,
/** 是否禁用文本框 */
disabled: {
type: Boolean,
Expand Down Expand Up @@ -75,6 +77,8 @@ export default {
onBlur: Function as PropType<TdTextareaProps['onBlur']>,
/** 输入内容变化时触发 */
onChange: Function as PropType<TdTextareaProps['onChange']>,
/** 清空按钮点击时触发 */
onClear: Function as PropType<TdTextareaProps['onClear']>,
/** 获得焦点时触发 */
onFocus: Function as PropType<TdTextareaProps['onFocus']>,
/** 键盘按下时触发 */
Expand Down
13 changes: 11 additions & 2 deletions packages/components/textarea/textarea.en-US.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
:: BASE_DOC ::

### Clearable Textarea

Textarea with a clear operation can quickly clear entered content.

{{ clearable }}

## API

### Textarea Props
Expand All @@ -19,8 +25,10 @@ status | String | default | options: default/success/warning/error | N
tips | String / Slot / Function | - | Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N
value | String / Number | - | `v-model` and `v-model:value` is supported。Typescript:`TextareaValue` `type TextareaValue = string \| number`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/textarea/type.ts) | N
defaultValue | String / Number | - | uncontrolled property。Typescript:`TextareaValue` `type TextareaValue = string \| number`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/textarea/type.ts) | N
clearable | Boolean | false | Whether the content can be cleared | N
onBlur | Function | | Typescript:`(value: TextareaValue, context: { e: FocusEvent }) => void`<br/> | N
onChange | Function | | Typescript:`(value: TextareaValue, context?: { e?: InputEvent }) => void`<br/> | N
onChange | Function | | Typescript:`(value: TextareaValue, context?: { e?: InputEvent \| MouseEvent; trigger?: 'input' \| 'clear' }) => void`<br/> | N
onClear | Function | | Typescript:`(context: { e: MouseEvent }) => 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
Expand All @@ -32,7 +40,8 @@ onValidate | Function | | Typescript:`(context: { error?: 'exceed-maximum' \|
name | params | description
-- | -- | --
blur | `(value: TextareaValue, context: { e: FocusEvent })` | \-
change | `(value: TextareaValue, context?: { e?: InputEvent })` | \-
change | `(value: TextareaValue, context?: { e?: InputEvent \| MouseEvent; trigger?: 'input' \| 'clear' })` | \-
clear | `(context: { e: MouseEvent })` | \-
focus | `(value: TextareaValue, context : { e: FocusEvent })` | \-
keydown | `(value: TextareaValue, context: { e: KeyboardEvent })` | \-
keypress | `(value: TextareaValue, context: { e: KeyboardEvent })` | \-
Expand Down
13 changes: 11 additions & 2 deletions packages/components/textarea/textarea.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
:: BASE_DOC ::

### 可清空的多行文本框

带清空操作的多行文本框,可快捷清空输入过的内容。

{{ clearable }}

## API

### Textarea Props
Expand All @@ -19,8 +25,10 @@ status | String | default | 文本框状态。可选项:default/success/warnin
tips | String / Slot / Function | - | 输入框下方提示文本,会根据不同的 `status` 呈现不同的样式。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N
value | String / Number | - | 文本框值。支持语法糖 `v-model` 或 `v-model:value`。TS 类型:`TextareaValue` `type TextareaValue = string \| number`。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/textarea/type.ts) | N
defaultValue | String / Number | - | 文本框值。非受控属性。TS 类型:`TextareaValue` `type TextareaValue = string \| number`。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/textarea/type.ts) | N
clearable | Boolean | false | 是否可清空 | N
onBlur | Function | | TS 类型:`(value: TextareaValue, context: { e: FocusEvent }) => void`<br/>失去焦点时触发 | N
onChange | Function | | TS 类型:`(value: TextareaValue, context?: { e?: InputEvent }) => void`<br/>输入内容变化时触发 | N
onChange | Function | | TS 类型:`(value: TextareaValue, context?: { e?: InputEvent \| MouseEvent; trigger?: 'input' \| 'clear' }) => void`<br/>输入内容变化时触发 | N
onClear | Function | | TS 类型:`(context: { e: MouseEvent }) => void`<br/>清空按钮点击时触发 | N
onFocus | Function | | TS 类型:`(value: TextareaValue, context : { e: FocusEvent }) => void`<br/>获得焦点时触发 | N
onKeydown | Function | | TS 类型:`(value: TextareaValue, context: { e: KeyboardEvent }) => void`<br/>键盘按下时触发 | N
onKeypress | Function | | TS 类型:`(value: TextareaValue, context: { e: KeyboardEvent }) => void`<br/>按下字符键时触发(keydown -> keypress -> keyup) | N
Expand All @@ -32,7 +40,8 @@ onValidate | Function | | TS 类型:`(context: { error?: 'exceed-maximum' \|
名称 | 参数 | 描述
-- | -- | --
blur | `(value: TextareaValue, context: { e: FocusEvent })` | 失去焦点时触发
change | `(value: TextareaValue, context?: { e?: InputEvent })` | 输入内容变化时触发
change | `(value: TextareaValue, context?: { e?: InputEvent \| MouseEvent; trigger?: 'input' \| 'clear' })` | 输入内容变化时触发
clear | `(context: { e: MouseEvent })` | 清空按钮点击时触发
focus | `(value: TextareaValue, context : { e: FocusEvent })` | 获得焦点时触发
keydown | `(value: TextareaValue, context: { e: KeyboardEvent })` | 键盘按下时触发
keypress | `(value: TextareaValue, context: { e: KeyboardEvent })` | 按下字符键时触发(keydown -> keypress -> keyup)
Expand Down
56 changes: 53 additions & 3 deletions packages/components/textarea/textarea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import {
StyleValue,
CSSProperties,
} from 'vue';
import { isObject, merge, omit } from 'lodash-es';
import { isNil, isObject, merge, omit } from 'lodash-es';
import { CloseCircleFilledIcon as TdCloseCircleFilledIcon } from 'tdesign-icons-vue-next';

import { FormItemInjectionKey } from '../form/constants';
import setStyle from '@tdesign/common-js/utils/setStyle';
Expand All @@ -24,6 +25,7 @@ import {
useTNodeJSX,
usePrefixClass,
useCommonClassName,
useGlobalIcon,
} from '@tdesign/shared-hooks';

import { useLengthLimit } from '../input/hooks/useLengthLimit';
Expand All @@ -42,6 +44,9 @@ export default defineComponent({
const name = usePrefixClass('textarea');
const TEXTAREA_TIPS_CLASS = computed(() => `${name.value}__tips`);
const TEXTAREA_LIMIT = computed(() => `${name.value}__limit`);
const { CloseCircleFilledIcon } = useGlobalIcon({
CloseCircleFilledIcon: TdCloseCircleFilledIcon,
});

const { value, modelValue } = toRefs(props);
const [innerValue, setInnerValue] = useVModel(value, modelValue, props.defaultValue, props.onChange);
Expand All @@ -51,6 +56,7 @@ export default defineComponent({

const refTextareaElem = ref<HTMLTextAreaElement>();
const focused = ref(false);
const isHover = ref(false);
const isComposing = ref(false);

const focus = () => refTextareaElem.value?.focus();
Expand Down Expand Up @@ -95,7 +101,7 @@ export default defineComponent({
val = typeof stringInfo === 'object' && stringInfo.characters;
}
}
!isComposing.value && setInnerValue(val, { e });
!isComposing.value && setInnerValue(val, { e, trigger: 'input' });
nextTick(() => setInputValue(val));
adjustTextareaHeight();
};
Expand Down Expand Up @@ -143,16 +149,44 @@ export default defineComponent({
formItem?.handleBlur();
};

const getClearEvent = (context: MouseEvent | { e: MouseEvent }) => ('e' in context ? context.e : context);

const emitClear = (context: MouseEvent | { e: MouseEvent }) => {
const e = getClearEvent(context);
setInnerValue('', { e, trigger: 'clear' });
props.onClear?.({ e });
nextTick(() => {
refTextareaElem.value?.focus();
adjustTextareaHeight();
});
};

const onClearMouseDown = (context: MouseEvent | { e: MouseEvent }) => {
const e = getClearEvent(context);
e.preventDefault();
};

// computed
const textareaClasses = computed(() => {
return [
name.value,
{
[`${prefix.value}-is-disabled`]: disabled.value,
[`${prefix.value}-is-readonly`]: isReadonly.value,
[`${name.value}--clearable`]: props.clearable,
},
];
});
const showClear = computed(() => {
return Boolean(
props.clearable &&
!isNil(innerValue.value) &&
innerValue.value !== '' &&
!disabled.value &&
!isReadonly.value &&
isHover.value,
);
});
const inputAttrs = computed<Record<string, any>>(() => {
return getValidAttrs({
autofocus: props.autofocus,
Expand Down Expand Up @@ -255,7 +289,16 @@ export default defineComponent({
));

return (
<div class={textareaClasses.value} {...omit(attrs, ['style'])}>
<div
class={textareaClasses.value}
onMouseenter={() => {
isHover.value = true;
}}
onMouseleave={() => {
isHover.value = false;
}}
{...omit(attrs, ['style'])}
>
<textarea
onInput={handleInput}
onCompositionstart={onCompositionstart}
Expand All @@ -266,6 +309,13 @@ export default defineComponent({
{...inputEvents}
{...inputAttrs.value}
></textarea>
{props.clearable && !disabled.value && !isReadonly.value ? (
<CloseCircleFilledIcon
class={[`${name.value}__clear`, { [`${name.value}__clear--visible`]: showClear.value }]}
onMousedown={onClearMouseDown}
onClick={emitClear}
/>
) : null}
{textTips || limitText ? (
<div
class={[
Expand Down
11 changes: 10 additions & 1 deletion packages/components/textarea/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ export interface TdTextareaProps {
* @default false
*/
autosize?: boolean | { minRows?: number; maxRows?: number };
/**
* 是否可清空
* @default false
*/
clearable?: boolean;
/**
* 是否禁用文本框
*/
Expand Down Expand Up @@ -75,7 +80,11 @@ export interface TdTextareaProps {
/**
* 输入内容变化时触发
*/
onChange?: (value: TextareaValue, context?: { e?: InputEvent }) => void;
onChange?: (value: TextareaValue, context?: { e?: InputEvent | MouseEvent; trigger?: 'input' | 'clear' }) => void;
/**
* 清空按钮点击时触发
*/
onClear?: (context: { e: MouseEvent }) => void;
/**
* 获得焦点时触发
*/
Expand Down
Loading