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 ( + <> +
+ + {tips &&
{tips}
} +
+ + ); + } +} 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;