compose and add-account are real forms, so enter submits them
Compose's To/Cc/Bcc/Subject fields, and each of the three add-account forms, sat in a plain div — enter did nothing and the button was the only way in. Wrapped each in a <form onSubmit>, marked every button that isn't the submit action type="button" (attach, cancel, cc/bcc toggles, the recipient-suggestion picks), so none of them accidentally submit now that a form exists around them. The compose body stays a contenteditable div, which never triggers native enter-to-submit, so Cmd/Ctrl+Enter wasn't needed to keep newlines working. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+121
-100
@@ -70,6 +70,7 @@ const RecipientInput = ({
|
||||
{suggestions.map((s) => (
|
||||
<button
|
||||
key={s.address}
|
||||
type="button"
|
||||
onMouseDown={(ev) => {
|
||||
ev.preventDefault();
|
||||
pick(s.address);
|
||||
@@ -215,7 +216,10 @@ export const ComposeModal = () => {
|
||||
setDraft(null);
|
||||
};
|
||||
|
||||
const send = async () => {
|
||||
// The body is a contenteditable div, not an <input>/<textarea>, so it never triggers a native
|
||||
// Enter-to-submit — only the To/Cc/Bcc/Subject fields do, which is the point.
|
||||
const send = async (ev?: React.FormEvent) => {
|
||||
ev?.preventDefault();
|
||||
if (!to.trim()) {
|
||||
toast.error('Add at least one recipient');
|
||||
return;
|
||||
@@ -286,115 +290,132 @@ export const ComposeModal = () => {
|
||||
Drop images to inline · other files attach
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center border-b px-4 py-2.5 pr-10">
|
||||
<DialogTitle className="text-sm font-semibold">{/^re:/i.test(subject) ? 'Reply' : 'New message'}</DialogTitle>
|
||||
</div>
|
||||
{/* A real <form>: Enter in To/Cc/Bcc/Subject sends, matching every other single-line field.
|
||||
`contents` keeps it out of the flex layout — the fields around it don't know it's there. */}
|
||||
<form onSubmit={send} className="contents">
|
||||
<div className="flex items-center border-b px-4 py-2.5 pr-10">
|
||||
<DialogTitle className="text-sm font-semibold">
|
||||
{/^re:/i.test(subject) ? 'Reply' : 'New message'}
|
||||
</DialogTitle>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center border-b">
|
||||
<span className="pl-4 text-xs opacity-50">To</span>
|
||||
<div className="flex-1">
|
||||
<RecipientInput value={to} onChange={setTo} placeholder="Recipients" autoFocus />
|
||||
</div>
|
||||
<div className="flex items-center gap-2 px-3">
|
||||
{!showCc && (
|
||||
<button onClick={() => setShowCc(true)} className="text-xs opacity-50 hover:opacity-100 cursor-pointer">
|
||||
Cc
|
||||
</button>
|
||||
)}
|
||||
{!showBcc && (
|
||||
<button onClick={() => setShowBcc(true)} className="text-xs opacity-50 hover:opacity-100 cursor-pointer">
|
||||
Bcc
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{showCc && (
|
||||
<div className="flex items-center border-b">
|
||||
<span className="pl-4 text-xs opacity-50">Cc</span>
|
||||
<span className="pl-4 text-xs opacity-50">To</span>
|
||||
<div className="flex-1">
|
||||
<RecipientInput value={cc} onChange={setCc} placeholder="Cc recipients" />
|
||||
<RecipientInput value={to} onChange={setTo} placeholder="Recipients" autoFocus />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{showBcc && (
|
||||
<div className="flex items-center border-b">
|
||||
<span className="pl-4 text-xs opacity-50">Bcc</span>
|
||||
<div className="flex-1">
|
||||
<RecipientInput value={bcc} onChange={setBcc} placeholder="Bcc recipients" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
value={subject}
|
||||
onChange={(ev) => setSubject(ev.target.value)}
|
||||
placeholder="Subject"
|
||||
className="border-b bg-transparent px-4 py-2 text-sm outline-none placeholder:opacity-40"
|
||||
/>
|
||||
<div className="relative flex-1 overflow-hidden">
|
||||
{bodyEmpty && (
|
||||
<div className="pointer-events-none absolute left-4 top-3 text-sm opacity-40">Write your message…</div>
|
||||
)}
|
||||
<div
|
||||
ref={editorRef}
|
||||
contentEditable
|
||||
onInput={refreshEmpty}
|
||||
onPaste={onEditorPaste}
|
||||
className="h-full overflow-y-auto px-4 py-3 text-sm outline-none [&_img]:my-1 [&_img]:rounded"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{files.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5 border-t px-4 py-2">
|
||||
{files.map((f, i) => (
|
||||
<span key={`${f.name}-${i}`} className="flex items-center gap-1.5 rounded bg-accent px-2 py-1 text-xs">
|
||||
<Paperclip className="h-3 w-3 opacity-60" />
|
||||
<span className="max-w-[16rem] truncate">{f.name}</span>
|
||||
<span className="opacity-50">{fmtSize(f.size)}</span>
|
||||
<div className="flex items-center gap-2 px-3">
|
||||
{!showCc && (
|
||||
<button
|
||||
onClick={() => setFiles((prev) => prev.filter((_, idx) => idx !== i))}
|
||||
className="opacity-50 hover:opacity-100 cursor-pointer"
|
||||
title="Remove"
|
||||
type="button"
|
||||
onClick={() => setShowCc(true)}
|
||||
className="text-xs opacity-50 hover:opacity-100 cursor-pointer"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
Cc
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
)}
|
||||
{!showBcc && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowBcc(true)}
|
||||
className="text-xs opacity-50 hover:opacity-100 cursor-pointer"
|
||||
>
|
||||
Bcc
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-end gap-2 border-t px-4 py-2.5">
|
||||
{showCc && (
|
||||
<div className="flex items-center border-b">
|
||||
<span className="pl-4 text-xs opacity-50">Cc</span>
|
||||
<div className="flex-1">
|
||||
<RecipientInput value={cc} onChange={setCc} placeholder="Cc recipients" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{showBcc && (
|
||||
<div className="flex items-center border-b">
|
||||
<span className="pl-4 text-xs opacity-50">Bcc</span>
|
||||
<div className="flex-1">
|
||||
<RecipientInput value={bcc} onChange={setBcc} placeholder="Bcc recipients" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
ref={fileInput}
|
||||
type="file"
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={(ev) => {
|
||||
if (ev.target.files?.length) addAttachments(ev.target.files);
|
||||
ev.target.value = '';
|
||||
}}
|
||||
value={subject}
|
||||
onChange={(ev) => setSubject(ev.target.value)}
|
||||
placeholder="Subject"
|
||||
className="border-b bg-transparent px-4 py-2 text-sm outline-none placeholder:opacity-40"
|
||||
/>
|
||||
<button
|
||||
onClick={() => fileInput.current?.click()}
|
||||
className="mr-auto flex items-center gap-1.5 rounded-md px-2 py-1.5 text-sm opacity-60 hover:opacity-100 cursor-pointer"
|
||||
title="Attach files"
|
||||
>
|
||||
<Paperclip className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={close}
|
||||
className="rounded-md px-4 py-1.5 text-sm opacity-60 hover:opacity-100 cursor-pointer"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={send}
|
||||
disabled={sending}
|
||||
className="flex items-center gap-1.5 rounded-md bg-duck-teal px-4 py-1.5 text-sm font-medium text-white hover:bg-duck-teal/90 disabled:opacity-50 cursor-pointer"
|
||||
>
|
||||
{sending ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Send className="h-3.5 w-3.5" />}
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
<div className="relative flex-1 overflow-hidden">
|
||||
{bodyEmpty && (
|
||||
<div className="pointer-events-none absolute left-4 top-3 text-sm opacity-40">Write your message…</div>
|
||||
)}
|
||||
<div
|
||||
ref={editorRef}
|
||||
contentEditable
|
||||
onInput={refreshEmpty}
|
||||
onPaste={onEditorPaste}
|
||||
className="h-full overflow-y-auto px-4 py-3 text-sm outline-none [&_img]:my-1 [&_img]:rounded"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{files.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5 border-t px-4 py-2">
|
||||
{files.map((f, i) => (
|
||||
<span key={`${f.name}-${i}`} className="flex items-center gap-1.5 rounded bg-accent px-2 py-1 text-xs">
|
||||
<Paperclip className="h-3 w-3 opacity-60" />
|
||||
<span className="max-w-[16rem] truncate">{f.name}</span>
|
||||
<span className="opacity-50">{fmtSize(f.size)}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setFiles((prev) => prev.filter((_, idx) => idx !== i))}
|
||||
className="opacity-50 hover:opacity-100 cursor-pointer"
|
||||
title="Remove"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-end gap-2 border-t px-4 py-2.5">
|
||||
<input
|
||||
ref={fileInput}
|
||||
type="file"
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={(ev) => {
|
||||
if (ev.target.files?.length) addAttachments(ev.target.files);
|
||||
ev.target.value = '';
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fileInput.current?.click()}
|
||||
className="mr-auto flex items-center gap-1.5 rounded-md px-2 py-1.5 text-sm opacity-60 hover:opacity-100 cursor-pointer"
|
||||
title="Attach files"
|
||||
>
|
||||
<Paperclip className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={close}
|
||||
className="rounded-md px-4 py-1.5 text-sm opacity-60 hover:opacity-100 cursor-pointer"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={sending}
|
||||
className="flex items-center gap-1.5 rounded-md bg-duck-teal px-4 py-1.5 text-sm font-medium text-white hover:bg-duck-teal/90 disabled:opacity-50 cursor-pointer"
|
||||
>
|
||||
{sending ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Send className="h-3.5 w-3.5" />}
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user