Next.js PWA聊天应用iOS端textarea光标越界问题求助
iOS端Textarea光标超出边界问题修复方案
问题分析
这个iOS独有的bug源于Safari在textarea动态修改高度时,光标位置的渲染未同步跟上DOM高度变化,尤其是快速输入/删除多行内容时,DOM更新与光标重绘的时序错位,最终导致光标溢出边界。Android端无此问题是因为Chrome的渲染机制对这类同步处理更及时。
修复方案
1. 改用useLayoutEffect确保DOM更新后立即调整高度
useEffect在浏览器绘制完成后执行,此时光标已渲染,再调整高度会导致错位;useLayoutEffect在DOM更新后、浏览器绘制前执行,能保证高度调整与光标渲染同步。
2. 加入requestAnimationFrame确保计算时机准确
iOS Safari中scrollHeight的计算可能存在微小延迟,用requestAnimationFrame可让高度计算在浏览器下一次重绘前执行,避免高度计算偏差。
3. 重置光标位置(极端场景补充)
若高度调整后光标仍错位,可手动将光标定位到内容末尾,强制Safari重绘光标位置。
4. 优化CSS样式
确保textarea的内边距、行高在iOS下一致,同时重置默认样式避免干扰。
修改后的代码
Input组件修改:
import { Textarea, TextareaProps } from '@/components/ui/textarea' import { useBounds } from '@/hooks/bounds' import { cn } from '@/utils/utils' import React, { useLayoutEffect, useRef, useState } from 'react' interface IProps extends TextareaProps { isLoading?: boolean formRef?: React.RefObject<HTMLFormElement> } export const Input = ({ className, isLoading, formRef, ...rest }: IProps) => { const ref = useRef<HTMLTextAreaElement>(null) useLayoutEffect(() => { const adjustHeight = () => { if (!ref.current) return const textarea = ref.current // 先重置高度为auto,确保scrollHeight计算准确 textarea.style.height = 'auto' const scrollHeight = textarea.scrollHeight // 设置高度时预留2px余量,避免光标刚好触边界 textarea.style.height = `${scrollHeight + 2}px` // 可选:重置光标到内容末尾,强制Safari重绘光标位置 if (textarea.selectionStart !== undefined) { const contentLength = textarea.value.length textarea.setSelectionRange(contentLength, contentLength) } } // 用requestAnimationFrame保证时序正确 const frameId = requestAnimationFrame(adjustHeight) return () => cancelAnimationFrame(frameId) }, [rest.value]) const { isDesktop } = useBounds() return ( <Textarea ref={ref} onKeyDown={e => { if(isDesktop && e.key.toLowerCase() === 'enter' && !e.shiftKey) { e.preventDefault(); formRef?.current?.requestSubmit(); } }} className={cn('resize-none overflow-y-hidden px-3 py-2.5 bg-background leading-normal', className)} {...rest} /> ) }
Textarea组件补充样式:
import * as React from 'react' import { cn } from '@/utils/utils' export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {} const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>( ({ className, ...props }, ref) => { return ( <textarea className={cn( 'flex w-full rounded-md border border-input bg-transparent px-2 py-1 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50', // 重置iOS默认样式,避免渲染干扰 '-webkit-appearance: none; resize: none;', className )} ref={ref} {...props} /> ) } ) Textarea.displayName = 'Textarea' export { Textarea }
关键说明
- 改用
useLayoutEffect替代useEffect,保证高度调整在浏览器绘制前完成,避免光标渲染滞后。 - 把
height: 0px改成height: auto,让textarea先自动适配内容,再获取准确的scrollHeight,避免计算偏差。 - 给高度加2px余量,避免内容刚好填满时光标溢出边界。
- 可选的光标重置逻辑,在极端输入场景下强制Safari更新光标位置。
- 重置iOS的
-webkit-appearance属性,消除默认样式对textarea渲染的干扰。
内容的提问来源于stack exchange,提问作者ijprocel
相关产品推荐
相关产品推荐

