标题中的前缀指的是 输入框之前的固定内容.
它与文本占据同样的位置 但是可以设置不同的样式
考虑使用wrapper包裹textarea 用绝对定位设置前缀 使用textarea和前缀的宽高动态设置样式 使用useLayoutEffect避免闪烁\
由于js精度的问题 即使dom尺寸未发生变化 两次取得的数据也可能不一致 因此需要写函数比较尺寸而不能直接用等号判断
type Size = {
width: number
height: number
}
const isSizeStable = (prev: Size, current: Size) => {
const isNumberStable = (prev: number, current: number) => {
return Math.abs(current - prev) <= 1
}
return isNumberStable(prev.height, current.height) && isNumberStable(prev.width, current.width)
}
组件代码
import { useControllableValue } from 'ahooks'
export type TextAreaWithPrefix = {
className?: string
style?: React.CSSProperties
innerClassName?: string
innerStyle?: React.CSSProperties
value?: string
onChange?: (value: string) => void
prefix?: ReactNode
}
export const TextAreaWithPrefix: FC<TextAreaWithPrefix> = (props) => {
const { prefix, className, style = {}, innerClassName, innerStyle = {} } = props
// 处理未传value和onChange的情形 确保组件受控
const [value, onChange] = useControllableValue<string>(props)
// 获取前缀的尺寸
const [prefixSize, _setPrefixSize] = useState({ width: 0, height: 0 })
const sizeContainer = useRef<HTMLDivElement>(null)
const setPrefixSize = useCallback(() => {
const size = sizeContainer.current?.getBoundingClientRect()
if (!size) return
const { width, height } = size
_setPrefixSize((prev) => {
if (isSizeStable(prev, { width, height })) return prev
return { width, height }
})
}, [])
useLayoutEffect(setPrefixSize)
// 获取textarea所需的尺寸
const [textAreaSize, _setTextAreaSize] = useState({ width: 0, height: 0 })
const textAreaContainer = useRef<HTMLTextAreaElement>(null)
const setTextAreaSize = useCallback(() => {
if (!textAreaContainer.current) return
const { scrollWidth: width, scrollHeight: height } = textAreaContainer.current
_setTextAreaSize((prev) => {
if (isSizeStable(prev, { width, height })) return prev
return {
width,
height,
}
})
}, [])
useLayoutEffect(setTextAreaSize)
return (
<div
className={cn(
'relative flex resize-y flex-col overflow-auto focus-within:outline',
className,
)}
style={{
minHeight: prefixSize.height,
minWidth: prefixSize.width,
...style,
}}
>
<div className='absolute top-0 left-0' ref={sizeContainer}>
{prefix}
</div>
<textarea
value={value}
onChange={(e) => {
onChange?.(e.target.value)
setPrefixSize()
setTextAreaSize()
}}
// 使textarea填满容器 使用min-height确保滚动发生在容器而不是textarea
className={cn('flex-1 resize-none overflow-hidden outline-none', innerClassName)}
style={{
minHeight: textAreaSize.height,
textIndent: prefixSize.width,
...innerStyle,
}}
/>
<div className='h-0 overflow-hidden'>
<textarea
// 用一个不实际展示textarea计算尺寸
// value必须受控 不然无法计算
value={value}
ref={textAreaContainer}
className={cn('w-full resize-none overflow-hidden outline-none', innerClassName)}
style={{
textIndent: prefixSize.width,
...innerStyle,
}}
/>
</div>
</div>
)
}
需要注意的是 textarea不能单独设行高 因此如果前缀高度超过行高 需要进行特殊调整.\
使用例
const Page: FC = () => {
const prefix = <span className='mx-2'>前缀</span>
return <TextAreaWithPrefix prefix={prefix} className='m-4 w-40' />
}