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) => (
|
||||
<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
@@ -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 */}
|
||||
|
||||
Reference in New Issue
Block a user