You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 20:43:10