同步 AutoGrowTextArea 支持转发输入事件
改进旧浏览器的高度动态重测逻辑 相关测试完善
This commit is contained in:
@@ -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` 领域校验必须同时拒绝纯附件消息。
|
||||
- 会话管理入口在对话框头部:当前会话标题 + 历史会话下拉(按更新时间倒序)+ 新建对话按钮,全部包在对话框内。
|
||||
|
||||
@@ -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(
|
||||
<AutoGrowTextArea
|
||||
aria-label="非受控自动增长输入框"
|
||||
defaultValue="初始内容"
|
||||
style={{ minHeight: '40px', maxHeight: '128px' }}
|
||||
onInput={onInput}
|
||||
/>,
|
||||
);
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<HTMLTextAreaElement> = (event) => {
|
||||
if (!supportsNativeFieldSizing()) {
|
||||
resizeFallbackTextArea(event.currentTarget);
|
||||
}
|
||||
onInput?.(event);
|
||||
};
|
||||
|
||||
return (
|
||||
<textarea
|
||||
{...textareaProps}
|
||||
ref={textareaRef}
|
||||
className={resolvedClassName}
|
||||
onInput={handleInput}
|
||||
rows={rows}
|
||||
value={value}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user