Skip to content
Merged
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
64 changes: 42 additions & 22 deletions packages/components/textarea/Textarea.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import React, { forwardRef, useState, useEffect, useMemo, useRef, useImperativeHandle } from 'react';
import React, { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
import classNames from 'classnames';
import { getCharacterLength, getUnicodeLength, limitUnicodeMaxLength } from '@tdesign/common-js/utils/helper';

import calcTextareaHeight from '@tdesign/common-js/utils/calcTextareaHeight';
import useConfig from '../hooks/useConfig';
import { TdTextareaProps } from './type';
import { StyledProps } from '../common';
import { getCharacterLength, getUnicodeLength, limitUnicodeMaxLength } from '@tdesign/common-js/utils/helper';
import noop from '../_util/noop';
import parseTNode from '../_util/parseTNode';
import useConfig from '../hooks/useConfig';
import useControlled from '../hooks/useControlled';
import { textareaDefaultProps } from './defaultProps';
import useDefaultProps from '../hooks/useDefaultProps';
import useIsomorphicLayoutEffect from '../hooks/useLayoutEffect';
import useEventCallback from '../hooks/useEventCallback';
import useIsomorphicLayoutEffect from '../hooks/useLayoutEffect';
import { textareaDefaultProps } from './defaultProps';

import type { StyledProps } from '../common';
import type { TdTextareaProps } from './type';

const DEFAULT_TEXTAREA_STYLE = { height: 'auto', minHeight: 'auto' };

Expand All @@ -29,6 +32,7 @@ export interface TextareaRefInterface {
const Textarea = forwardRef<TextareaRefInterface, TextareaProps>((originalProps, ref) => {
const props = useDefaultProps<TextareaProps>(originalProps, textareaDefaultProps);
const {
count,
disabled,
maxlength,
maxcharacter,
Expand All @@ -46,16 +50,19 @@ const Textarea = forwardRef<TextareaRefInterface, TextareaProps>((originalProps,
rows,
...otherProps
} = props;
const hasMaxcharacter = typeof maxcharacter !== 'undefined';

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

const [isFocused, setIsFocused] = useState(false);
const [isOvermax, setIsOvermax] = useState(false);
const [textareaStyle, setTextareaStyle] = useState<Partial<typeof DEFAULT_TEXTAREA_STYLE>>(DEFAULT_TEXTAREA_STYLE);
const composingRef = useRef(false);
const [composingValue, setComposingValue] = useState<string>('');
const hasMaxcharacter = typeof maxcharacter !== 'undefined';

const composingRef = useRef(false);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const wrapperRef = useRef<HTMLDivElement>(null);

const currentLength = useMemo(() => getUnicodeLength(value), [value]);
const characterLength = useMemo(() => {
const characterInfo = getCharacterLength(String(value), allowInputOverMax ? Infinity : maxcharacter);
Expand Down Expand Up @@ -141,16 +148,31 @@ const Textarea = forwardRef<TextareaRefInterface, TextareaProps>((originalProps,
}
}

const renderLimitText = (current: number, max: number) => (
<span className={`${classPrefix}-textarea__limit`}>
{isOvermax && allowInputOverMax ? (
<span className={`${classPrefix}-textarea__tips--warning`}> {current}</span>
) : (
`${current}`
)}
{`/${max}`}
</span>
);
const renderLimitText = (current: number, max: number) => {
if (count === false) return null;

// 不设置 maxLength 或 maxCharacter,也支持渲染自定义节点
if (typeof count === 'function') {
return parseTNode(count, {
value,
count: current,
maxLength: hasMaxcharacter ? undefined : maxlength,
maxCharacter: hasMaxcharacter ? maxcharacter : undefined,
});
}

if (!max) return;
return (
<span className={`${classPrefix}-textarea__limit`}>
{isOvermax && allowInputOverMax ? (
<span className={`${classPrefix}-textarea__tips--warning`}> {current}</span>
) : (
`${current}`
)}
{`/${max}`}
</span>
);
};

useIsomorphicLayoutEffect(() => {
if (autosize === false) {
Expand Down Expand Up @@ -187,9 +209,7 @@ const Textarea = forwardRef<TextareaRefInterface, TextareaProps>((originalProps,
</div>
);

const limitText =
(hasMaxcharacter && renderLimitText(characterLength, maxcharacter)) ||
(!hasMaxcharacter && maxlength && renderLimitText(currentLength, maxlength));
const limitText = renderLimitText(currentLength, maxlength ?? maxcharacter);

return (
<div style={style} ref={wrapperRef} className={classNames(`${classPrefix}-textarea`, className)}>
Expand Down
15 changes: 14 additions & 1 deletion packages/components/textarea/_example/maxlength.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { Textarea, Space } from 'tdesign-react';
import { Space, Textarea } from 'tdesign-react';

export default function InputExample() {
return (
Expand All @@ -12,6 +12,19 @@ export default function InputExample() {
maxcharacter={20}
allowInputOverMax
/>
<Textarea
placeholder="自定义计数元素"
maxlength={20}
count={(ctx) => {
const isMaxReached = ctx.count >= ctx.maxLength;
return (
<div style={{ fontSize: '12px' }}>
<span style={{ color: isMaxReached ? 'red' : 'gray' }}>{ctx.count}</span>/
<span style={{ color: 'mediumblue' }}>{ctx.maxLength}</span>
</div>
);
}}
/>
</Space>
);
}
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,
disabled: false,
placeholder: undefined,
readonly: false,
};
13 changes: 7 additions & 6 deletions packages/components/textarea/textarea.en-US.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
:: BASE_DOC ::

## API

### 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 | \- | 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
label | TNode | - | Typescript: `string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N
maxcharacter | Number | - | \- | N
maxlength | Number | - | \- | N
maxlength | Number | - | Typescript: `number` | N
name | String | - | \- | N
placeholder | String | undefined | \- | N
readonly | Boolean | false | \- | N
status | String | - | optionsdefault/success/warning/error | N
status | String | - | 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 / Number | - | 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 / Number | - | 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
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
Expand Down
9 changes: 5 additions & 4 deletions packages/components/textarea/textarea.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
:: BASE_DOC ::

## API

### Textarea Props

名称 | 类型 | 默认值 | 描述 | 必传
-- | -- | -- | -- | --
className | String | - | 类名 | N
style | Object | - | 样式,TS 类型:`React.CSSProperties` | N
allowInputOverMax | Boolean | false | 超出maxlength或maxcharacter之后是否还允许输入 | N
allowInputOverMax | Boolean | false | 超出 `maxlength` 或 `maxcharacter` 之后是否还允许输入 | N
autofocus | Boolean | false | 自动聚焦,拉起键盘 | N
autosize | Boolean / Object | false | 高度自动撑开。 autosize = true 表示组件高度自动撑开,同时,依旧允许手动拖高度。如果设置了 autosize.maxRows 或者 autosize.minRows 则不允许手动调整高度。TS 类型:`boolean \| { minRows?: number; maxRows?: number }` | N
count | Boolean / Function | - | 文字计数元素。设置 `maxlength` 或 `maxchanacter` 时,默认为 true。TS 类型:`boolean \| ((ctx: { value: string; count: number; maxLength?: number; maxCharacter?: number }) => TNode)`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N
disabled | Boolean | false | 是否禁用文本框 | N
label | TNode | - | 左侧文本。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N
maxcharacter | Number | - | 用户最多可以输入的字符个数,一个中文汉字表示两个字符长度 | N
maxlength | Number | - | 用户最多可以输入的字符个数 | N
name | String | - | 名称,HTML 元素原生属性 | N
placeholder | String | undefined | 占位符 | N
readonly | Boolean | false | 只读状态 | N
status | String | - | 文本框状态。可选项:default/success/warning/error | N
tips | TNode | - | 输入框下方提示文本,会根据不同的 `status` 呈现不同的样式。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N
value | String / Number | - | 文本框值。TS 类型:`TextareaValue` `type TextareaValue = string`。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/textarea/type.ts) | N
defaultValue | String / Number | - | 文本框值。非受控属性。TS 类型:`TextareaValue` `type TextareaValue = string`。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/textarea/type.ts) | N
value | String | - | 文本框值。TS 类型:`TextareaValue` `type TextareaValue = string`。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/textarea/type.ts) | N
defaultValue | String | - | 文本框值。非受控属性。TS 类型:`TextareaValue` `type TextareaValue = string`。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/textarea/type.ts) | N
onBlur | Function | | TS 类型:`(value: TextareaValue, context: { e: FocusEvent }) => void`<br/>失去焦点时触发 | N
onChange | Function | | TS 类型:`(value: TextareaValue, context?: { e?: InputEvent }) => void`<br/>输入内容变化时触发 | N
onFocus | Function | | TS 类型:`(value: TextareaValue, context : { e: FocusEvent }) => void`<br/>获得焦点时触发 | N
Expand Down
10 changes: 5 additions & 5 deletions packages/components/textarea/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { KeyboardEvent, FocusEvent, FormEvent } from 'react';

export interface TdTextareaProps {
/**
* 超出maxlength或maxcharacter之后是否还允许输入
* 超出 `maxlength` 或 `maxcharacter` 之后是否还允许输入
* @default false
*/
allowInputOverMax?: boolean;
Expand All @@ -23,15 +23,15 @@ export interface TdTextareaProps {
* @default false
*/
autosize?: boolean | { minRows?: number; maxRows?: number };
/**
* 文字计数元素。设置 `maxlength` 或 `maxchanacter` 时,默认为 true
*/
count?: boolean | ((ctx: { value: string; count: number; maxLength?: number; maxCharacter?: number }) => TNode);
/**
* 是否禁用文本框
* @default false
*/
disabled?: boolean;
/**
* 左侧文本
*/
label?: TNode;
/**
* 用户最多可以输入的字符个数,一个中文汉字表示两个字符长度
*/
Expand Down
34 changes: 33 additions & 1 deletion test/snap/__snapshots__/csr.test.jsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -135472,6 +135472,38 @@ exports[`csr snapshot test > csr test packages/components/textarea/_example/maxl
</div>
</div>
</div>
<div
class="t-space-item"
>
<div
class="t-textarea"
>
<textarea
class="t-textarea__inner"
placeholder="自定义计数元素"
style="height: auto; min-height: auto;"
/>
<div
class="t-textarea__info_wrapper t-textarea__info_wrapper_align"
>
<div
style="font-size: 12px;"
>
<span
style="color: gray;"
>
0
</span>
/
<span
style="color: mediumblue;"
>
20
</span>
</div>
</div>
</div>
</div>
</div>
</div>
`;
Expand Down Expand Up @@ -150888,7 +150920,7 @@ exports[`ssr snapshot test > ssr test packages/components/textarea/_example/base

exports[`ssr snapshot test > ssr test packages/components/textarea/_example/events.tsx 1`] = `"<div class="t-textarea"><textarea placeholder="请输入内容" style="height:auto;min-height:auto" class="t-textarea__inner"></textarea></div>"`;

exports[`ssr snapshot test > ssr test packages/components/textarea/_example/maxlength.tsx 1`] = `"<div style="width:100%;gap:16px" class="t-space t-space-vertical"><div class="t-space-item"><div class="t-textarea"><textarea placeholder="请输入内容,超出限制无法输入" style="height:auto;min-height:auto" class="t-textarea__inner"></textarea><div class="t-textarea__info_wrapper"><div class="t-textarea__tips t-textarea__tips--normal">这里可以放一些提示文字</div><span class="t-textarea__limit">0<!-- -->/20</span></div></div></div><div class="t-space-item"><div class="t-textarea"><textarea placeholder="请输入内容,超出限制可以输入" style="height:auto;min-height:auto" class="t-textarea__inner"></textarea><div class="t-textarea__info_wrapper t-textarea__info_wrapper_align"><span class="t-textarea__limit">0<!-- -->/20</span></div></div></div><div class="t-space-item"><div class="t-textarea"><textarea placeholder="请输入内容,一个中文汉字表示两个字符长度,超出限制无法输入" style="height:auto;min-height:auto" class="t-textarea__inner"></textarea><div class="t-textarea__info_wrapper t-textarea__info_wrapper_align"><span class="t-textarea__limit">0<!-- -->/20</span></div></div></div><div class="t-space-item"><div class="t-textarea"><textarea placeholder="请输入内容,一个中文汉字表示两个字符长度,超出限制可以输入" style="height:auto;min-height:auto" class="t-textarea__inner"></textarea><div class="t-textarea__info_wrapper t-textarea__info_wrapper_align"><span class="t-textarea__limit">0<!-- -->/20</span></div></div></div></div>"`;
exports[`ssr snapshot test > ssr test packages/components/textarea/_example/maxlength.tsx 1`] = `"<div style="width:100%;gap:16px" class="t-space t-space-vertical"><div class="t-space-item"><div class="t-textarea"><textarea placeholder="请输入内容,超出限制无法输入" style="height:auto;min-height:auto" class="t-textarea__inner"></textarea><div class="t-textarea__info_wrapper"><div class="t-textarea__tips t-textarea__tips--normal">这里可以放一些提示文字</div><span class="t-textarea__limit">0<!-- -->/20</span></div></div></div><div class="t-space-item"><div class="t-textarea"><textarea placeholder="请输入内容,超出限制可以输入" style="height:auto;min-height:auto" class="t-textarea__inner"></textarea><div class="t-textarea__info_wrapper t-textarea__info_wrapper_align"><span class="t-textarea__limit">0<!-- -->/20</span></div></div></div><div class="t-space-item"><div class="t-textarea"><textarea placeholder="请输入内容,一个中文汉字表示两个字符长度,超出限制无法输入" style="height:auto;min-height:auto" class="t-textarea__inner"></textarea><div class="t-textarea__info_wrapper t-textarea__info_wrapper_align"><span class="t-textarea__limit">0<!-- -->/20</span></div></div></div><div class="t-space-item"><div class="t-textarea"><textarea placeholder="请输入内容,一个中文汉字表示两个字符长度,超出限制可以输入" style="height:auto;min-height:auto" class="t-textarea__inner"></textarea><div class="t-textarea__info_wrapper t-textarea__info_wrapper_align"><span class="t-textarea__limit">0<!-- -->/20</span></div></div></div><div class="t-space-item"><div class="t-textarea"><textarea placeholder="自定义计数元素" style="height:auto;min-height:auto" class="t-textarea__inner"></textarea><div class="t-textarea__info_wrapper t-textarea__info_wrapper_align"><div style="font-size:12px"><span style="color:gray">0</span>/<span style="color:mediumblue">20</span></div></div></div></div></div>"`;

exports[`ssr snapshot test > ssr test packages/components/textarea/_example/type.tsx 1`] = `"<div style="width:100%;gap:16px" class="t-space t-space-vertical"><div class="t-space-item"><div class="t-textarea"><textarea placeholder="请输入内容" style="height:auto;min-height:auto" class="t-textarea__inner" readonly="">只读状态</textarea></div></div><div class="t-space-item"><div class="t-textarea"><textarea placeholder="请输入内容" style="height:auto;min-height:auto" class="t-textarea__inner t-is-disabled" disabled="">禁用状态</textarea></div></div><div class="t-space-item"><div class="t-textarea"><textarea placeholder="normal" style="height:auto;min-height:auto" class="t-textarea__inner">普通状态</textarea><div class="t-textarea__info_wrapper"><div class="t-textarea__tips t-textarea__tips--normal">正常提示</div></div></div></div><div class="t-space-item"><div class="t-textarea"><textarea placeholder="success" style="height:auto;min-height:auto" class="t-textarea__inner t-is-success">成功状态</textarea><div class="t-textarea__info_wrapper"><div class="t-textarea__tips t-textarea__tips--success">成功提示</div></div></div></div><div class="t-space-item"><div class="t-textarea"><textarea placeholder="warning" style="height:auto;min-height:auto" class="t-textarea__inner t-is-warning">警告状态</textarea><div class="t-textarea__info_wrapper"><div class="t-textarea__tips t-textarea__tips--warning">警告提示</div></div></div></div><div class="t-space-item"><div class="t-textarea"><textarea placeholder="error" style="height:auto;min-height:auto" class="t-textarea__inner t-is-error">错误状态</textarea><div class="t-textarea__info_wrapper"><div class="t-textarea__tips t-textarea__tips--error">错误提示</div></div></div></div></div>"`;

Expand Down
Loading
Loading