diff --git a/components/email/__tests__/viewer-reading.test.tsx b/components/email/__tests__/viewer-reading.test.tsx index 6beb7d355..bc836a301 100644 --- a/components/email/__tests__/viewer-reading.test.tsx +++ b/components/email/__tests__/viewer-reading.test.tsx @@ -274,3 +274,35 @@ describe('pausing animated images', () => { expect(await frameKey()).not.toContain('/p'); }); }); + +describe('reader toolbar actions', () => { + async function openMenu() { + await act(async () => { screen.getByRole('button', { name: 'more_actions' }).click(); }); + } + const item = (name: string) => screen.getAllByRole('menuitem').find((el) => el.textContent?.includes(name)); + + it('switches one HTML message to plain text and back', async () => { + const { container } = await show(message({ html: '
Fancy HTML version
', text: 'The plain version' })); + await openMenu(); + await act(async () => { item('show_as_plain_text')!.click(); }); + expect(container.querySelector('iframe')).toBeNull(); + expect(screen.getByText('The plain version')).toBeInTheDocument(); + await openMenu(); + expect(item('show_formatted')).toBeTruthy(); + }); + + it('opens the reader view as a dialog that Escape closes, with focus back', async () => { + await show(message({ html: 'Read this in peace.
' })); + const more = screen.getByRole('button', { name: 'more_actions' }); + await openMenu(); + await act(async () => { item('reader_view')!.click(); }); + const dialog = screen.getByRole('dialog', { name: 'Bonjour' }); + expect(dialog).toHaveAttribute('aria-modal', 'true'); + expect(screen.getByTestId('reader-view-body').textContent).toContain('Read this (https://ex.com/r) in peace.'); + expect(dialog.contains(document.activeElement)).toBe(true); + await act(async () => { document.activeElement!.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })); }); + expect(screen.queryByRole('dialog', { name: 'Bonjour' })).toBeNull(); + await act(async () => { await new Promise((r) => setTimeout(r, 0)); }); + expect(document.activeElement === more || more.contains(document.activeElement)).toBe(true); + }); +}); diff --git a/components/email/email-viewer.tsx b/components/email/email-viewer.tsx index f33bf24a5..dcf28eeed 100644 --- a/components/email/email-viewer.tsx +++ b/components/email/email-viewer.tsx @@ -13,6 +13,7 @@ import { applyMailTextSize, mailFitOptions } from "@/lib/mail-reading"; import { useMailReading } from "@/hooks/use-mail-reading"; import { useReadAloud } from "@/hooks/use-read-aloud"; import { ReadAloudBar } from "./read-aloud-bar"; +import { ReaderViewDialog } from "./reader-view-dialog"; import { collectReferencedCids, isEmbeddedInBody, normalizeCid, rewriteCidReferences } from "@/lib/attachment-visibility"; import { collapsePlainTextQuotes, setupQuoteCollapse } from "@/lib/quote-collapse"; import { DARK_PENDING_CLASS, applyEmailDarkMode, emailColorScheme, emailDarkModeCss, emailHasNativeDarkMode as htmlHasNativeDarkMode, resolveColorSchemeMedia } from "@/lib/email-dark-mode"; @@ -108,6 +109,7 @@ import { Minimize2, Type, Volume2, + BookOpen, ArrowLeft, Pencil, } from "@/components/icons"; @@ -2224,6 +2226,7 @@ export function EmailViewer({ setShowSourceModal(false); setEmailViewDarkOverride(null); setPlainViewOverride(null); + setReaderViewOpen(false); setPluginRenderedHtml(null); setPluginRenderedText(null); setPluginRenderedAttachments(NO_POSTAL_ATTACHMENTS); @@ -2860,8 +2863,8 @@ export function EmailViewer({ const [plainViewOverride, setPlainViewOverride] = useState