mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-06 17:38:35 +00:00
fix(webui): dismiss mobile keyboard after send
This commit is contained in:
parent
52bc79d3a0
commit
a9bb39b833
@ -75,6 +75,7 @@ import {
|
|||||||
} from "@/hooks/useAttachedImages";
|
} from "@/hooks/useAttachedImages";
|
||||||
import { useClipboardAndDrop } from "@/hooks/useClipboardAndDrop";
|
import { useClipboardAndDrop } from "@/hooks/useClipboardAndDrop";
|
||||||
import { useLogoFallback } from "@/hooks/useLogoFallback";
|
import { useLogoFallback } from "@/hooks/useLogoFallback";
|
||||||
|
import { useMediaQuery } from "@/hooks/useMediaQuery";
|
||||||
import type { SendAttachment, SendOptions } from "@/hooks/useNanobotStream";
|
import type { SendAttachment, SendOptions } from "@/hooks/useNanobotStream";
|
||||||
import { usePageVisibility } from "@/hooks/usePageVisibility";
|
import { usePageVisibility } from "@/hooks/usePageVisibility";
|
||||||
import { useVoiceRecorder, type VoiceRecorderErrorKey } from "@/hooks/useVoiceRecorder";
|
import { useVoiceRecorder, type VoiceRecorderErrorKey } from "@/hooks/useVoiceRecorder";
|
||||||
@ -862,6 +863,7 @@ export function ThreadComposer({
|
|||||||
const [cursorPosition, setCursorPosition] = useState(0);
|
const [cursorPosition, setCursorPosition] = useState(0);
|
||||||
const [recentSlashCommands, setRecentSlashCommands] = useState<string[]>(() => readSlashRecents());
|
const [recentSlashCommands, setRecentSlashCommands] = useState<string[]>(() => readSlashRecents());
|
||||||
const [queuedPrompts, setQueuedPrompts] = useState<QueuedPrompt[]>([]);
|
const [queuedPrompts, setQueuedPrompts] = useState<QueuedPrompt[]>([]);
|
||||||
|
const hasTouchPrimaryPointer = useMediaQuery("(hover: none) and (pointer: coarse)");
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const formRef = useRef<HTMLFormElement>(null);
|
const formRef = useRef<HTMLFormElement>(null);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
@ -964,12 +966,12 @@ export function ThreadComposer({
|
|||||||
} = useClipboardAndDrop(addFiles);
|
} = useClipboardAndDrop(addFiles);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (disabled) return;
|
if (disabled || hasTouchPrimaryPointer) return;
|
||||||
const el = textareaRef.current;
|
const el = textareaRef.current;
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
const id = requestAnimationFrame(() => el.focus());
|
const id = requestAnimationFrame(() => el.focus());
|
||||||
return () => cancelAnimationFrame(id);
|
return () => cancelAnimationFrame(id);
|
||||||
}, [disabled]);
|
}, [disabled, hasTouchPrimaryPointer]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!focusRequest || disabled) return;
|
if (!focusRequest || disabled) return;
|
||||||
@ -1300,13 +1302,13 @@ export function ThreadComposer({
|
|||||||
};
|
};
|
||||||
}, [filteredMentionCandidates.length, filteredSlashCommands.length, showAnyPalette]);
|
}, [filteredMentionCandidates.length, filteredSlashCommands.length, showAnyPalette]);
|
||||||
|
|
||||||
const resizeTextarea = useCallback(() => {
|
const resizeTextarea = useCallback((restoreFocus = true) => {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
const el = textareaRef.current;
|
const el = textareaRef.current;
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
el.style.height = "auto";
|
el.style.height = "auto";
|
||||||
el.style.height = `${Math.min(el.scrollHeight, 260)}px`;
|
el.style.height = `${Math.min(el.scrollHeight, 260)}px`;
|
||||||
el.focus();
|
if (restoreFocus) el.focus();
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -1477,13 +1479,13 @@ export function ThreadComposer({
|
|||||||
[cliAppMention, resizeTextarea, value],
|
[cliAppMention, resizeTextarea, value],
|
||||||
);
|
);
|
||||||
|
|
||||||
const clearComposerText = useCallback(() => {
|
const clearComposerText = useCallback((restoreFocus = true) => {
|
||||||
setValue("");
|
setValue("");
|
||||||
setInlineError(null);
|
setInlineError(null);
|
||||||
setSlashMenuDismissed(false);
|
setSlashMenuDismissed(false);
|
||||||
setCliAppMenuDismissed(false);
|
setCliAppMenuDismissed(false);
|
||||||
setCursorPosition(0);
|
setCursorPosition(0);
|
||||||
resizeTextarea();
|
resizeTextarea(restoreFocus);
|
||||||
}, [resizeTextarea]);
|
}, [resizeTextarea]);
|
||||||
|
|
||||||
const queueGuidancePrompt = useCallback(() => {
|
const queueGuidancePrompt = useCallback(() => {
|
||||||
@ -1692,11 +1694,12 @@ export function ThreadComposer({
|
|||||||
}
|
}
|
||||||
: options,
|
: options,
|
||||||
);
|
);
|
||||||
|
if (hasTouchPrimaryPointer) textareaRef.current?.blur();
|
||||||
setQueuedPrompts([]);
|
setQueuedPrompts([]);
|
||||||
// Bubble owns the data URL copy; safe to revoke every staged blob
|
// Bubble owns the data URL copy; safe to revoke every staged blob
|
||||||
// preview here without affecting the rendered message.
|
// preview here without affecting the rendered message.
|
||||||
clear();
|
clear();
|
||||||
clearComposerText();
|
clearComposerText(!hasTouchPrimaryPointer);
|
||||||
onQuotedContextChange?.(null);
|
onQuotedContextChange?.(null);
|
||||||
}, [
|
}, [
|
||||||
activeCliMentionApps,
|
activeCliMentionApps,
|
||||||
@ -1704,6 +1707,7 @@ export function ThreadComposer({
|
|||||||
canSend,
|
canSend,
|
||||||
clear,
|
clear,
|
||||||
clearComposerText,
|
clearComposerText,
|
||||||
|
hasTouchPrimaryPointer,
|
||||||
handleStop,
|
handleStop,
|
||||||
isStreaming,
|
isStreaming,
|
||||||
maxTextBytes,
|
maxTextBytes,
|
||||||
|
|||||||
@ -336,6 +336,41 @@ function longPress(badge: HTMLElement, pointerId = 7) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("ThreadComposer", () => {
|
describe("ThreadComposer", () => {
|
||||||
|
it("dismisses the touch keyboard after a successful send", async () => {
|
||||||
|
vi.stubGlobal("matchMedia", vi.fn((query: string) => ({
|
||||||
|
matches: query === "(hover: none) and (pointer: coarse)",
|
||||||
|
media: query,
|
||||||
|
onchange: null,
|
||||||
|
addEventListener: vi.fn(),
|
||||||
|
removeEventListener: vi.fn(),
|
||||||
|
dispatchEvent: vi.fn(),
|
||||||
|
})));
|
||||||
|
const onSend = vi.fn();
|
||||||
|
render(
|
||||||
|
<ThreadComposer
|
||||||
|
onSend={onSend}
|
||||||
|
placeholder="Type your message..."
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const input = screen.getByLabelText("Message input");
|
||||||
|
await act(async () => {
|
||||||
|
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
|
||||||
|
});
|
||||||
|
expect(input).not.toHaveFocus();
|
||||||
|
input.focus();
|
||||||
|
expect(input).toHaveFocus();
|
||||||
|
fireEvent.change(input, { target: { value: "hello from mobile" } });
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Send message" }));
|
||||||
|
await act(async () => {
|
||||||
|
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(onSend).toHaveBeenCalledWith("hello from mobile", undefined, undefined);
|
||||||
|
expect(input).toHaveValue("");
|
||||||
|
expect(input).not.toHaveFocus();
|
||||||
|
});
|
||||||
|
|
||||||
it("focuses and sends a removable quoted answer excerpt", async () => {
|
it("focuses and sends a removable quoted answer excerpt", async () => {
|
||||||
const onSend = vi.fn();
|
const onSend = vi.fn();
|
||||||
const onQuotedContextChange = vi.fn();
|
const onQuotedContextChange = vi.fn();
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user