fix(FN-xxx): preserve quick chat input focus on mobile send
This commit is contained in:
5
.changeset/preserve-quick-chat-focus-mobile-send.md
Normal file
5
.changeset/preserve-quick-chat-focus-mobile-send.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Preserve Quick Chat input focus on mobile send taps so the keyboard stays open.
|
||||||
@@ -894,6 +894,8 @@ export function QuickChatFAB({
|
|||||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
const pendingAttachmentsRef = useRef<PendingAttachment[]>([]);
|
const pendingAttachmentsRef = useRef<PendingAttachment[]>([]);
|
||||||
const shouldAutoFocusComposerRef = useRef(false);
|
const shouldAutoFocusComposerRef = useRef(false);
|
||||||
|
const handledMobileActionRef = useRef(false);
|
||||||
|
const preserveComposerFocusRef = useRef(false);
|
||||||
// Always-mounted offscreen input used to claim the iOS soft keyboard
|
// Always-mounted offscreen input used to claim the iOS soft keyboard
|
||||||
// synchronously inside the FAB click gesture, before the real composer
|
// synchronously inside the FAB click gesture, before the real composer
|
||||||
// input has rendered (or while it is still `disabled` waiting for the
|
// input has rendered (or while it is still `disabled` waiting for the
|
||||||
@@ -1351,6 +1353,12 @@ export function QuickChatFAB({
|
|||||||
input.focus({ preventScroll: true });
|
input.focus({ preventScroll: true });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const markPreserveComposerFocus = useCallback(() => {
|
||||||
|
if (typeof window === "undefined") return;
|
||||||
|
if (window.innerWidth > QUICK_CHAT_DESKTOP_BREAKPOINT) return;
|
||||||
|
preserveComposerFocusRef.current = true;
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleSendMessage = useCallback(async () => {
|
const handleSendMessage = useCallback(async () => {
|
||||||
const trimmed = messageInput.trim();
|
const trimmed = messageInput.trim();
|
||||||
const attachmentsToSend = pendingAttachmentsRef.current;
|
const attachmentsToSend = pendingAttachmentsRef.current;
|
||||||
@@ -1374,6 +1382,7 @@ export function QuickChatFAB({
|
|||||||
// Keep pending attachments on failure so user can retry.
|
// Keep pending attachments on failure so user can retry.
|
||||||
} finally {
|
} finally {
|
||||||
focusComposerInput();
|
focusComposerInput();
|
||||||
|
preserveComposerFocusRef.current = false;
|
||||||
}
|
}
|
||||||
}, [sendMessage, inputDisabled, messageInput, focusComposerInput]);
|
}, [sendMessage, inputDisabled, messageInput, focusComposerInput]);
|
||||||
|
|
||||||
@@ -1468,6 +1477,13 @@ export function QuickChatFAB({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleInputBlur = useCallback(() => {
|
const handleInputBlur = useCallback(() => {
|
||||||
|
if (preserveComposerFocusRef.current) {
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
focusComposerInput();
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Pre-grow the panel ahead of iOS's keyboard dismiss animation so the
|
// Pre-grow the panel ahead of iOS's keyboard dismiss animation so the
|
||||||
// user sees the panel snap to full height immediately instead of
|
// user sees the panel snap to full height immediately instead of
|
||||||
// following the keyboard slide-down. The suppress flag prevents the
|
// following the keyboard slide-down. The suppress flag prevents the
|
||||||
@@ -1498,7 +1514,7 @@ export function QuickChatFAB({
|
|||||||
fileMention.dismissMention();
|
fileMention.dismissMention();
|
||||||
hideMentionPopupTimeoutRef.current = null;
|
hideMentionPopupTimeoutRef.current = null;
|
||||||
}, 120);
|
}, 120);
|
||||||
}, [fileMention]);
|
}, [fileMention, focusComposerInput]);
|
||||||
|
|
||||||
const handleInputFocus = useCallback(() => {
|
const handleInputFocus = useCallback(() => {
|
||||||
// Re-enable visualViewport tracking — the suppress flag set on blur
|
// Re-enable visualViewport tracking — the suppress flag set on blur
|
||||||
@@ -2045,12 +2061,31 @@ export function QuickChatFAB({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="chat-input-stop quick-chat-send-btn"
|
className="chat-input-stop quick-chat-send-btn"
|
||||||
|
onPointerDown={(event) => {
|
||||||
|
if (typeof window === "undefined" || window.innerWidth > QUICK_CHAT_DESKTOP_BREAKPOINT) return;
|
||||||
|
event.preventDefault();
|
||||||
|
if (event.pointerType && event.pointerType !== "mouse") {
|
||||||
|
handledMobileActionRef.current = true;
|
||||||
|
stopStreaming();
|
||||||
|
}
|
||||||
|
}}
|
||||||
onTouchStart={(event) => {
|
onTouchStart={(event) => {
|
||||||
if (typeof window === "undefined" || window.innerWidth > QUICK_CHAT_DESKTOP_BREAKPOINT) return;
|
if (typeof window === "undefined" || window.innerWidth > QUICK_CHAT_DESKTOP_BREAKPOINT) return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
handledMobileActionRef.current = true;
|
||||||
|
stopStreaming();
|
||||||
|
}}
|
||||||
|
onMouseDown={(event) => {
|
||||||
|
if (typeof window === "undefined" || window.innerWidth > QUICK_CHAT_DESKTOP_BREAKPOINT) return;
|
||||||
|
event.preventDefault();
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
if (handledMobileActionRef.current) {
|
||||||
|
handledMobileActionRef.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
stopStreaming();
|
stopStreaming();
|
||||||
}}
|
}}
|
||||||
onClick={stopStreaming}
|
|
||||||
aria-label="Stop generation"
|
aria-label="Stop generation"
|
||||||
data-testid="quick-chat-stop"
|
data-testid="quick-chat-stop"
|
||||||
>
|
>
|
||||||
@@ -2060,13 +2095,33 @@ export function QuickChatFAB({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="quick-chat-send-btn"
|
className="quick-chat-send-btn"
|
||||||
|
onPointerDown={(event) => {
|
||||||
|
if (typeof window === "undefined" || window.innerWidth > QUICK_CHAT_DESKTOP_BREAKPOINT) return;
|
||||||
|
event.preventDefault();
|
||||||
|
if (event.pointerType && event.pointerType !== "mouse") {
|
||||||
|
handledMobileActionRef.current = true;
|
||||||
|
markPreserveComposerFocus();
|
||||||
|
focusComposerInput();
|
||||||
|
void handleSendMessage();
|
||||||
|
}
|
||||||
|
}}
|
||||||
onTouchStart={(event) => {
|
onTouchStart={(event) => {
|
||||||
if (typeof window === "undefined" || window.innerWidth > QUICK_CHAT_DESKTOP_BREAKPOINT) return;
|
if (typeof window === "undefined" || window.innerWidth > QUICK_CHAT_DESKTOP_BREAKPOINT) return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
handledMobileActionRef.current = true;
|
||||||
|
markPreserveComposerFocus();
|
||||||
focusComposerInput();
|
focusComposerInput();
|
||||||
void handleSendMessage();
|
void handleSendMessage();
|
||||||
}}
|
}}
|
||||||
|
onMouseDown={(event) => {
|
||||||
|
if (typeof window === "undefined" || window.innerWidth > QUICK_CHAT_DESKTOP_BREAKPOINT) return;
|
||||||
|
event.preventDefault();
|
||||||
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
if (handledMobileActionRef.current) {
|
||||||
|
handledMobileActionRef.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
void handleSendMessage();
|
void handleSendMessage();
|
||||||
}}
|
}}
|
||||||
disabled={inputDisabled || (messageInput.trim().length === 0 && pendingAttachments.length === 0)}
|
disabled={inputDisabled || (messageInput.trim().length === 0 && pendingAttachments.length === 0)}
|
||||||
|
|||||||
@@ -2156,6 +2156,7 @@ describe("QuickChatFAB", () => {
|
|||||||
|
|
||||||
const sendButton = screen.getByTestId("quick-chat-send");
|
const sendButton = screen.getByTestId("quick-chat-send");
|
||||||
fireEvent.touchStart(sendButton);
|
fireEvent.touchStart(sendButton);
|
||||||
|
fireEvent.click(sendButton);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(mockStreamChatResponse).toHaveBeenCalledTimes(1);
|
expect(mockStreamChatResponse).toHaveBeenCalledTimes(1);
|
||||||
|
|||||||
Reference in New Issue
Block a user