From 3236c2f0fe6405136bed9bf9998f0a6e0939eb6b Mon Sep 17 00:00:00 2001 From: anlyyao Date: Fri, 20 Sep 2024 16:45:35 +0800 Subject: [PATCH] chore(Textarea): complete refactoring --- site/mobile/mobile.config.js | 2 +- src/_util/helper.ts | 15 +++ src/textarea/Textarea.tsx | 117 ++++++++++++++---- .../_example/{autosize.jsx => autosize.tsx} | 2 +- .../_example/{events.jsx => base.tsx} | 11 +- src/textarea/_example/card.tsx | 24 ++++ src/textarea/_example/custom.tsx | 13 ++ src/textarea/_example/disable.tsx | 8 ++ .../_example/{index.jsx => index.tsx} | 22 ++-- src/textarea/_example/label.jsx | 6 - src/textarea/_example/{base.jsx => label.tsx} | 2 +- src/textarea/_example/maxcharacter.jsx | 6 - src/textarea/_example/maxcharacter.tsx | 20 +++ src/textarea/_example/maxlength.jsx | 6 - src/textarea/_example/maxlength.tsx | 21 ++++ src/textarea/_example/status.jsx | 6 - src/textarea/_example/style/index.less | 47 +++++++ src/textarea/defaultProps.ts | 17 +++ src/textarea/style/index.js | 2 +- src/textarea/textarea.en-US.md | 28 +++++ src/textarea/textarea.md | 17 ++- src/textarea/type.ts | 30 ++++- 22 files changed, 347 insertions(+), 75 deletions(-) rename src/textarea/_example/{autosize.jsx => autosize.tsx} (65%) rename src/textarea/_example/{events.jsx => base.tsx} (66%) create mode 100644 src/textarea/_example/card.tsx create mode 100644 src/textarea/_example/custom.tsx create mode 100644 src/textarea/_example/disable.tsx rename src/textarea/_example/{index.jsx => index.tsx} (68%) delete mode 100644 src/textarea/_example/label.jsx rename src/textarea/_example/{base.jsx => label.tsx} (52%) delete mode 100644 src/textarea/_example/maxcharacter.jsx create mode 100644 src/textarea/_example/maxcharacter.tsx delete mode 100644 src/textarea/_example/maxlength.jsx create mode 100644 src/textarea/_example/maxlength.tsx delete mode 100644 src/textarea/_example/status.jsx create mode 100644 src/textarea/_example/style/index.less create mode 100644 src/textarea/defaultProps.ts create mode 100644 src/textarea/textarea.en-US.md diff --git a/site/mobile/mobile.config.js b/site/mobile/mobile.config.js index d8fcfdec..a3a2d8a9 100644 --- a/site/mobile/mobile.config.js +++ b/site/mobile/mobile.config.js @@ -230,7 +230,7 @@ export default { { title: 'Textarea 多行文本框', name: 'textarea', - component: () => import('tdesign-mobile-react/textarea/_example/index.jsx'), + component: () => import('tdesign-mobile-react/textarea/_example/index.tsx'), }, { title: 'Steps 步骤条', diff --git a/src/_util/helper.ts b/src/_util/helper.ts index 5ef0f6cc..eb76921c 100644 --- a/src/_util/helper.ts +++ b/src/_util/helper.ts @@ -48,3 +48,18 @@ export function getCharacterLength(str: string, maxCharacter?: number) { export function pxCompat(param: string | number) { return typeof param === 'number' ? `${param}px` : param; } + +/** + * 修正 Unicode 最大字符长度 + * '👨👨👨'.slice(0, 2) === '👨' + * limitUnicodeMaxLength('👨👨👨', 2) === '👨👨' + * @param str + * @param maxLength + * @param oldStr + * @returns {string} + */ +export function limitUnicodeMaxLength(str?: string, maxLength?: number, oldStr?: string): string { + // 旧字符满足字数要求则返回 + if ([...(oldStr ?? '')].slice().length === maxLength) return oldStr || ''; + return [...(str ?? '')].slice(0, maxLength).join(''); +} diff --git a/src/textarea/Textarea.tsx b/src/textarea/Textarea.tsx index be8d41bb..b0b47190 100644 --- a/src/textarea/Textarea.tsx +++ b/src/textarea/Textarea.tsx @@ -1,25 +1,53 @@ import React, { forwardRef, useState, useEffect, useMemo, useRef, useCallback, useImperativeHandle } from 'react'; import classNames from 'classnames'; -import useConfig from '../_util/useConfig'; +import parseTNode from '../_util/parseTNode'; import useDefault from '../_util/useDefault'; -import { getCharacterLength } from '../_util/helper'; +import { getCharacterLength, limitUnicodeMaxLength } from '../_util/helper'; import calcTextareaHeight from '../_common/js/utils/calcTextareaHeight'; +import { textareaDefaultProps } from './defaultProps'; import { TdTextareaProps } from './type'; import { StyledProps } from '../common'; +import { usePrefixClass } from '../hooks/useClass'; +import useDefaultProps from '../hooks/useDefaultProps'; + +export interface TextareaProps + extends Omit< + React.TextareaHTMLAttributes, + 'value' | 'defaultValue' | 'onBlur' | 'onChange' | 'onFocus' + >, + TdTextareaProps, + StyledProps {} -export interface TextareaProps extends TdTextareaProps, StyledProps {} export interface TextareaRefInterface extends React.RefObject { currentElement: HTMLDivElement; textareaElement: HTMLTextAreaElement; } -const Textarea = forwardRef((props: TextareaProps, ref: TextareaRefInterface) => { - const { disabled, maxlength, maxcharacter, autofocus, defaultValue, autosize = false, label, ...otherProps } = props; - const { classPrefix } = useConfig(); - const baseClass = `${classPrefix}-textarea`; +const Textarea = forwardRef((originProps: TextareaProps, ref: TextareaRefInterface) => { + const props = useDefaultProps(originProps, textareaDefaultProps); + const { + className, + style, + allowInputOverMax, + autofocus, + bordered, + disabled, + defaultValue, + maxlength, + maxcharacter, + layout, + autosize, + label, + indicator, + readonly, + ...otherProps + } = props; + + const textareaClass = usePrefixClass('textarea'); const [value = '', setValue] = useDefault(props.value, defaultValue, props.onChange); const [textareaStyle, setTextareaStyle] = useState({}); + const composingRef = useRef(false); const textareaRef: React.RefObject = useRef(); const wrapperRef: React.RefObject = useRef(); @@ -50,30 +78,56 @@ const Textarea = forwardRef((props: TextareaProps, ref: TextareaRefInterface) => return eventProps; }, {}); - const textareaClassNames = classNames(`${baseClass}__wrapper`, { - [`${baseClass}-is-disabled`]: disabled, + const textareaClasses = classNames( + `${textareaClass}`, + { + [`${textareaClass}--layout-${layout}`]: layout, + [`${textareaClass}--border`]: bordered, + }, + className, + ); + + const textareaInnerClasses = classNames(`${textareaClass}__wrapper-inner`, { + [`${textareaClass}--disabled`]: disabled, + [`${textareaClass}--readonly`]: readonly, }); const adjustTextareaHeight = useCallback(() => { if (autosize === true) { setTextareaStyle(calcTextareaHeight(textareaRef.current as HTMLTextAreaElement)); + } else if (autosize === false) { + props.rows + ? setTextareaStyle({ height: 'auto', minHeight: 'auto' }) + : setTextareaStyle(calcTextareaHeight(textareaRef.current as HTMLTextAreaElement, 1, 1)); } else if (typeof autosize === 'object') { const { minRows, maxRows } = autosize; setTextareaStyle(calcTextareaHeight(textareaRef.current as HTMLTextAreaElement, minRows, maxRows)); - } else if (autosize === false) { - setTextareaStyle({ height: 'auto', minHeight: '96px' }); } - }, [autosize]); + }, [autosize, props.rows]); - function inputValueChangeHandle(e: React.FormEvent) { + const inputValueChangeHandle = (e: React.FormEvent) => { const { target } = e; let val = (target as HTMLInputElement).value; - if (maxcharacter && maxcharacter >= 0) { - const stringInfo = getCharacterLength(val, maxcharacter); - val = typeof stringInfo === 'object' && stringInfo.characters; + if (!allowInputOverMax && !composingRef.current) { + val = limitUnicodeMaxLength(val, maxlength); + if (maxcharacter && maxcharacter >= 0) { + const stringInfo = getCharacterLength(val, maxcharacter); + val = typeof stringInfo === 'object' && stringInfo.characters; + } } setValue(val, { e }); - } + }; + + const handleCompositionStart = () => { + composingRef.current = true; + }; + + const handleCompositionEnd = (e) => { + if (composingRef.current) { + composingRef.current = false; + inputValueChangeHandle(e); + } + }; useEffect(() => { adjustTextareaHeight(); @@ -84,27 +138,40 @@ const Textarea = forwardRef((props: TextareaProps, ref: TextareaRefInterface) => textareaElement: textareaRef.current, })); + const renderLabel = () => label &&
{parseTNode(label)}
; + + const renderIndicator = () => { + const isShowIndicator = indicator && (maxcharacter || maxlength); + if (!isShowIndicator) { + return null; + } + return
{`${textareaLength}/${maxcharacter || maxlength}`}
; + }; + return ( -
- {label &&
{label}
} -
+
+ {renderLabel()} +