fix(chat): keep mobile send from losing focus

This commit is contained in:
2026-07-01 20:00:54 +08:00
parent 1cb031f793
commit 8586e87ab6
4 changed files with 52 additions and 5 deletions
+33 -5
View File
@@ -10,6 +10,7 @@ import { ChatSendButton } from "./chat-send-button";
import styles from "./chat-input-bar.module.css";
const log = new Logger("AppChatComponentsChatInputBar");
const POINTER_SEND_CLICK_DEDUPE_MS = 500;
export interface ChatInputBarProps {
disabled?: boolean;
@@ -19,12 +20,15 @@ export function ChatInputBar({ disabled = false }: ChatInputBarProps) {
const dispatch = useChatDispatch();
const [input, setInput] = useState("");
const [isFocused, setIsFocused] = useState(false);
const barRef = useRef<HTMLDivElement>(null);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const lastPointerSendAtRef = useRef(0);
const hasContent = input.trim().length > 0;
useKeyboardHeight({
targetRef: textareaRef,
containerRef: barRef,
active: isFocused,
onKeyboardDismiss: () => {
log.important("info", "[chat-input-bar] keyboard dismissed", {
@@ -51,9 +55,23 @@ export function ChatInputBar({ disabled = false }: ChatInputBarProps) {
setIsFocused(focused);
};
const handleSend = () => {
if (!hasContent) return;
const handleSend = (source: "click" | "keyboard" | "pointerdown") => {
if (source === "click") {
const elapsed = Date.now() - lastPointerSendAtRef.current;
if (elapsed >= 0 && elapsed < POINTER_SEND_CLICK_DEDUPE_MS) {
log.important("info", "[chat-input-bar] suppress duplicate click", {
elapsed,
contentLength: input.length,
hasContent,
disabled,
isFocused,
});
return;
}
}
if (disabled || !hasContent) return;
log.debug("[chat-input-bar] handleSend", {
source,
contentLength: input.length,
contentPreview: input.slice(0, 50),
hasContent,
@@ -61,6 +79,7 @@ export function ChatInputBar({ disabled = false }: ChatInputBarProps) {
isFocused,
});
log.important("info", "[chat-input-bar] send", {
source,
contentLength: input.length,
hasContent,
disabled,
@@ -71,21 +90,30 @@ export function ChatInputBar({ disabled = false }: ChatInputBarProps) {
textareaRef.current?.focus();
};
const handlePointerDownSend = () => {
lastPointerSendAtRef.current = Date.now();
handleSend("pointerdown");
};
return (
<div className={`${styles.bar} ${isFocused ? styles.barFocused : ""}`}>
<div
ref={barRef}
className={`${styles.bar} ${isFocused ? styles.barFocused : ""}`}
>
<div className={`${styles.row} ${isFocused ? styles.rowFocused : ""}`}>
<ChatInputTextField
ref={textareaRef}
value={input}
onChange={handleInputChange}
onSubmit={handleSend}
onSubmit={() => handleSend("keyboard")}
onFocusChange={handleFocusChange}
disabled={disabled}
/>
<ChatSendButton
disabled={disabled}
hasContent={hasContent}
onClick={handleSend}
onClick={() => handleSend("click")}
onPointerDownSend={handlePointerDownSend}
/>
</div>
</div>
@@ -18,12 +18,14 @@ export interface ChatSendButtonProps {
disabled: boolean;
hasContent: boolean;
onClick: () => void;
onPointerDownSend: () => void;
}
export function ChatSendButton({
disabled,
hasContent,
onClick,
onPointerDownSend,
}: ChatSendButtonProps) {
return (
<button
@@ -32,6 +34,13 @@ export function ChatSendButton({
hasContent && !disabled ? styles.buttonActive : styles.buttonEmpty
}`}
disabled={disabled}
onPointerDown={(event) => {
if (event.pointerType === "mouse") return;
if (disabled) return;
event.preventDefault();
if (!hasContent) return;
onPointerDownSend();
}}
onClick={onClick}
aria-label="Send message"
>