mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-04 08:28:36 +00:00
fix(webui): stabilize thread during IME input
This commit is contained in:
parent
ac216c3e94
commit
eeecfac538
@ -1799,6 +1799,7 @@ export function ThreadComposer({
|
||||
};
|
||||
|
||||
const onInput: React.FormEventHandler<HTMLTextAreaElement> = (e) => {
|
||||
if ((e.nativeEvent as InputEvent).isComposing) return;
|
||||
const el = e.currentTarget;
|
||||
el.style.height = "auto";
|
||||
el.style.height = `${Math.min(el.scrollHeight, 260)}px`;
|
||||
|
||||
@ -186,6 +186,7 @@ export const ThreadViewport = forwardRef<ThreadViewportHandle, ThreadViewportPro
|
||||
const pendingPromptJumpRef = useRef<string | null>(null);
|
||||
const restoreScrollAfterPrependRef =
|
||||
useRef<{ height: number; top: number } | null>(null);
|
||||
const composerInputScrollTopRef = useRef<number | null>(null);
|
||||
const composerDockHeightRef = useRef(0);
|
||||
const [atBottom, setAtBottom] = useState(true);
|
||||
const [composerDockHeight, setComposerDockHeight] = useState(0);
|
||||
@ -688,9 +689,23 @@ export const ThreadViewport = forwardRef<ThreadViewportHandle, ThreadViewportPro
|
||||
data-testid="thread-composer-dock"
|
||||
onInputCapture={(event) => {
|
||||
if (event.target instanceof HTMLTextAreaElement) {
|
||||
composerInputScrollTopRef.current = scrollRef.current?.scrollTop ?? null;
|
||||
threadMotionRef.current?.handleComposerInput();
|
||||
}
|
||||
}}
|
||||
onInput={(event) => {
|
||||
if (!(event.target instanceof HTMLTextAreaElement)) return;
|
||||
const previousScrollTop = composerInputScrollTopRef.current;
|
||||
composerInputScrollTopRef.current = null;
|
||||
const scrollEl = scrollRef.current;
|
||||
if (scrollEl && previousScrollTop !== null) {
|
||||
// Textarea autosizing briefly collapses to `height: auto` while
|
||||
// measuring. Chrome can clamp the sibling thread scrollport in
|
||||
// that intermediate layout; restore it before paint, then let
|
||||
// ResizeObserver handle any real final composer height change.
|
||||
scrollEl.scrollTop = previousScrollTop;
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
"row-start-2 z-10 w-full",
|
||||
hasMessages ? "relative bg-background" : "relative self-center",
|
||||
|
||||
@ -441,6 +441,33 @@ describe("ThreadComposer", () => {
|
||||
expect(input.parentElement?.parentElement?.className).toContain("max-w-[58rem]");
|
||||
});
|
||||
|
||||
it("defers textarea autosizing until IME composition commits", () => {
|
||||
render(
|
||||
<ThreadComposer
|
||||
onSend={vi.fn()}
|
||||
placeholder="Type your message..."
|
||||
/>,
|
||||
);
|
||||
const input = screen.getByLabelText("Message input") as HTMLTextAreaElement;
|
||||
Object.defineProperty(input, "scrollHeight", {
|
||||
configurable: true,
|
||||
value: 120,
|
||||
});
|
||||
input.style.height = "50px";
|
||||
|
||||
fireEvent.input(input, {
|
||||
target: { value: "zhongwen" },
|
||||
isComposing: true,
|
||||
});
|
||||
expect(input.style.height).toBe("50px");
|
||||
|
||||
fireEvent.input(input, {
|
||||
target: { value: "中文" },
|
||||
isComposing: false,
|
||||
});
|
||||
expect(input.style.height).toBe("120px");
|
||||
});
|
||||
|
||||
it("lets long model preset labels use their intrinsic width", () => {
|
||||
render(
|
||||
<ThreadComposer
|
||||
|
||||
@ -1116,6 +1116,36 @@ describe("ThreadViewport", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("restores thread scroll after the textarea autosize measurement collapses it", () => {
|
||||
let scroller: HTMLElement | null = null;
|
||||
const { container } = render(
|
||||
<ThreadViewport
|
||||
messages={messages}
|
||||
isStreaming={false}
|
||||
composer={(
|
||||
<textarea
|
||||
aria-label="Message input"
|
||||
onInput={() => {
|
||||
if (scroller) scroller.scrollTop = 692;
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>,
|
||||
);
|
||||
scroller = getScroller(container);
|
||||
Object.defineProperty(scroller, "scrollTop", {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: 700,
|
||||
});
|
||||
|
||||
fireEvent.input(screen.getByLabelText("Message input"), {
|
||||
target: { value: "中文" },
|
||||
});
|
||||
|
||||
expect(scroller.scrollTop).toBe(700);
|
||||
});
|
||||
|
||||
it("keeps the thread scrollport above a mobile soft keyboard", async () => {
|
||||
const visualViewport = stubVisualViewport({ innerHeight: 800, height: 480 });
|
||||
try {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user