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:
2026-08-19 10:45:21 +00:00
co-authored by Claude Opus 5
parent bc9934fc55
commit 3db9534614
2 changed files with 143 additions and 113 deletions
+26 -5
View File
@@ -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,8 +290,13 @@ export const ComposeModal = () => {
Drop images to inline · other files attach
</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>
<DialogTitle className="text-sm font-semibold">
{/^re:/i.test(subject) ? 'Reply' : 'New message'}
</DialogTitle>
</div>
<div className="flex items-center border-b">
@@ -297,12 +306,20 @@ export const ComposeModal = () => {
</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">
<button
type="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">
<button
type="button"
onClick={() => setShowBcc(true)}
className="text-xs opacity-50 hover:opacity-100 cursor-pointer"
>
Bcc
</button>
)}
@@ -351,6 +368,7 @@ export const ComposeModal = () => {
<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"
@@ -374,6 +392,7 @@ export const ComposeModal = () => {
}}
/>
<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"
@@ -381,13 +400,14 @@ export const ComposeModal = () => {
<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
onClick={send}
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"
>
@@ -395,6 +415,7 @@ export const ComposeModal = () => {
Send
</button>
</div>
</form>
</DialogContent>
</Dialog>
);
+22 -13
View File
@@ -85,7 +85,8 @@ export const EmailAccounts = () => {
}
};
const handleGmailOAuth = async () => {
const handleGmailOAuth = async (ev?: React.FormEvent) => {
ev?.preventDefault();
setSaving(true);
try {
const status = await client.get<{ connected: boolean; email: string | null }>('/integrations/google/status');
@@ -116,7 +117,8 @@ export const EmailAccounts = () => {
}
};
const handlePasswordSubmit = async () => {
const handlePasswordSubmit = async (ev?: React.FormEvent) => {
ev?.preventDefault();
if (!form.email.trim() || !form.password.trim()) return;
setSaving(true);
try {
@@ -304,7 +306,10 @@ export const EmailAccounts = () => {
{/* Gmail OAuth form */}
{activeForm === 'gmail-oauth' && (
<div className="grid gap-3 rounded-lg border border-duck-dark/10 dark:border-foreground/10 p-4">
<form
onSubmit={handleGmailOAuth}
className="grid gap-3 rounded-lg border border-duck-dark/10 dark:border-foreground/10 p-4"
>
<p className="text-sm font-medium text-duck-dark dark:text-foreground">Add Gmail (OAuth)</p>
<input
type="text"
@@ -317,16 +322,19 @@ export const EmailAccounts = () => {
<Button type="button" variant="ghost" onClick={resetForm} className="h-9 cursor-pointer">
Cancel
</Button>
<Button type="button" onClick={handleGmailOAuth} disabled={saving} className="h-9 flex-1 cursor-pointer">
<Button type="submit" disabled={saving} className="h-9 flex-1 cursor-pointer">
{saving ? <Loader2 className="h-4 w-4 animate-spin" /> : 'Connect & Add'}
</Button>
</div>
</div>
</form>
)}
{/* Gmail App Password form */}
{activeForm === 'gmail-password' && (
<div className="grid gap-3 rounded-lg border border-duck-dark/10 dark:border-foreground/10 p-4">
<form
onSubmit={handlePasswordSubmit}
className="grid gap-3 rounded-lg border border-duck-dark/10 dark:border-foreground/10 p-4"
>
<p className="text-sm font-medium text-duck-dark dark:text-foreground">Add Gmail (App Password)</p>
<div className="text-xs text-duck-dark/50 dark:text-foreground/50 grid gap-1.5">
<p>To create an App Password:</p>
@@ -373,20 +381,22 @@ export const EmailAccounts = () => {
Cancel
</Button>
<Button
type="button"
onClick={handlePasswordSubmit}
type="submit"
disabled={!form.email.trim() || !form.password.trim() || saving}
className="h-9 flex-1 cursor-pointer"
>
{saving ? <Loader2 className="h-4 w-4 animate-spin" /> : 'Test & Save'}
</Button>
</div>
</div>
</form>
)}
{/* Generic IMAP form */}
{activeForm === 'imap' && (
<div className="grid gap-3 rounded-lg border border-duck-dark/10 dark:border-foreground/10 p-4">
<form
onSubmit={handlePasswordSubmit}
className="grid gap-3 rounded-lg border border-duck-dark/10 dark:border-foreground/10 p-4"
>
<p className="text-sm font-medium text-duck-dark dark:text-foreground">Add IMAP Account</p>
<input
type="email"
@@ -439,15 +449,14 @@ export const EmailAccounts = () => {
Cancel
</Button>
<Button
type="button"
onClick={handlePasswordSubmit}
type="submit"
disabled={!form.email.trim() || !form.password.trim() || !form.imapHost.trim() || saving}
className="h-9 flex-1 cursor-pointer"
>
{saving ? <Loader2 className="h-4 w-4 animate-spin" /> : 'Test & Save'}
</Button>
</div>
</div>
</form>
)}
{/* Add account button */}