From 58b07d451aca31b88478b9b52012637cd1d88d36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Tue, 4 Aug 2026 10:34:28 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=8C=E6=AD=A5=20AutoGrowTextArea=20?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E8=BD=AC=E5=8F=91=E8=BE=93=E5=85=A5=E4=BA=8B?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 改进旧浏览器的高度动态重测逻辑 相关测试完善 --- .../【编辑器】画布Agent对话面板-2026-07-03.md | 2 +- .../common/AutoGrowTextArea.test.tsx | 60 +++++++++++++++++++ src/components/common/AutoGrowTextArea.tsx | 9 +++ 3 files changed, 70 insertions(+), 1 deletion(-) diff --git a/docs/【编辑器】画布Agent对话面板-2026-07-03.md b/docs/【编辑器】画布Agent对话面板-2026-07-03.md index 4a1c4c217..c1cb6c4a7 100644 --- a/docs/【编辑器】画布Agent对话面板-2026-07-03.md +++ b/docs/【编辑器】画布Agent对话面板-2026-07-03.md @@ -61,7 +61,7 @@ - 对话框与左侧素材 / 图层侧栏**不互斥**,允许同时展开,便于在对话中选取和核对画布素材;左侧栏切换不改变 Agent 面板开关状态。 - 桌面端对话框固定宽约 360–400px;移动端抽屉式全宽覆盖;收起态为胶囊/圆形入口按钮。 - 底部消息输入框随输入内容从单行高度自动增长,最大高度为 - 128px;原生自适应和兼容降级统一由公共 `AutoGrowTextArea` 组件承载,`EditorAgentDraftTextarea` 只保留 Agent 专属视觉、粘贴和 Enter 提交语义。支持 `field-sizing: content` 的浏览器使用原生内容尺寸自适应,不支持该属性的旧 Safari / iOS WebView 使用前端测量降级,并在宽度变化时重新计算换行高度。内容超过最大高度后停止增长并启用内部纵向滚动,内容缩短或清空后同步收缩。内部滚动条使用浅灰窄滑块和透明轨道,上下留白不得溢出输入框圆角边界;输入框在窄屏下允许收缩且不产生横向滚动。 + 128px;原生自适应和兼容降级统一由公共 `AutoGrowTextArea` 组件承载,`EditorAgentDraftTextarea` 只保留 Agent 专属视觉、粘贴和 Enter 提交语义。支持 `field-sizing: content` 的浏览器使用原生内容尺寸自适应;不支持该属性的旧 Safari / iOS WebView 使用前端测量降级,受控 `value` 更新、非受控输入事件和宽度变化都必须重新计算换行高度,同时继续转发调用方的 `onInput`。内容超过最大高度后停止增长并启用内部纵向滚动,内容缩短或清空后同步收缩。内部滚动条使用浅灰窄滑块和透明轨道,上下留白不得溢出输入框圆角边界;输入框在窄屏下允许收缩且不产生横向滚动。 - Enter 发送必须同时排除 `isComposing` 和旧 Safari / WebKit 候选词确认事件的 `keyCode === 229`,避免输入法选词时误发送。 - 用户消息必须包含去除首尾空白后的非空文本;附件只能随文本消息发送,前端发送门禁与后端 `module-editor-agent` 领域校验必须同时拒绝纯附件消息。 - 会话管理入口在对话框头部:当前会话标题 + 历史会话下拉(按更新时间倒序)+ 新建对话按钮,全部包在对话框内。 diff --git a/src/components/common/AutoGrowTextArea.test.tsx b/src/components/common/AutoGrowTextArea.test.tsx index d912a7c94..e415ecdc9 100644 --- a/src/components/common/AutoGrowTextArea.test.tsx +++ b/src/components/common/AutoGrowTextArea.test.tsx @@ -129,4 +129,64 @@ describe('AutoGrowTextArea', () => { unmount(); expect(disconnect).toHaveBeenCalledTimes(1); }); + + it('remeasures uncontrolled input changes and forwards the consumer input handler', () => { + const onInput = vi.fn(); + vi.stubGlobal('CSS', { supports: vi.fn().mockReturnValue(false) }); + vi.stubGlobal( + 'ResizeObserver', + vi.fn().mockImplementation(() => ({ + observe: vi.fn(), + unobserve: vi.fn(), + disconnect: vi.fn(), + })), + ); + + render( + , + ); + const input = screen.getByLabelText( + '非受控自动增长输入框', + ) as HTMLTextAreaElement; + let contentHeight = 90; + Object.defineProperty(input, 'offsetHeight', { + configurable: true, + get: () => { + const styleHeight = Number.parseFloat(input.style.height); + return Number.isNaN(styleHeight) ? 42 : styleHeight; + }, + }); + Object.defineProperty(input, 'clientHeight', { + configurable: true, + get: () => input.offsetHeight - 2, + }); + Object.defineProperty(input, 'scrollHeight', { + configurable: true, + get: () => contentHeight, + }); + + fireEvent.input(input, { + target: { value: '第一行\n第二行\n第三行\n第四行' }, + }); + expect(input.style.height).toBe('92px'); + expect(input.style.overflowY).toBe('hidden'); + + contentHeight = 180; + fireEvent.input(input, { + target: { value: '很多行内容'.repeat(30) }, + }); + expect(input.style.height).toBe('128px'); + expect(input.style.overflowY).toBe('auto'); + + contentHeight = 20; + fireEvent.input(input, { target: { value: '短内容' } }); + expect(input.style.height).toBe('40px'); + expect(input.style.overflowY).toBe('hidden'); + expect(onInput).toHaveBeenCalledTimes(3); + }); }); diff --git a/src/components/common/AutoGrowTextArea.tsx b/src/components/common/AutoGrowTextArea.tsx index 0005511ad..93ee0bfa1 100644 --- a/src/components/common/AutoGrowTextArea.tsx +++ b/src/components/common/AutoGrowTextArea.tsx @@ -1,4 +1,5 @@ import { + type InputEventHandler, type TextareaHTMLAttributes, useLayoutEffect, useRef, @@ -50,6 +51,7 @@ function resizeFallbackTextArea(textarea: HTMLTextAreaElement) { export function AutoGrowTextArea({ className, + onInput, rows = 1, value, ...textareaProps @@ -103,12 +105,19 @@ export function AutoGrowTextArea({ ] .filter(Boolean) .join(' '); + const handleInput: InputEventHandler = (event) => { + if (!supportsNativeFieldSizing()) { + resizeFallbackTextArea(event.currentTarget); + } + onInput?.(event); + }; return (