mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-05 08:16:22 +00:00
feat: add Ctrl+K for a link, Ctrl+S to save the draft and Ctrl+Shift+C or B for Cc and Bcc in the composer
This commit is contained in:
@@ -411,6 +411,24 @@ describe('composer close guard with a follow-up', () => {
|
||||
expect(onDiscardDraft).toHaveBeenCalledWith('draft-9', expect.anything());
|
||||
});
|
||||
|
||||
// compose-19: the keys people bring from other mail apps.
|
||||
it('saves the draft on Ctrl+S, and keeps the browser from saving the page', async () => {
|
||||
const createDraft = savingClient();
|
||||
renderWithCloseRef();
|
||||
const subject = screen.getByDisplayValue('Half-written more');
|
||||
expect(fireEvent.keyDown(subject, { key: 's', ctrlKey: true })).toBe(false);
|
||||
await waitFor(() => expect(createDraft).toHaveBeenCalledTimes(1));
|
||||
});
|
||||
|
||||
it('shows Cc on Ctrl+Shift+C and Bcc on Ctrl+Shift+B, and puts the cursor there', async () => {
|
||||
renderWithCloseRef();
|
||||
const subject = screen.getByDisplayValue('Half-written more');
|
||||
expect(fireEvent.keyDown(subject, { key: 'C', ctrlKey: true, shiftKey: true })).toBe(false);
|
||||
await waitFor(() => expect(document.activeElement).toBe(screen.getByRole('combobox', { name: 'cc_label' })));
|
||||
expect(fireEvent.keyDown(subject, { key: 'B', ctrlKey: true, shiftKey: true })).toBe(false);
|
||||
await waitFor(() => expect(document.activeElement).toBe(screen.getByRole('combobox', { name: 'bcc_label' })));
|
||||
});
|
||||
|
||||
it('closes straight through when there is nothing to lose', async () => {
|
||||
draftToast.showDraftSavedToast.mockClear();
|
||||
const requestCloseRef = { current: null } as React.MutableRefObject<((afterClose?: () => void) => void) | null>;
|
||||
|
||||
@@ -36,6 +36,17 @@ describe('Insert link', () => {
|
||||
expect(editor.getHTML()).toContain('href="https://example.com/docs"');
|
||||
});
|
||||
|
||||
it('opens on Ctrl+K in the text, as in Gmail, Outlook and Word', async () => {
|
||||
const editor = await setup('<p>docs here</p>');
|
||||
act(() => { editor.commands.setTextSelection({ from: 1, to: 5 }); });
|
||||
|
||||
const event = new KeyboardEvent('keydown', { key: 'k', ctrlKey: true, bubbles: true, cancelable: true });
|
||||
act(() => { editor.view.dom.dispatchEvent(event); });
|
||||
|
||||
expect(event.defaultPrevented).toBe(true);
|
||||
expect(screen.getByRole('dialog', { name: 'insert_link' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('removes a link from the dialog', async () => {
|
||||
const editor = await setup('<p><a href="https://example.com">docs</a> here</p>');
|
||||
act(() => { editor.commands.setTextSelection(2); });
|
||||
|
||||
@@ -4805,6 +4805,25 @@ export function EmailComposer({
|
||||
const isSendShortcut = e.key === 'Enter' && hasPrimaryModifier && !e.altKey && !e.shiftKey;
|
||||
const isScheduleShortcut = e.key === 'Enter' && hasPrimaryModifier && !e.altKey && e.shiftKey;
|
||||
if (!isPlainEscape && !isSendShortcut && !isScheduleShortcut) {
|
||||
// Ctrl+S saves the draft (Outlook, Thunderbird, Apple Mail), not the
|
||||
// page; Ctrl+Shift+C and Ctrl+Shift+B show Cc and Bcc (Gmail).
|
||||
const key = e.key.toLowerCase();
|
||||
if (hasPrimaryModifier && !e.altKey && !e.shiftKey && key === 's') {
|
||||
e.preventDefault();
|
||||
if (!isSavingAndClosing) void saveDraft({ announce: true });
|
||||
return;
|
||||
}
|
||||
if (hasPrimaryModifier && !e.altKey && e.shiftKey && (key === 'c' || key === 'b')) {
|
||||
e.preventDefault();
|
||||
if (key === 'c') {
|
||||
setShowCc(true);
|
||||
setTimeout(() => ccInputRef.current?.focus(), 0);
|
||||
} else {
|
||||
setShowBcc(true);
|
||||
setTimeout(() => bccInputRef.current?.focus(), 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// A plugin's composer button with a shortcut (composer-toolbar-actions)
|
||||
// works while the composer has focus, also while typing.
|
||||
if (!e.altKey && !hasPrimaryModifier) return;
|
||||
|
||||
@@ -496,6 +496,8 @@ export function RichTextEditor({
|
||||
// Images waiting for a description, one dialog at a time: a new image is
|
||||
// found by its src (it has no alt yet), an existing one by its position.
|
||||
const [altQueue, setAltQueue] = useState<AltRequest[]>([]);
|
||||
// Insert link, for the key handler set up with the editor.
|
||||
const addLinkRef = useRef<(() => void) | null>(null);
|
||||
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
@@ -598,6 +600,12 @@ export function RichTextEditor({
|
||||
event.preventDefault();
|
||||
return true;
|
||||
}
|
||||
// Ctrl+K inserts a link, as in Gmail, Outlook and Word.
|
||||
if ((event.ctrlKey || event.metaKey) && !event.shiftKey && !event.altKey && event.key.toLowerCase() === "k" && addLinkRef.current) {
|
||||
event.preventDefault();
|
||||
addLinkRef.current();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
handleDrop: (view, event) => {
|
||||
@@ -727,6 +735,7 @@ export function RichTextEditor({
|
||||
const { from, to } = editor.state.selection;
|
||||
setLinkDialog({ href: previousUrl ?? "", from, to });
|
||||
}, [editor]);
|
||||
addLinkRef.current = addLink;
|
||||
const applyLink = useCallback((url: string) => {
|
||||
if (!editor || !linkDialog) return;
|
||||
editor
|
||||
|
||||
@@ -38,7 +38,11 @@ export const MAIL_SHORTCUTS = {
|
||||
composer: [
|
||||
{ key: "Mod + Enter", description: "shortcuts.composer.send" },
|
||||
{ key: "Mod + Shift + Enter", description: "shortcuts.composer.schedule_send" },
|
||||
{ key: "t", description: "shortcuts.composer.template_picker" },
|
||||
// "t" opens templates only outside a text field, so it isn't listed: in the composer it types a t.
|
||||
{ key: "Mod + S", description: "compose_ux.shortcut_save_draft" },
|
||||
{ key: "Mod + K", description: "compose_ux.shortcut_insert_link" },
|
||||
{ key: "Mod + Shift + C", description: "compose_ux.shortcut_show_cc" },
|
||||
{ key: "Mod + Shift + B", description: "compose_ux.shortcut_show_bcc" },
|
||||
// Proofreading (components/email/proofreading); shown even without a checker.
|
||||
{ key: "Mod + '", description: "proofreading.shortcuts.next_issue" },
|
||||
{ key: "Mod + ;", description: "proofreading.shortcuts.previous_issue" },
|
||||
|
||||
Reference in New Issue
Block a user