fix: show one preview line in regular list rows, two in comfortable ones

The skeleton test counted two preview lines per regular row; regular rows now draw one, so it expects one there and two for comfortable rows.
This commit is contained in:
Linus Rath
2026-11-04 06:45:10 +01:00
parent 295eb35671
commit 1b4cecd2a5
6 changed files with 99 additions and 7 deletions
+2
View File
@@ -362,6 +362,8 @@
@custom-variant boot-dense (&:where([data-boot-density="compact"] *, [data-boot-density="extra-compact"] *));
/* Message previews turned off (showPreview). */
@custom-variant boot-no-preview (&:where([data-boot-preview="off"] *));
/* The preview is one line except in comfortable rows. */
@custom-variant boot-one-preview-line (&:where(:root:not([data-boot-density="comfortable"]) *));
/* The default interface layout, the opposite of mv: (both can't be told apart by order). */
@custom-variant boot-std (&:where(:root:not([data-interface-layout="mountain-view"]) *));
/* Mountain View's side panel shows only with calendar or contacts. */
@@ -30,14 +30,16 @@ describe('mail skeleton', () => {
expect(pane.querySelector('.lg\\:mv\\:hidden.border-b')).toBeNull();
});
// Regular rows have a one-line preview, comfortable ones two (mail-15).
it.each([
['extra-compact', 0, 0],
['compact', 16, 0],
['regular', 16, 16],
] as const)('draws %s stacked rows with %i avatars and %i previews', (density, avatars, previews) => {
['comfortable', 16, 32],
] as const)('draws %s stacked rows with %i avatars and %i preview lines', (density, avatars, previewLines) => {
const rows = html(<MailListRowsSkeleton shape="stacked" density={density} preview />);
expect(rows.querySelectorAll('.w-10.h-10').length).toBe(avatars);
expect(rows.querySelectorAll('.leading-relaxed').length).toBe(previews * 2);
expect(rows.querySelectorAll('.leading-relaxed').length).toBe(previewLines);
});
it('leaves the preview out when previews are turned off', () => {
@@ -49,6 +51,7 @@ describe('mail skeleton', () => {
const rows = html(<MailListRowsSkeleton />);
expect(rows.querySelector('.boot-density-xc\\:hidden')).not.toBeNull();
expect(rows.querySelector('.boot-dense\\:hidden')).not.toBeNull();
expect(rows.querySelector('.boot-one-preview-line\\:hidden')).not.toBeNull();
expect(rows.querySelector('.md\\:mv\\:block')).not.toBeNull();
});
});
+10 -1
View File
@@ -181,6 +181,12 @@ function previewClass(o: RowOptions): string | null {
return o.preview === false || o.density === "compact" || o.density === "extra-compact" ? null : "";
}
/** Classes for the preview's second line, which only comfortable rows have (mail-15). */
function secondPreviewLineClass(o: RowOptions): string | null {
if (o.density === undefined) return "boot-one-preview-line:hidden";
return o.density === "comfortable" ? "" : null;
}
/*
* The rows below are drawn dozens of times, so they use the short classes in
* app/boot-skeleton.css (mail section) for what every row repeats: sk-mrow
@@ -202,6 +208,7 @@ function L({ className, w }: { className?: string; w: string }) {
function StackedRow({ i, o }: { i: number; o: RowOptions }) {
const avatar = avatarClass(o);
const preview = previewClass(o);
const secondLine = secondPreviewLineClass(o);
return (
<div className="sk-mrow max-md:mv:border-b-transparent">
<div className="sk-mcell sk-mcell-min">
@@ -220,7 +227,9 @@ function StackedRow({ i, o }: { i: number; o: RowOptions }) {
<span className={cn("block", preview)}>
{/* Mountain View's phone rows keep the preview to one line, beside a 32px star. */}
<L className="text-sm leading-relaxed max-md:mv:h-8" w="95%" />
<L className="text-sm leading-relaxed max-md:mv:hidden" w={textWidth(i + 5, 45, 40)} />
{secondLine !== null && (
<L className={cn("text-sm leading-relaxed max-md:mv:hidden", secondLine)} w={textWidth(i + 5, 45, 40)} />
)}
</span>
)}
</span>
@@ -0,0 +1,77 @@
import { render, screen } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ThreadListItem } from '../thread-list-item';
import { useSettingsStore, DEFAULT_KEYWORDS } from '@/stores/settings-store';
import { useEmailStore } from '@/stores/email-store';
import { useUIStore } from '@/stores/ui-store';
import { groupEmailsByThread } from '@/lib/thread-utils';
import type { Email } from '@/lib/jmap/types';
vi.mock('@/hooks/use-email-drag', () => ({
useEmailDrag: () => ({ dragHandlers: {}, isDragging: false }),
}));
vi.mock('@/stores/auth-store', () => {
const state = { identities: [], username: null, client: null, activeAccountId: null, getClientForAccount: () => undefined };
const useAuthStore = Object.assign(
(selector?: (s: typeof state) => unknown) => (selector ? selector(state) : state),
{ getState: () => state },
);
return { useAuthStore };
});
const PREVIEW = 'Hi Robin, thanks for the quick reply. I went through the points again.';
const makeEmail = (overrides: Partial<Email> = {}): Email => ({
id: 'email-1',
threadId: 'thread-1',
mailboxIds: { inbox: true },
keywords: { $seen: true },
size: 1000,
receivedAt: '2024-01-15T10:00:00Z',
from: [{ name: 'Alice', email: 'alice@example.com' }],
subject: 'Test Subject',
preview: PREVIEW,
hasAttachment: false,
...overrides,
});
function renderRow(emails: Email[]) {
const [thread] = groupEmailsByThread(emails);
return render(
<ThreadListItem thread={thread} isExpanded={false} onToggleExpand={() => {}} onEmailSelect={() => {}} />,
);
}
beforeEach(() => {
useUIStore.setState({ isMobile: false, isTablet: false, isDesktop: true });
useSettingsStore.setState({
emailKeywords: [...DEFAULT_KEYWORDS],
showPreview: true,
mailLayout: 'split',
interfaceLayout: 'default',
});
useEmailStore.setState({ selectedEmailKeys: new Set<string>(), selectedMailbox: 'inbox' });
});
// mail-15: the default density shows one preview line, so more rows fit;
// comfortable keeps two.
describe('list row preview lines', () => {
it.each([
['regular', 'line-clamp-1'],
['comfortable', 'line-clamp-2'],
] as const)('a %s row clamps its preview with %s', (density, clamp) => {
useSettingsStore.setState({ density });
renderRow([makeEmail()]);
expect(screen.getByText(PREVIEW).className.split(/\s+/)).toContain(clamp);
});
it.each([
['regular', 'line-clamp-1'],
['comfortable', 'line-clamp-2'],
] as const)('a %s conversation row clamps its preview with %s', (density, clamp) => {
useSettingsStore.setState({ density });
renderRow([makeEmail({ id: 'a' }), makeEmail({ id: 'b', receivedAt: '2024-01-16T10:00:00Z' })]);
expect(screen.getByText(PREVIEW).className.split(/\s+/)).toContain(clamp);
});
});
+3 -2
View File
@@ -463,9 +463,10 @@ export function EmailList({
let size = { 'extra-compact': 32, compact: 60, regular: 84, comfortable: 104 }[density];
if (showPreview && density !== 'extra-compact') {
// A mail without a preview draws a one-line "No preview available",
// a line (23px) shorter than a real one.
// a line (23px) shorter than a real one. Only comfortable rows show a
// second preview line (mail-15).
const emptyPreview = !!latest && !cleanPreview(latest.preview) && !latest.searchSnippet?.preview;
size += emptyPreview ? 36 - 23 : 36;
size += emptyPreview || density !== 'comfortable' ? 36 - 23 : 36;
}
// The chip row (attachments, verification code): a 22px chip plus 6px margin.
const hasCode = !!latest && showVerificationCodes && !!listVerificationCode(latest);
+2 -2
View File
@@ -975,7 +975,7 @@ const SingleEmailItem = React.forwardRef<HTMLDivElement, SingleEmailItemProps>(
<div className={cn("flex min-w-0 items-center gap-1", !mvMobileRow && "contents")}>
<p dir="auto" className={cn(
"text-sm leading-relaxed",
mvMobileRow ? "line-clamp-1 min-w-0 flex-1" : "line-clamp-2",
mvMobileRow ? "line-clamp-1 min-w-0 flex-1" : density === 'comfortable' ? "line-clamp-2" : "line-clamp-1",
isUnread
? "text-muted-foreground"
: "text-muted-foreground"
@@ -1754,7 +1754,7 @@ export const ThreadListItem = React.forwardRef<HTMLDivElement, ThreadListItemPro
<div className={cn("flex min-w-0 items-center gap-1", !mvMobileRow && "contents")}>
<p dir="auto" className={cn(
"text-sm leading-relaxed",
mvMobileRow ? "line-clamp-1 min-w-0 flex-1" : "line-clamp-2",
mvMobileRow ? "line-clamp-1 min-w-0 flex-1" : density === 'comfortable' ? "line-clamp-2" : "line-clamp-1",
hasUnread
? "text-muted-foreground"
: "text-muted-foreground"