mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 00:12:39 +00:00
fix: keep the reading-mode toggle mounted so toolbar buttons stop jumping #964
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import type { Email, Mailbox } from '@/lib/jmap/types';
|
||||
|
||||
// #964: the reading-mode (Sun/Moon) toggle used to mount only once the body
|
||||
// had loaded and turned out to be HTML. That late mount changed the toolbar
|
||||
// width and re-ran the overflow calculation, so the other buttons jumped.
|
||||
// The toggle must be in the DOM from the start, merely disabled.
|
||||
|
||||
vi.mock('@/hooks/use-media-query', () => ({
|
||||
useDeviceDetection: () => ({ isMobile: false, isTablet: false, isDesktop: true }),
|
||||
useMediaQuery: () => false,
|
||||
}));
|
||||
|
||||
vi.mock('@/components/tour/tour-provider', () => ({
|
||||
useTour: () => ({ activeTour: null, startTour: vi.fn(), endTour: vi.fn(), registerStep: vi.fn() }),
|
||||
}));
|
||||
|
||||
vi.mock('@/components/plugins/plugin-slot', () => ({
|
||||
PluginSlot: () => null,
|
||||
}));
|
||||
|
||||
vi.mock('@/hooks/use-plugin-slot-offers', () => ({
|
||||
usePluginSlotOffers: () => [],
|
||||
}));
|
||||
|
||||
class NoopResizeObserver {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
}
|
||||
vi.stubGlobal('ResizeObserver', NoopResizeObserver);
|
||||
|
||||
const { EmailViewer } = await import('../email-viewer');
|
||||
|
||||
const mailboxes: Mailbox[] = [
|
||||
{ id: 'inbox', name: 'Inbox', role: 'inbox', parentId: null, myRights: { mayAddItems: true } },
|
||||
] as unknown as Mailbox[];
|
||||
|
||||
function makeEmail(overrides: Partial<Email> = {}): Email {
|
||||
return {
|
||||
id: 'e1',
|
||||
blobId: 'b1',
|
||||
threadId: 't1',
|
||||
mailboxIds: { inbox: true },
|
||||
keywords: { $seen: true },
|
||||
from: [{ name: 'Alice', email: 'alice@example.com' }],
|
||||
to: [{ name: 'Bob', email: 'bob@example.com' }],
|
||||
subject: 'Hello',
|
||||
receivedAt: '2026-01-01T00:00:00Z',
|
||||
preview: 'Hello',
|
||||
hasAttachment: false,
|
||||
size: 100,
|
||||
textBody: [],
|
||||
htmlBody: [],
|
||||
attachments: [],
|
||||
bodyValues: {},
|
||||
...overrides,
|
||||
} as unknown as Email;
|
||||
}
|
||||
|
||||
function findToggle(): HTMLElement | null {
|
||||
return (
|
||||
screen.queryByTitle('View in dark mode') ?? screen.queryByTitle('View in light mode')
|
||||
);
|
||||
}
|
||||
|
||||
describe('reading-mode toggle stays mounted (#964)', () => {
|
||||
it('is in the DOM while the body is still loading, disabled', () => {
|
||||
render(
|
||||
<EmailViewer
|
||||
email={makeEmail({ htmlBody: [{ partId: '1', type: 'text/html' }] } as never)}
|
||||
isLoading
|
||||
mailboxes={mailboxes}
|
||||
selectedMailbox="inbox"
|
||||
currentMailboxRole="inbox"
|
||||
/>,
|
||||
);
|
||||
|
||||
const toggle = findToggle();
|
||||
expect(toggle).not.toBeNull();
|
||||
expect(toggle).toBeDisabled();
|
||||
});
|
||||
|
||||
it('is enabled once the HTML body is available', () => {
|
||||
render(
|
||||
<EmailViewer
|
||||
email={makeEmail({
|
||||
htmlBody: [{ partId: '1', type: 'text/html' }],
|
||||
bodyValues: { '1': { value: '<p>Hi</p>', isTruncated: false } },
|
||||
} as never)}
|
||||
mailboxes={mailboxes}
|
||||
selectedMailbox="inbox"
|
||||
currentMailboxRole="inbox"
|
||||
/>,
|
||||
);
|
||||
|
||||
const toggle = findToggle();
|
||||
expect(toggle).not.toBeNull();
|
||||
expect(toggle).not.toBeDisabled();
|
||||
});
|
||||
});
|
||||
@@ -3218,8 +3218,10 @@ export function EmailViewer({
|
||||
<Code className="w-4 h-4" />
|
||||
</Button>
|
||||
|
||||
{/* Dark/light mode toggle for HTML emails */}
|
||||
{effectiveEmailContent.isHtml && (
|
||||
{/* Dark/light mode toggle for HTML emails. Always mounted: `isHtml`
|
||||
flips from false to true once the body arrives, and mounting the
|
||||
button then changes the toolbar width and re-runs the overflow
|
||||
calculation, so the other buttons jump. Disable it instead. (#964) */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@@ -3228,10 +3230,11 @@ export function EmailViewer({
|
||||
data-overflow-priority="11"
|
||||
className="hidden sm:inline-flex h-8 gap-1.5"
|
||||
title={isDark ? 'View in light mode' : 'View in dark mode'}
|
||||
disabled={!effectiveEmailContent.isHtml}
|
||||
aria-disabled={!effectiveEmailContent.isHtml}
|
||||
>
|
||||
{isDark ? <Sun className="w-4 h-4" /> : <Moon className="w-4 h-4" />}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Fullscreen toggle - hidden on mobile (already fullscreen there).
|
||||
Never overflows into the More menu: in fullscreen this button is
|
||||
|
||||
Reference in New Issue
Block a user