diff --git a/components/email/__tests__/composer-close-guard.test.tsx b/components/email/__tests__/composer-close-guard.test.tsx index 3eab0edba..b1524987e 100644 --- a/components/email/__tests__/composer-close-guard.test.tsx +++ b/components/email/__tests__/composer-close-guard.test.tsx @@ -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>; diff --git a/components/email/__tests__/rich-text-editor-link-dialog.test.tsx b/components/email/__tests__/rich-text-editor-link-dialog.test.tsx index e815efa53..c15a51ce4 100644 --- a/components/email/__tests__/rich-text-editor-link-dialog.test.tsx +++ b/components/email/__tests__/rich-text-editor-link-dialog.test.tsx @@ -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('
docs here
'); + 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('docs here
'); act(() => { editor.commands.setTextSelection(2); }); diff --git a/components/email/email-composer.tsx b/components/email/email-composer.tsx index 0f0da05c1..05c4fb78c 100644 --- a/components/email/email-composer.tsx +++ b/components/email/email-composer.tsx @@ -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; diff --git a/components/email/rich-text-editor.tsx b/components/email/rich-text-editor.tsx index 532291294..a18d8252e 100644 --- a/components/email/rich-text-editor.tsx +++ b/components/email/rich-text-editor.tsx @@ -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