diff --git a/package-lock.json b/package-lock.json
index ea9b30e6..22f935fd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
- "name": "@tencent/tdesign-web-components",
- "version": "0.0.0",
+ "name": "tdesign-web-components",
+ "version": "0.0.1-alpha.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
- "name": "@tencent/tdesign-web-components",
- "version": "0.0.0",
+ "name": "tdesign-web-components",
+ "version": "0.0.1-alpha.0",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.24.7",
diff --git a/site/sidebar.config.ts b/site/sidebar.config.ts
index 6fdc10a8..85322b71 100644
--- a/site/sidebar.config.ts
+++ b/site/sidebar.config.ts
@@ -91,6 +91,12 @@ export default [
path: '/components/switch',
component: () => import('tdesign-web-components/switch/README.md'),
},
+ {
+ title: 'Textarea 文本框',
+ name: 'textarea',
+ path: '/components/textarea',
+ component: () => import('tdesign-web-components/textarea/README.md'),
+ },
],
},
{
diff --git a/src/index.ts b/src/index.ts
index cffa1f5d..6e224fed 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,4 +1,5 @@
export * from './button';
export * from './icon';
+export * from './textarea';
export * from './space';
export * from './switch';
diff --git a/src/textarea/README.md b/src/textarea/README.md
new file mode 100644
index 00000000..f6bb4ccc
--- /dev/null
+++ b/src/textarea/README.md
@@ -0,0 +1,59 @@
+---
+title: Textarea 文本框
+description: 通过鼠标或键盘输入多行内容。
+isComponent: true
+usage: { title: '', description: '' }
+spline: base
+---
+
+### 基础使用
+
+高度自适应
+
+{{ base }}
+
+### 自定义事件
+
+{{ event }}
+
+### 字数限制
+
+支持中文和英文字符数限制
+
+{{ limit }}
+
+### 自定义状态
+
+支持禁用、只读,和default/success/warning/error四种status状态
+
+{{ status }}
+
+
+
+
+## API
+
+### Textarea Props
+
+名称 | 类型 | 默认值 | 说明 | 必传
+-- | -- | -- | -- | --
+allowInputOverMax | Boolean | false | 超出`maxlength`或`maxcharacter`之后是否还允许输入 | N
+autofocus | Boolean | false | 自动聚焦,拉起键盘 | N
+autosize | Boolean/Object | false | 高度自动撑开。Object属性:`minRows:number,maxRows:number`。autosize = true 表示组件高度自动撑开,同时,依旧允许手动拖高度。如果设置了 autosize.maxRows 或者 autosize.minRows 则不允许手动调整高度 | N
+disabled | Boolean | false | 是否禁用文本框 | N
+label | TNode | - | 左侧文本 | N
+maxcharacter | Number | - | 用户最多可以输入的字符个数,一个中文汉字表示两个字符长度 | N
+maxlength | Number | - | 用户最多可以输入的字符个数 | N
+name | String | - | 名称 | N
+placeholder | String | '' | 占位符 | N
+readonly | Boolean | false | 只读状态 | N
+status | String | - | 文本框状态。可选项:`default/success/warning/error` | N
+tips | TNode | - | 输入框下方提示文本 | N
+value | String | - | 文本框值 | N
+defaultValue | String | - | 文本框值,非受控属性 | N
+onBlur | Function | | TS 类型:`(value: string, context: FocusEvent) => void`
失去焦点时触发 | N
+onFocus | Function | | TS 类型:`(value: string, context: FocusEvent) => void`
获得焦点时触发 | N
+onKeydown | Function | | TS 类型:`(value: string, context: KeyboardEvent) => void`
键盘按下时触发 | N
+onKeypress | Function | | TS 类型:`(value: string, context: KeyboardEvent) => void`
按下字符键时触发 | N
+onKeyup | Function | | TS 类型:`(value: string, context: KeyboardEvent) => void`
释放键盘时触发 | N
+onChange | Function | | TS 类型:`(value: string, context: Event) => void`
输入内容变化时触发 | N
diff --git a/src/textarea/_example/base.tsx b/src/textarea/_example/base.tsx
new file mode 100644
index 00000000..ff4ca5c2
--- /dev/null
+++ b/src/textarea/_example/base.tsx
@@ -0,0 +1,12 @@
+import 'tdesign-web-components/textarea';
+
+export default function Textarea() {
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/src/textarea/_example/event.tsx b/src/textarea/_example/event.tsx
new file mode 100644
index 00000000..4763bef5
--- /dev/null
+++ b/src/textarea/_example/event.tsx
@@ -0,0 +1,41 @@
+import 'tdesign-web-components/textarea';
+
+export default function Textarea() {
+ const value = '';
+
+ const onBlur = (value, { e }) => {
+ console.log('onBlur: ', value, e);
+ };
+
+ const onFocus = (value, { e }) => {
+ console.log('onFocus: ', value, e);
+ };
+
+ const onKeyup = (value, { e }) => {
+ console.log('onKeyup', value, e);
+ };
+
+ const onKeypress = (value, { e }) => {
+ console.log('onKeypress', value, e);
+ };
+
+ const onKeydown = (value, { e }) => {
+ console.log('onKeydown', value, e);
+ };
+ const onChange = (value, { e }) => {
+ console.log('onChange', value, e);
+ };
+
+ return (
+
+ );
+}
diff --git a/src/textarea/_example/limit.tsx b/src/textarea/_example/limit.tsx
new file mode 100644
index 00000000..22cdcfb3
--- /dev/null
+++ b/src/textarea/_example/limit.tsx
@@ -0,0 +1,14 @@
+import 'tdesign-web-components/textarea';
+import 'tdesign-web-components/space';
+
+export default function Textarea() {
+ return (
+
+
+
+
+ );
+}
diff --git a/src/textarea/_example/status.tsx b/src/textarea/_example/status.tsx
new file mode 100644
index 00000000..3f85ba9a
--- /dev/null
+++ b/src/textarea/_example/status.tsx
@@ -0,0 +1,15 @@
+import 'tdesign-web-components/textarea';
+import 'tdesign-web-components/space';
+
+export default function Textarea() {
+ return (
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/textarea/index.ts b/src/textarea/index.ts
new file mode 100644
index 00000000..087aa5db
--- /dev/null
+++ b/src/textarea/index.ts
@@ -0,0 +1,9 @@
+import './style/index.js';
+
+import _Textarea from './textarea';
+
+export type { TextareaProps } from './textarea';
+export const Textarea = _Textarea;
+export default Textarea;
+
+export * from './type';
diff --git a/src/textarea/style/index.js b/src/textarea/style/index.js
new file mode 100644
index 00000000..ea3d74ca
--- /dev/null
+++ b/src/textarea/style/index.js
@@ -0,0 +1,11 @@
+import { css, globalCSS } from 'omi';
+
+// 为了做主题切换
+import styles from '../../_common/style/web/components/textarea/_index.less';
+
+export const styleSheet = css`
+ ${styles}
+`;
+
+globalCSS(styleSheet);
+
diff --git a/src/textarea/textarea.tsx b/src/textarea/textarea.tsx
new file mode 100644
index 00000000..31c7234d
--- /dev/null
+++ b/src/textarea/textarea.tsx
@@ -0,0 +1,173 @@
+import { bind, classNames, Component, createRef, tag } from 'omi';
+
+import calcTextareaHeight from '../_common/js/utils/calcTextareaHeight';
+import { getCharacterLength, limitUnicodeMaxLength } from '../_common/js/utils/helper';
+import { getClassPrefix } from '../_util/classname';
+import { TdTextareaProps } from './type';
+
+export type TextareaProps = TdTextareaProps;
+@tag('t-textarea')
+export default class Textarea extends Component {
+ static css = [];
+
+ constructor() {
+ super();
+ this.props = {
+ allowInputOverMax: false,
+ autofocus: false,
+ autosize: false,
+ disabled: false,
+ readonly: false,
+ value: '',
+ ...this.props,
+ };
+ }
+
+ value = '';
+
+ isFocused = false;
+
+ eventPropsNames;
+
+ eventProps;
+
+ classPrefix = getClassPrefix();
+
+ installed() {
+ const { value, disabled, ...otherProps } = this.props;
+ this.value = value;
+ this.eventPropsNames = Object.keys(otherProps).filter((key) => /^on[A-Z]/.test(key));
+ this.eventProps = this.eventPropsNames.reduce((eventProps, key) => {
+ Object.assign(eventProps, {
+ [key]: (e) => {
+ if (disabled) return;
+ if (key === 'onFocus') {
+ this.isFocused = true;
+ this.update();
+ }
+ if (key === 'onBlur') {
+ this.isFocused = false;
+ this.update();
+ }
+ this.props[key](e.currentTarget.value, { e });
+ e.stopPropagation();
+ },
+ });
+ return eventProps;
+ }, {});
+
+ this.update();
+
+ const node = this.textArea.current;
+ this.value = node.value;
+ this.onInput();
+ }
+
+ countCharacters(text: string) {
+ // 按照一个中文汉字等于一个字符长度计算
+ const chineseCharacterRegex = /[\u4e00-\u9fa5]/g;
+ const chineseCharacters = text.match(chineseCharacterRegex) || [];
+ return text.length + chineseCharacters.length;
+ }
+
+ // textarea ref
+ textArea = createRef();
+
+ getTextareaStatus(status: string) {
+ return `${this.classPrefix}-is-${status || ''}`;
+ }
+
+ getTipsStyle(status: string) {
+ return `${this.classPrefix}-textarea__tips--${status}`;
+ }
+
+ getTextareaIsDisabled(disabled: boolean) {
+ return `${this.classPrefix}-is-${disabled ? 'disabled' : ''}`;
+ }
+
+ textareaClassPrefix = `${this.classPrefix}-textarea`;
+
+ cls() {
+ return classNames(`${this.textareaClassPrefix}__inner`, {
+ [`${this.classPrefix}-is-${this.props.status}`]: this.props.status,
+ [`${this.classPrefix}-is-disabled`]: this.props.disabled,
+ [`${this.classPrefix}-is-focused`]: this.isFocused,
+ [`${this.classPrefix}-resize-none`]: typeof this.props.autosize === 'object',
+ });
+ }
+
+ setHeight(heightObj) {
+ const node = this.textArea.current;
+ const clacMinHeight = heightObj.minHeight;
+ const clacHeight = heightObj.height;
+ node.style.minHeight = clacMinHeight;
+ node.style.height = clacHeight;
+ }
+
+ @bind
+ onInput() {
+ const node = this.textArea.current;
+ const { autosize, maxcharacter } = this.props;
+ if (autosize === true) {
+ const heightObj = calcTextareaHeight(node);
+ this.setHeight(heightObj);
+ } else if (typeof autosize === 'object') {
+ const heightObj = calcTextareaHeight(node, autosize?.minRows, autosize?.maxRows);
+ this.setHeight(heightObj);
+ }
+ if (maxcharacter) {
+ const text = node.value;
+ const length = this.countCharacters(text);
+ if (length > maxcharacter) {
+ if (text[text.length - 1].match('/[\u4e00-\u9fa5]/g')) {
+ node.value = text.slice(0, maxcharacter - 1);
+ } else {
+ node.value = text.slice(0, maxcharacter);
+ }
+ }
+ }
+ }
+
+ onChange(e) {
+ const { target } = e;
+ let val = (target as HTMLInputElement).value;
+ if (!this.props?.allowInputOverMax && !this.textArea.current) {
+ val = limitUnicodeMaxLength(val, this.props?.maxlength);
+ if (this.props?.maxcharacter && this.props?.maxcharacter >= 0) {
+ const stringInfo = getCharacterLength(val, this.props?.maxcharacter);
+ val = typeof stringInfo === 'object' && stringInfo.characters;
+ }
+ }
+ // setValue(val, { e });
+ this.value = val;
+
+ this.props?.onChange(val, { e });
+ this.update();
+ }
+
+ render(props: TextareaProps) {
+ const { autofocus, placeholder, readonly, status, disabled, tips, maxlength, maxcharacter } = props;
+
+ return (
+ <>
+
+ >
+ );
+ }
+}
diff --git a/src/textarea/type.ts b/src/textarea/type.ts
new file mode 100644
index 00000000..dfc37d52
--- /dev/null
+++ b/src/textarea/type.ts
@@ -0,0 +1,92 @@
+import { TNode } from '../common';
+
+export interface TdTextareaProps {
+ /**
+ * 超出maxlength或maxcharacter之后是否还允许输入
+ * @default false
+ */
+ allowInputOverMax?: boolean;
+ /**
+ * 自动聚焦,拉起键盘
+ * @default false
+ */
+ autofocus?: boolean;
+ /**
+ * 高度自动撑开。 autosize = true 表示组件高度自动撑开,同时,依旧允许手动拖高度。如果设置了 autosize.maxRows 或者 autosize.minRows 则不允许手动调整高度
+ * @default false
+ */
+ autosize?: boolean | { minRows?: number; maxRows?: number };
+ /**
+ * 是否禁用文本框
+ * @default false
+ */
+ disabled?: boolean;
+ /**
+ * 左侧文本
+ */
+ label?: TNode;
+ /**
+ * 用户最多可以输入的字符个数,一个中文汉字表示两个字符长度
+ */
+ maxcharacter?: number;
+ /**
+ * 用户最多可以输入的字符个数
+ */
+ maxlength?: number;
+ /**
+ * 名称,HTML 元素原生属性
+ * @default ''
+ */
+ name?: string;
+ /**
+ * 占位符
+ */
+ placeholder?: string;
+ /**
+ * 只读状态
+ * @default false
+ */
+ readonly?: boolean;
+ /**
+ * 文本框状态
+ */
+ status?: 'default' | 'success' | 'warning' | 'error';
+ /**
+ * 输入框下方提示文本,会根据不同的 `status` 呈现不同的样式
+ */
+ tips?: TNode;
+ /**
+ * 文本框值
+ */
+ value?: TextareaValue;
+ /**
+ * 文本框值,非受控属性
+ */
+ defaultValue?: TextareaValue;
+ /**
+ * 失去焦点时触发
+ */
+ onBlur?: (value: TextareaValue, context: { e: FocusEvent }) => void;
+ /**
+ * 输入内容变化时触发:
+ */
+ onChange?: (value: TextareaValue, context?: { e?: Event }) => void;
+ /**
+ * 获得焦点时触发
+ */
+ onFocus?: (value: TextareaValue, context: { e: FocusEvent }) => void;
+ /**
+ * 键盘按下时触发
+ */
+ onKeydown?: (value: TextareaValue, context: { e: KeyboardEvent }) => void;
+ /**
+ * 按下字符键时触发(keydown -> keypress -> keyup)
+ */
+ onKeypress?: (value: TextareaValue, context: { e: KeyboardEvent }) => void;
+ /**
+ * 释放键盘时触发
+ */
+ onKeyup?: (value: TextareaValue, context: { e: KeyboardEvent }) => void;
+}
+
+export type TextareaValue = string;