fix: scope Ctrl/Cmd+Enter send to focused composer

This commit is contained in:
Linus Rath
2026-05-28 18:28:07 +02:00
parent 2818a16f06
commit 7d1fb73290
+12 -11
View File
@@ -854,7 +854,6 @@ export function EmailComposer({
return () => window.removeEventListener('keydown', handleTemplateKey); return () => window.removeEventListener('keydown', handleTemplateKey);
}, []); }, []);
const addFiles = useCallback(async (files: File[]) => { const addFiles = useCallback(async (files: File[]) => {
if (!client || files.length === 0) return; if (!client || files.length === 0) return;
@@ -1575,21 +1574,23 @@ export function EmailComposer({
} }
}; };
// Ctrl+Enter (Windows/Linux) / Cmd+Enter (macOS) sends the open // Ctrl+Enter (Win/Linux) / Cmd+Enter (macOS) sends the open compose
// compose draft — same as every other major mail client. Fires // draft. Scoped to events whose target lives inside this composer's
// even when focus is inside the To/Cc/Bcc chips, subject input, // DOM tree — in Pro mode multiple composer tabs can be mounted at
// body textarea, or the rich-text editor's contentEditable region. // once (inactive tabs are CSS-hidden, not unmounted), so a window
// Plain Enter in the body still inserts a newline; only Enter + // listener would otherwise fire every mounted composer's handleSend
// the platform modifier sends. handleSend is rebound every render, // on a single keystroke. handleSend is rebound every render, so we
// so we route through a ref to keep the window listener stable. // route through a ref to keep the listener stable.
const handleSendRef = useRef<(skipAttachmentCheck?: boolean) => Promise<void>>(); const composerRootRef = useRef<HTMLDivElement | null>(null);
const handleSendRef = useRef<((skipAttachmentCheck?: boolean) => Promise<void>) | undefined>(undefined);
handleSendRef.current = handleSend; handleSendRef.current = handleSend;
useEffect(() => { useEffect(() => {
const handleSendShortcut = (e: KeyboardEvent) => { const handleSendShortcut = (e: KeyboardEvent) => {
if (e.key !== 'Enter') return; if (e.key !== 'Enter') return;
if (!(e.ctrlKey || e.metaKey)) return; if (!(e.ctrlKey || e.metaKey)) return;
// Don't hijack autocomplete-confirm or chip-commit Enters.
if (e.altKey || e.shiftKey) return; if (e.altKey || e.shiftKey) return;
const root = composerRootRef.current;
if (!root || !(e.target instanceof Node) || !root.contains(e.target)) return;
e.preventDefault(); e.preventDefault();
void handleSendRef.current?.(); void handleSendRef.current?.();
}; };
@@ -1636,7 +1637,7 @@ export function EmailComposer({
}; };
return ( return (
<div className={cn("flex h-full bg-background", className)}> <div ref={composerRootRef} className={cn("flex h-full bg-background", className)}>
<PluginSlot <PluginSlot
name="composer-sidebar" name="composer-sidebar"
className="hidden md:flex shrink-0 h-full overflow-hidden border-r border-border" className="hidden md:flex shrink-0 h-full overflow-hidden border-r border-border"