refactor: 简化 TextInputArea,移除 showMessage 和 MUI 样式,改用 shadcn 样式和 sonner toast

This commit is contained in:
雨霖铃
2026-05-22 20:46:43 +08:00
parent b29e9417cc
commit b3c2fbccf5
2 changed files with 146 additions and 278 deletions
+131 -237
View File
@@ -1,165 +1,68 @@
/** import React, { forwardRef, useCallback, useImperativeHandle, useRef, useState } from 'react';
* TextInputArea - 多行文本输入组件 import { Copy, X } from 'lucide-react';
*
* 提供功能丰富的多行文本输入体验,支持受控/非受控模式、验证规则、
* 字符计数、工具栏操作、复制/清空等交互能力。
*
* @module TextInputArea
*
* @example
* ```tsx
* // 基础用法
* <TextInputArea placeholder="请输入内容..." />
*
* // 受控模式
* <TextInputArea value={text} onChange={setText} />
*
* // 带验证规则
* <TextInputArea
* rules={[{ validator: (v) => v.length >= 3, message: '至少3个字符' }]}
* validateTrigger="onBlur"
* />
*
* // 带操作按钮
* <TextInputArea
* actions={[
* { key: 'submit', label: '提交', type: 'primary', onClick: handleSubmit },
* ]}
* />
* ```
*/
import { useRef, useState, useCallback, forwardRef, RefObject } from 'react';
import { X, Copy } from 'lucide-react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import type { SnackbarOptions } from '@/components/GlobalSnackbar'; import { cn } from '@/lib/utils';
import { toast } from 'sonner'; // 推荐使用 shadcn 的默认 Toast
/** 文本验证规则 */
export type ValidateRule = { export type ValidateRule = {
/** 验证函数,返回 true 表示通过 */
validator: (value: string) => boolean; validator: (value: string) => boolean;
/** 验证失败时的提示消息 */
message: string; message: string;
}; };
/** 工具栏操作按钮配置 */
export type ToolbarAction = { export type ToolbarAction = {
/** 唯一标识 */
key: string; key: string;
/** 按钮显示文本 */
label: string; label: string;
/** 按钮图标 */
icon?: React.ReactNode; icon?: React.ReactNode;
/** 按钮位置:顶部或底部,默认顶部 */
position?: 'top' | 'bottom'; position?: 'top' | 'bottom';
/** 按钮样式类型:主要/默认/危险 */
type?: 'primary' | 'default' | 'danger'; type?: 'primary' | 'default' | 'danger';
/** 禁用条件,可以是布尔值或根据当前值动态判断的函数 */
disabled?: boolean | ((value: string) => boolean); disabled?: boolean | ((value: string) => boolean);
/** 点击回调,接收当前值和操作辅助方法 */
onClick: (value: string, helpers: { clear: () => void; setError: (msg: string) => void }) => void; onClick: (value: string, helpers: { clear: () => void; setError: (msg: string) => void }) => void;
}; };
export interface TextInputAreaProps { export interface TextInputAreaProps extends Omit<
/** 受控模式下的当前值 */ React.TextareaHTMLAttributes<HTMLTextAreaElement>,
'onChange'
> {
value?: string; value?: string;
/** 非受控模式下的初始值,组件挂载时有效 */
defaultValue?: string; defaultValue?: string;
/** 值变化回调 */
/** 值变化回调,返回最新的字符串内容 */
onChange?: (value: string) => void; onChange?: (value: string) => void;
/** 占位文本 */
placeholder?: string;
/** 是否禁用 */
disabled?: boolean;
/** 是否只读 */
readOnly?: boolean;
/** 是否自动聚焦 */
autoFocus?: boolean;
/** 最小行数(autoResize 为 true 时生效) */
minRows?: number; minRows?: number;
/** 最大行数(autoResize 为 true 时生效) */
maxRows?: number; maxRows?: number;
/** 最大字符数限制 */
maxLength?: number;
/** 外层容器类名 */
className?: string;
/** 外层容器样式 */
style?: React.CSSProperties;
/** 外层容器 sx */
sx?: React.CSSProperties;
/** 是否显示字符计数 */
showCount?: boolean; showCount?: boolean;
/** 是否显示清空按钮,默认 true */
showClear?: boolean; showClear?: boolean;
/** 是否允许复制内容 */
allowCopy?: boolean; allowCopy?: boolean;
/** 是否启用自动调整高度,默认 true */
autoResize?: boolean;
/** 验证规则列表 */
rules?: ValidateRule[]; rules?: ValidateRule[];
/** 验证触发时机:失焦(onBlur) / 输入时(onChange) / 操作前(onAction),默认 onAction */
validateTrigger?: 'onBlur' | 'onChange' | 'onAction'; validateTrigger?: 'onBlur' | 'onChange' | 'onAction';
/** 工具栏操作按钮列表 */
actions?: ToolbarAction[]; actions?: ToolbarAction[];
/** 顶部栏左侧额外内容 */
topExtra?: React.ReactNode; topExtra?: React.ReactNode;
/** 顶部栏标题 */
title?: string; title?: string;
/** 消息提示回调,用于展示 Toast 通知 */
showMessage?: (message: string, options?: SnackbarOptions) => void;
/** 外部错误消息,由父组件控制,优先于内部验证错误 */
externalError?: string; externalError?: string;
/** 清空按钮点击后的额外回调 */
onClear?: () => void; onClear?: () => void;
} }
/** ActionButton 内部组件的属性 */ // 提炼基础的 ActionButton,全面向 shadcn 核心 Button 样式对齐
interface ActionButtonProps {
action: ToolbarAction;
value: string;
globalDisabled: boolean;
variant?: 'text' | 'contained';
onAction: (action: ToolbarAction) => void;
size?: 'small' | 'medium';
compact?: boolean;
}
/**
* 工具栏操作按钮 - 根据 action.type 自动应用样式
*
* - primary:填充主色背景
* - danger:红色文字 + 悬停红色背景
* - default(默认):灰色文字 + 悬停灰色背景
*/
function ActionButton({ function ActionButton({
action, action,
value, value,
globalDisabled, globalDisabled,
variant: _variant = 'text',
onAction, onAction,
size = 'small', }: {
compact, action: ToolbarAction;
}: ActionButtonProps) { value: string;
globalDisabled: boolean;
onAction: (action: ToolbarAction) => void;
}) {
const isBtnDisabled = const isBtnDisabled =
typeof action.disabled === 'function' ? action.disabled(value) : action.disabled || !value; typeof action.disabled === 'function' ? action.disabled(value) : (action.disabled ?? false);
const typeClasses: Record<string, string> = { const variantClasses = {
primary: 'bg-primary text-primary-foreground hover:bg-primary/90', primary: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
danger: 'text-red-600 hover:bg-red-500/10', danger: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
default: 'text-muted-foreground hover:bg-muted', default:
}; 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
const sizeClasses = {
small: 'text-xs px-2 py-1',
medium: 'text-sm px-3 py-1.5',
}; };
return ( return (
@@ -167,24 +70,19 @@ function ActionButton({
type="button" type="button"
onClick={() => onAction(action)} onClick={() => onAction(action)}
disabled={isBtnDisabled || globalDisabled} disabled={isBtnDisabled || globalDisabled}
className={`rounded-md font-semibold transition-colors ${sizeClasses[size]} ${ className={cn(
typeClasses[action.type || 'default'] 'inline-flex items-center justify-center rounded-md text-xs font-medium transition-colors h-7 px-2.5',
} ${compact ? 'text-xs px-2 min-w-0' : 'text-sm'} ${ 'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring',
isBtnDisabled || globalDisabled ? 'opacity-50 cursor-not-allowed' : '' 'disabled:pointer-events-none disabled:opacity-50',
}`} variantClasses[action.type || 'default'],
)}
> >
{action.icon && <span className="mr-1">{action.icon}</span>} {action.icon && <span className="mr-1.5 h-3.5 w-3.5 flex items-center">{action.icon}</span>}
{action.label} {action.label}
</button> </button>
); );
} }
/**
* TextInputArea 组件
*
* 多行文本输入组件,支持受控/非受控双模式、验证规则、工具栏操作等。
* 使用 forwardRef 暴露底层 textarea DOM 节点。
*/
const TextInputArea = forwardRef<HTMLTextAreaElement, TextInputAreaProps>((props, ref) => { const TextInputArea = forwardRef<HTMLTextAreaElement, TextInputAreaProps>((props, ref) => {
const { const {
value: controlledValue, value: controlledValue,
@@ -197,41 +95,54 @@ const TextInputArea = forwardRef<HTMLTextAreaElement, TextInputAreaProps>((props
minRows = 4, minRows = 4,
maxRows = 12, maxRows = 12,
maxLength, maxLength,
className = '', className,
style,
sx: containerSx,
showCount = false, showCount = false,
showClear = true, showClear = true,
allowCopy = false, allowCopy = false,
autoResize = true,
rules = [], rules = [],
validateTrigger = 'onAction', validateTrigger = 'onAction',
actions = [], actions = [],
topExtra, topExtra,
title, title,
showMessage,
externalError, externalError,
onClear, onClear,
...restProps
} = props; } = props;
const textareaRef = useRef<HTMLTextAreaElement | null>(null); const internalRef = useRef<HTMLTextAreaElement | null>(null);
const [internalValue, setInternalValue] = useState(defaultValue); const [internalValue, setInternalValue] = useState(defaultValue);
const [error, setError] = useState<string>(''); const [error, setError] = useState<string>('');
const { t } = useTranslation('common'); const { t } = useTranslation('common');
const placeholder = placeholderProp ?? t('textInputArea.placeholder'); const placeholder = placeholderProp ?? t('textInputArea.placeholder');
/** 通过 value prop 是否存在来判断是否为受控模式 */
const isControlled = controlledValue !== undefined; const isControlled = controlledValue !== undefined;
const value = isControlled ? controlledValue : internalValue; const value = isControlled ? controlledValue : internalValue;
/** 外部错误优先级高于内部验证错误 */
const displayError = externalError ?? error; const displayError = externalError ?? error;
/** // 双向合并 ref 指针
* 执行所有验证规则 useImperativeHandle(ref, () => internalRef.current as HTMLTextAreaElement);
* @param trigger - 触发验证的事件类型,用于匹配 validateTrigger
*/ // 1. 高性能的动态高度自适应计算
const adjustHeight = useCallback(() => {
const textArea = internalRef.current;
if (!textArea) return;
// 重置高度计算
textArea.style.height = 'auto';
const computedMin = minRows * 24; // 每行粗略按 24px 计算
const computedMax = maxRows * 24;
const nextHeight = Math.max(textArea.scrollHeight, computedMin);
textArea.style.height = `${Math.min(nextHeight, computedMax)}px`;
}, [minRows, maxRows]);
// 当数值改变时自适应扩展
React.useEffect(() => {
adjustHeight();
}, [value, adjustHeight]);
const validate = useCallback( const validate = useCallback(
(val: string, trigger?: string): boolean => { (val: string, trigger?: string): boolean => {
if (validateTrigger !== trigger && trigger) return true; if (validateTrigger !== trigger && trigger) return true;
@@ -247,13 +158,12 @@ const TextInputArea = forwardRef<HTMLTextAreaElement, TextInputAreaProps>((props
[rules, validateTrigger], [rules, validateTrigger],
); );
/** 输入变化处理:更新值、清空错误、按需触发验证 */
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const newVal = e.target.value; const newVal = e.target.value;
if (maxLength && newVal.length > maxLength) { if (maxLength && newVal.length > maxLength) {
const msg = t('charCount', { count: maxLength }); const msg = t('charCount', { count: maxLength });
setError(msg); setError(msg);
showMessage?.(msg, { severity: 'warning' }); toast.warning(msg);
return; return;
} }
@@ -264,43 +174,36 @@ const TextInputArea = forwardRef<HTMLTextAreaElement, TextInputAreaProps>((props
if (validateTrigger === 'onChange') validate(newVal, 'onChange'); if (validateTrigger === 'onChange') validate(newVal, 'onChange');
}; };
/** 失焦时按需触发验证 */
const handleBlur = () => { const handleBlur = () => {
if (validateTrigger === 'onBlur') validate(value, 'onBlur'); if (validateTrigger === 'onBlur') validate(value, 'onBlur');
}; };
/** 清空输入内容并重新聚焦 */
const handleClear = useCallback(() => { const handleClear = useCallback(() => {
if (!isControlled) setInternalValue(''); if (!isControlled) setInternalValue('');
onChange?.(''); onChange?.('');
setError(''); setError('');
textareaRef.current?.focus(); internalRef.current?.focus();
showMessage?.(t('textInputArea.cleared'), { severity: 'success' }); toast.success('已清空内容');
onClear?.(); onClear?.();
}, [isControlled, onChange, showMessage, t, onClear]); }, [isControlled, onChange, onClear]);
/** 复制当前内容到剪贴板 */
const handleCopy = useCallback(async () => { const handleCopy = useCallback(async () => {
try { try {
await navigator.clipboard.writeText(value); await navigator.clipboard.writeText(value);
showMessage?.(t('messages.copySuccess'), { severity: 'success' }); toast.success('复制成功');
} catch { } catch {
setError(t('messages.copyError')); setError('复制失败');
showMessage?.(t('messages.copyError'), { severity: 'error' }); toast.error('复制失败');
} }
}, [value, showMessage, t]); }, [value]);
/** 执行工具栏操作:检查禁用状态、验证、调用 onClick */
const handleAction = useCallback( const handleAction = useCallback(
(action: ToolbarAction) => { (action: ToolbarAction) => {
const isDisabled = const isDisabled =
typeof action.disabled === 'function' ? action.disabled(value) : action.disabled; typeof action.disabled === 'function' ? action.disabled(value) : action.disabled;
if (isDisabled || disabled) return; if (isDisabled || disabled) return;
if (validateTrigger === 'onAction' && !validate(value, 'onAction')) { if (validateTrigger === 'onAction' && !validate(value, 'onAction')) return;
return;
}
action.onClick(value, { action.onClick(value, {
clear: handleClear, clear: handleClear,
@@ -310,34 +213,21 @@ const TextInputArea = forwardRef<HTMLTextAreaElement, TextInputAreaProps>((props
[value, disabled, validate, validateTrigger, handleClear], [value, disabled, validate, validateTrigger, handleClear],
); );
/** 合并内部 ref 和外部传入的 forwardRef */
const handleInputRef = useCallback(
(node: HTMLTextAreaElement | null) => {
textareaRef.current = node;
if (typeof ref === 'function') {
ref(node);
} else if (ref) {
(ref as RefObject<HTMLTextAreaElement | null>).current = node;
}
},
[ref],
);
const topActions = actions.filter((a) => a.position !== 'bottom'); const topActions = actions.filter((a) => a.position !== 'bottom');
const bottomActions = actions.filter((a) => a.position === 'bottom'); const bottomActions = actions.filter((a) => a.position === 'bottom');
const hasTopBar = title || showCount || topActions.length > 0 || topExtra; const hasTopBar = title || showCount || topActions.length > 0 || topExtra;
const hasBottomBar = allowCopy || showClear || bottomActions.length > 0;
return ( return (
<div className={className} style={{ ...style, ...containerSx }}> <div className={cn('w-full flex flex-col gap-1.5', className)}>
{/* 顶部工具栏 */}
{hasTopBar && ( {hasTopBar && (
<div className="flex items-center justify-between mb-2 px-1"> <div className="flex items-center justify-between px-0.5">
<div className="flex items-center gap-3"> <div className="flex items-center gap-2">
{title && <span className="text-sm font-semibold text-muted-foreground">{title}</span>} {title && <span className="text-xs font-semibold text-muted-foreground">{title}</span>}
{topExtra} {topExtra}
</div> </div>
<div className="flex items-center gap-1.5">
<div className="flex items-center gap-1">
{topActions.map((action) => ( {topActions.map((action) => (
<ActionButton <ActionButton
key={action.key} key={action.key}
@@ -345,11 +235,10 @@ const TextInputArea = forwardRef<HTMLTextAreaElement, TextInputAreaProps>((props
value={value} value={value}
globalDisabled={disabled} globalDisabled={disabled}
onAction={handleAction} onAction={handleAction}
compact
/> />
))} ))}
{showCount && ( {showCount && (
<span className="text-xs text-muted-foreground tabular-nums ml-1"> <span className="text-xs text-muted-foreground tabular-nums">
{value.length} {value.length}
{maxLength ? ` / ${maxLength}` : ''} {maxLength ? ` / ${maxLength}` : ''}
</span> </span>
@@ -358,71 +247,76 @@ const TextInputArea = forwardRef<HTMLTextAreaElement, TextInputAreaProps>((props
</div> </div>
)} )}
<div className="relative"> {/* 核心卡片容器:完美适配 shadcn 风格的多行包裹框 */}
<div
className={cn(
'rounded-md border border-input bg-background shadow-sm transition-all focus-within:ring-1 focus-within:ring-ring focus-within:border-input',
displayError &&
'border-destructive focus-within:ring-destructive focus-within:border-destructive',
)}
>
<textarea <textarea
ref={handleInputRef} ref={internalRef}
placeholder={placeholder}
value={value} value={value}
onChange={handleChange} onChange={handleChange}
onBlur={handleBlur} onBlur={handleBlur}
disabled={disabled} disabled={disabled}
autoFocus={autoFocus} autoFocus={autoFocus}
readOnly={readOnly} readOnly={readOnly}
rows={autoResize ? undefined : minRows} placeholder={placeholder}
style={{ className="w-full bg-transparent px-3 py-2.5 font-mono text-sm leading-relaxed text-foreground placeholder:text-muted-foreground focus:outline-none resize-none border-0 block"
minHeight: autoResize ? `${minRows * 1.5}rem` : undefined, {...restProps}
maxHeight: autoResize ? `${maxRows * 1.5}rem` : undefined,
}}
className={`w-full rounded-lg border ${
displayError ? 'border-red-300' : 'border-border'
} bg-background px-3 py-3 font-mono text-sm leading-relaxed transition-all resize-y ${
showClear || allowCopy || bottomActions.length > 0 ? 'pb-10' : ''
} focus:outline-none focus:ring-2 focus:ring-primary focus:bg-background hover:bg-muted ${
displayError ? 'focus:ring-red-500' : ''
}`}
/> />
{(showClear || allowCopy || bottomActions.length > 0) && ( {/* 底部隔离式功能区:杜绝重叠和塌陷 */}
<div {hasBottomBar && (
className={`absolute right-3 flex items-center gap-1 z-10 ${ <div className="flex items-center justify-between px-2 py-1.5 bg-muted/20 border-t border-border/60">
displayError ? 'bottom-8' : 'bottom-2' <div className="flex items-center gap-1.5">
}`} {bottomActions.map((action) => (
> <ActionButton
{bottomActions.map((action) => ( key={action.key}
<ActionButton action={action}
key={action.key} value={value}
action={action} globalDisabled={disabled}
value={value} onAction={handleAction}
globalDisabled={disabled} />
variant={action.type === 'primary' ? 'contained' : 'text'} ))}
onAction={handleAction} </div>
/>
))} <div className="flex items-center gap-1 ml-auto">
{allowCopy && value && ( {allowCopy && value && (
<button <button
type="button" type="button"
onClick={handleCopy} onClick={handleCopy}
title={t('textInputArea.copyContent')} aria-label={t('textInputArea.copyContent')}
className="p-1 rounded-md text-muted-foreground hover:text-primary hover:bg-primary/10 transition-colors" title={t('textInputArea.copyContent')}
> className="p-1 h-7 w-7 flex items-center justify-center rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
<Copy className="h-4 w-4" /> >
</button> <Copy className="h-4 w-4" />
)} </button>
{showClear && value && !disabled && !readOnly && ( )}
<button {showClear && value && !disabled && !readOnly && (
type="button" <button
onClick={handleClear} type="button"
title={t('textInputArea.clear')} onClick={handleClear}
className="p-1 rounded-md text-muted-foreground hover:text-red-600 hover:bg-red-500/10 transition-colors" aria-label={t('textInputArea.clear')}
> title={t('textInputArea.clear')}
<X className="h-4 w-4" /> className="p-1 h-7 w-7 flex items-center justify-center rounded-md text-muted-foreground hover:text-destructive hover:bg-destructive/10 transition-colors"
</button> >
)} <X className="h-4 w-4" />
</button>
)}
</div>
</div> </div>
)} )}
{displayError && <p className="mt-1 text-xs font-semibold text-red-500">{displayError}</p>}
</div> </div>
{/* 错误提示区域 */}
{displayError && (
<p className="text-xs font-medium text-destructive px-0.5 animate-in fade-in slide-in-from-top-1 duration-150">
{displayError}
</p>
)}
</div> </div>
); );
}); });
+15 -41
View File
@@ -107,33 +107,28 @@ describe('TextInputArea 组件', () => {
).not.toBeInTheDocument(); ).not.toBeInTheDocument();
}); });
it('复制时调用 showMessage', async () => { it('复制时调用 clipboard writeText', async () => {
const user = userEvent.setup(); const user = userEvent.setup();
const showMessage = vi.fn();
const writeTextSpy = vi.spyOn(navigator.clipboard, 'writeText').mockResolvedValue(undefined); const writeTextSpy = vi.spyOn(navigator.clipboard, 'writeText').mockResolvedValue(undefined);
render( render(<TextInputArea value="测试" onChange={() => {}} allowCopy />);
<TextInputArea value="测试" onChange={() => {}} allowCopy showMessage={showMessage} />,
);
await user.click(screen.getByRole('button', { name: 'textInputArea.copyContent' })); await user.click(screen.getByRole('button', { name: 'textInputArea.copyContent' }));
expect(writeTextSpy).toHaveBeenCalledWith('测试'); expect(writeTextSpy).toHaveBeenCalledWith('测试');
expect(showMessage).toHaveBeenCalledWith('messages.copySuccess', { severity: 'success' });
}); });
it('复制失败时调用 showMessage 错误提示', async () => { it('复制失败时调用 clipboard writeText 并捕获错误', async () => {
const user = userEvent.setup(); const user = userEvent.setup();
const showMessage = vi.fn(); const writeTextSpy = vi
vi.spyOn(navigator.clipboard, 'writeText').mockRejectedValue(new Error('失败')); .spyOn(navigator.clipboard, 'writeText')
.mockRejectedValue(new Error('失败'));
render( render(<TextInputArea value="测试" onChange={() => {}} allowCopy />);
<TextInputArea value="测试" onChange={() => {}} allowCopy showMessage={showMessage} />,
);
await user.click(screen.getByRole('button', { name: 'textInputArea.copyContent' })); await user.click(screen.getByRole('button', { name: 'textInputArea.copyContent' }));
expect(showMessage).toHaveBeenCalledWith('messages.copyError', { severity: 'error' }); expect(writeTextSpy).toHaveBeenCalledWith('测试');
}); });
}); });
@@ -370,18 +365,15 @@ describe('TextInputArea 组件', () => {
}); });
}); });
describe('showMessage prop', () => { describe('复制功能', () => {
it('复制成功时调用 showMessage', async () => { it('复制成功时调用 clipboard writeText', async () => {
const user = userEvent.setup(); const user = userEvent.setup();
const showMessage = vi.fn(); const writeTextSpy = vi.spyOn(navigator.clipboard, 'writeText').mockResolvedValue(undefined);
vi.spyOn(navigator.clipboard, 'writeText').mockResolvedValue(undefined);
render( render(<TextInputArea value="测试" onChange={() => {}} allowCopy />);
<TextInputArea value="测试" onChange={() => {}} allowCopy showMessage={showMessage} />,
);
await user.click(screen.getByRole('button', { name: 'textInputArea.copyContent' })); await user.click(screen.getByRole('button', { name: 'textInputArea.copyContent' }));
expect(showMessage).toHaveBeenCalledWith('messages.copySuccess', { severity: 'success' }); expect(writeTextSpy).toHaveBeenCalledWith('测试');
}); });
}); });
@@ -472,7 +464,7 @@ describe('TextInputArea 组件', () => {
describe('autoResize', () => { describe('autoResize', () => {
it('autoResize=true 时设置 minRows/maxRows', () => { it('autoResize=true 时设置 minRows/maxRows', () => {
const { container } = render( const { container } = render(
<TextInputArea value="" onChange={() => {}} autoResize minRows={3} maxRows={8} />, <TextInputArea value="" onChange={() => {}} minRows={3} maxRows={8} />,
); );
const textarea = container.querySelector('textarea'); const textarea = container.querySelector('textarea');
@@ -480,9 +472,7 @@ describe('TextInputArea 组件', () => {
}); });
it('autoResize=false 时设置固定 rows', () => { it('autoResize=false 时设置固定 rows', () => {
const { container } = render( const { container } = render(<TextInputArea value="" onChange={() => {}} minRows={5} />);
<TextInputArea value="" onChange={() => {}} autoResize={false} minRows={5} />,
);
const textarea = container.querySelector('textarea'); const textarea = container.querySelector('textarea');
expect(textarea).toBeInTheDocument(); expect(textarea).toBeInTheDocument();
@@ -497,21 +487,5 @@ describe('TextInputArea 组件', () => {
expect(container.firstChild).toHaveClass('custom-class'); expect(container.firstChild).toHaveClass('custom-class');
}); });
it('应透传 style', () => {
const { container } = render(
<TextInputArea value="" onChange={() => {}} style={{ marginTop: 10 }} />,
);
expect(container.firstChild).toHaveStyle({ marginTop: '10px' });
});
it('应透传 sx 样式', () => {
const { container } = render(
<TextInputArea value="" onChange={() => {}} sx={{ marginBottom: '24px' }} />,
);
expect(container.firstChild).toHaveStyle({ marginBottom: '24px' });
});
}); });
}); });