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:
+26
-5
@@ -70,6 +70,7 @@ const RecipientInput = ({
|
|||||||
{suggestions.map((s) => (
|
{suggestions.map((s) => (
|
||||||
<button
|
<button
|
||||||
key={s.address}
|
key={s.address}
|
||||||
|
type="button"
|
||||||
onMouseDown={(ev) => {
|
onMouseDown={(ev) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
pick(s.address);
|
pick(s.address);
|
||||||
@@ -215,7 +216,10 @@ export const ComposeModal = () => {
|
|||||||
setDraft(null);
|
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()) {
|
if (!to.trim()) {
|
||||||
toast.error('Add at least one recipient');
|
toast.error('Add at least one recipient');
|
||||||
return;
|
return;
|
||||||
@@ -286,8 +290,13 @@ export const ComposeModal = () => {
|
|||||||
Drop images to inline · other files attach
|
Drop images to inline · other files attach
|
||||||
</div>
|
</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">
|
<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>
|
||||||
|
|
||||||
<div className="flex items-center border-b">
|
<div className="flex items-center border-b">
|
||||||
@@ -297,12 +306,20 @@ export const ComposeModal = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 px-3">
|
<div className="flex items-center gap-2 px-3">
|
||||||
{!showCc && (
|
{!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
|
Cc
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{!showBcc && (
|
{!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
|
Bcc
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
@@ -351,6 +368,7 @@ export const ComposeModal = () => {
|
|||||||
<span className="max-w-[16rem] truncate">{f.name}</span>
|
<span className="max-w-[16rem] truncate">{f.name}</span>
|
||||||
<span className="opacity-50">{fmtSize(f.size)}</span>
|
<span className="opacity-50">{fmtSize(f.size)}</span>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => setFiles((prev) => prev.filter((_, idx) => idx !== i))}
|
onClick={() => setFiles((prev) => prev.filter((_, idx) => idx !== i))}
|
||||||
className="opacity-50 hover:opacity-100 cursor-pointer"
|
className="opacity-50 hover:opacity-100 cursor-pointer"
|
||||||
title="Remove"
|
title="Remove"
|
||||||
@@ -374,6 +392,7 @@ export const ComposeModal = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => fileInput.current?.click()}
|
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"
|
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"
|
title="Attach files"
|
||||||
@@ -381,13 +400,14 @@ export const ComposeModal = () => {
|
|||||||
<Paperclip className="h-4 w-4" />
|
<Paperclip className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={close}
|
onClick={close}
|
||||||
className="rounded-md px-4 py-1.5 text-sm opacity-60 hover:opacity-100 cursor-pointer"
|
className="rounded-md px-4 py-1.5 text-sm opacity-60 hover:opacity-100 cursor-pointer"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={send}
|
type="submit"
|
||||||
disabled={sending}
|
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"
|
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
|
Send
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</form>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
+22
-13
@@ -85,7 +85,8 @@ export const EmailAccounts = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleGmailOAuth = async () => {
|
const handleGmailOAuth = async (ev?: React.FormEvent) => {
|
||||||
|
ev?.preventDefault();
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
const status = await client.get<{ connected: boolean; email: string | null }>('/integrations/google/status');
|
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;
|
if (!form.email.trim() || !form.password.trim()) return;
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
@@ -304,7 +306,10 @@ export const EmailAccounts = () => {
|
|||||||
|
|
||||||
{/* Gmail OAuth form */}
|
{/* Gmail OAuth form */}
|
||||||
{activeForm === 'gmail-oauth' && (
|
{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>
|
<p className="text-sm font-medium text-duck-dark dark:text-foreground">Add Gmail (OAuth)</p>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -317,16 +322,19 @@ export const EmailAccounts = () => {
|
|||||||
<Button type="button" variant="ghost" onClick={resetForm} className="h-9 cursor-pointer">
|
<Button type="button" variant="ghost" onClick={resetForm} className="h-9 cursor-pointer">
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</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'}
|
{saving ? <Loader2 className="h-4 w-4 animate-spin" /> : 'Connect & Add'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Gmail App Password form */}
|
{/* Gmail App Password form */}
|
||||||
{activeForm === 'gmail-password' && (
|
{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>
|
<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">
|
<div className="text-xs text-duck-dark/50 dark:text-foreground/50 grid gap-1.5">
|
||||||
<p>To create an App Password:</p>
|
<p>To create an App Password:</p>
|
||||||
@@ -373,20 +381,22 @@ export const EmailAccounts = () => {
|
|||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="submit"
|
||||||
onClick={handlePasswordSubmit}
|
|
||||||
disabled={!form.email.trim() || !form.password.trim() || saving}
|
disabled={!form.email.trim() || !form.password.trim() || saving}
|
||||||
className="h-9 flex-1 cursor-pointer"
|
className="h-9 flex-1 cursor-pointer"
|
||||||
>
|
>
|
||||||
{saving ? <Loader2 className="h-4 w-4 animate-spin" /> : 'Test & Save'}
|
{saving ? <Loader2 className="h-4 w-4 animate-spin" /> : 'Test & Save'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Generic IMAP form */}
|
{/* Generic IMAP form */}
|
||||||
{activeForm === 'imap' && (
|
{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>
|
<p className="text-sm font-medium text-duck-dark dark:text-foreground">Add IMAP Account</p>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
@@ -439,15 +449,14 @@ export const EmailAccounts = () => {
|
|||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="submit"
|
||||||
onClick={handlePasswordSubmit}
|
|
||||||
disabled={!form.email.trim() || !form.password.trim() || !form.imapHost.trim() || saving}
|
disabled={!form.email.trim() || !form.password.trim() || !form.imapHost.trim() || saving}
|
||||||
className="h-9 flex-1 cursor-pointer"
|
className="h-9 flex-1 cursor-pointer"
|
||||||
>
|
>
|
||||||
{saving ? <Loader2 className="h-4 w-4 animate-spin" /> : 'Test & Save'}
|
{saving ? <Loader2 className="h-4 w-4 animate-spin" /> : 'Test & Save'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Add account button */}
|
{/* Add account button */}
|
||||||
|
|||||||
Reference in New Issue
Block a user