Description
Summary
When editing the quoted original inside a reply/forward (the QuotedHtml
"quote island" introduced in 1.7.3), global mailbox shortcuts still fire.
Pressing Delete or Backspace while redacting quoted text deletes the
currently open email from the mailbox instead of deleting text. Other
single-key shortcuts (s, e, r, f, c, j, k, …) are also
intercepted with preventDefault(), which makes typing inside the quote
appear broken — the keystrokes are swallowed as mailbox commands.
Reproduced on 1.7.7 (current main).
Root cause
components/email/quoted-html.ts renders the quote inside a Shadow Root
with an inner contentEditable div. As the file's own comment notes,
document.activeElement is retargeted to the host element inside a shadow
root. The NodeView tracks focus locally via focusin/focusout, but that
state is never exposed to the shortcut layer.
hooks/use-keyboard-shortcuts.ts → isInputFocused() only inspects
document.activeElement (tag name + contenteditable attribute). The
shadow host is a plain div, so the check returns false and the global
keydown handler treats the user as not typing. With an email open,
hasBatchTarget is true, so #/Delete/Backspace invoke onDelete().
Note that physicalShortcutKey() derives keys from event.code, so
non-Latin/regional layouts do not avoid the bug.
Fix
Replace the document.activeElement check with one based on
event.composedPath(), which sees through the shadow boundary:
function isTypingEvent(event: KeyboardEvent): boolean {
return event.composedPath().some((node) => {
if (!(node instanceof HTMLElement)) return false;
const tag = node.tagName.toLowerCase();
return (
tag === "input" ||
tag === "textarea" ||
tag === "select" ||
node.isContentEditable ||
node.getAttribute("contenteditable") === "true"
);
});
}
and in the keydown handler:
if (isTypingEvent(event)) return; // was: if (isInputFocused()) return;
Additionally I removed case "delete": / case "backspace": from the
delete action, keeping only the documented # shortcut. A single
unmodified key that irreversibly deletes mail (with permanent-delete
enabled) is dangerous defense-in-depth-wise even after the focus fix —
any future focus-detection regression would again cause silent data loss.
I understand this part is opinionated; the composedPath() fix alone
resolves the reported bug.
Steps to Reproduce
Steps to reproduce
- Open any received email.
- Press Forward.
- Click inside the quoted original (the QuotedHtml island).
- Press
s → the letter is not typed; the original email's star toggles.
- Press
Backspace → the original email is deleted from the mailbox.
With "Delete action: permanently delete" enabled, this is unrecoverable
data loss triggered by normal text editing.
Expected Behavior
While the cursor is inside the editable quoted original (the QuotedHtml
shadow-DOM island) in a reply/forward, keystrokes should behave as normal
text editing:
- Letter keys insert text.
Backspace/Delete remove the selected text or the character next to
the caret — and never touch the mailbox.
- Global mailbox shortcuts (
s, e, r, f, c, j, k, #, …)
are suppressed, exactly as they already are for regular inputs,
textareas, and the main TipTap editor body.
Actual Behavior
The global shortcut layer does not recognize the shadow-DOM
contentEditable as a typing context, so every single-key shortcut fires
with preventDefault():
- Letters are swallowed instead of typed (
s toggles the original
email's star, j/k move list selection, c opens compose, …), so
editing the quote appears broken.
Backspace/Delete invoke the mailbox delete action on the currently
open email. With "Delete action: permanently delete" enabled this is
unrecoverable data loss caused by ordinary text editing.
Affects all keyboard layouts, since physicalShortcutKey() maps from
event.code. Reproduced on 1.7.7 (current main).
Bulwark Version
1.7.7.
Stalwart Mail Server Version
0.16.13
Browser
Chrome / Chromium
Operating System
Windows
Screenshots / Screen Recording
No response
Relevant Logs or Error Output
Additional Context
No response
Description
Summary
When editing the quoted original inside a reply/forward (the
QuotedHtml"quote island" introduced in 1.7.3), global mailbox shortcuts still fire.
Pressing
DeleteorBackspacewhile redacting quoted text deletes thecurrently open email from the mailbox instead of deleting text. Other
single-key shortcuts (
s,e,r,f,c,j,k, …) are alsointercepted with
preventDefault(), which makes typing inside the quoteappear broken — the keystrokes are swallowed as mailbox commands.
Reproduced on 1.7.7 (current main).
Root cause
components/email/quoted-html.tsrenders the quote inside a Shadow Rootwith an inner
contentEditablediv. As the file's own comment notes,document.activeElementis retargeted to the host element inside a shadowroot. The NodeView tracks focus locally via
focusin/focusout, but thatstate is never exposed to the shortcut layer.
hooks/use-keyboard-shortcuts.ts→isInputFocused()only inspectsdocument.activeElement(tag name +contenteditableattribute). Theshadow host is a plain
div, so the check returnsfalseand the globalkeydownhandler treats the user as not typing. With an email open,hasBatchTargetis true, so#/Delete/BackspaceinvokeonDelete().Note that
physicalShortcutKey()derives keys fromevent.code, sonon-Latin/regional layouts do not avoid the bug.
Fix
Replace the
document.activeElementcheck with one based onevent.composedPath(), which sees through the shadow boundary:and in the
keydownhandler:Additionally I removed
case "delete":/case "backspace":from thedelete action, keeping only the documented
#shortcut. A singleunmodified key that irreversibly deletes mail (with permanent-delete
enabled) is dangerous defense-in-depth-wise even after the focus fix —
any future focus-detection regression would again cause silent data loss.
I understand this part is opinionated; the
composedPath()fix aloneresolves the reported bug.
Steps to Reproduce
Steps to reproduce
s→ the letter is not typed; the original email's star toggles.Backspace→ the original email is deleted from the mailbox.With "Delete action: permanently delete" enabled, this is unrecoverable
data loss triggered by normal text editing.
Expected Behavior
While the cursor is inside the editable quoted original (the QuotedHtml
shadow-DOM island) in a reply/forward, keystrokes should behave as normal
text editing:
Backspace/Deleteremove the selected text or the character next tothe caret — and never touch the mailbox.
s,e,r,f,c,j,k,#, …)are suppressed, exactly as they already are for regular inputs,
textareas, and the main TipTap editor body.
Actual Behavior
The global shortcut layer does not recognize the shadow-DOM
contentEditable as a typing context, so every single-key shortcut fires
with
preventDefault():stoggles the originalemail's star,
j/kmove list selection,copens compose, …), soediting the quote appears broken.
Backspace/Deleteinvoke the mailbox delete action on the currentlyopen email. With "Delete action: permanently delete" enabled this is
unrecoverable data loss caused by ordinary text editing.
Affects all keyboard layouts, since
physicalShortcutKey()maps fromevent.code. Reproduced on 1.7.7 (current main).Bulwark Version
1.7.7.
Stalwart Mail Server Version
0.16.13
Browser
Chrome / Chromium
Operating System
Windows
Screenshots / Screen Recording
No response
Relevant Logs or Error Output
Additional Context
No response