fix: keep Mountain View's row actions, checkbox and star on the subject line when attachment chips make the row taller

This commit is contained in:
Linus Rath
2026-10-28 19:53:33 +01:00
parent 720e146225
commit 6559b34135
3 changed files with 52 additions and 5 deletions
@@ -0,0 +1,39 @@
import { render, screen } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { Email } from '@/lib/jmap/types';
import { useSettingsStore } from '@/stores/settings-store';
import { EmailHoverActions } from '../email-hover-actions';
vi.mock('@/components/plugins/plugin-actions', () => ({
PluginActions: () => null,
usePluginActions: () => [],
useMessageActionTarget: () => ({ accountKind: 'personal', keywords: [] }),
}));
// Gmail keeps a row's actions on its first line when attachment chips make the
// row taller; ours sat centred between the subject and the chips.
const email = { id: 'e1', keywords: { $seen: true }, mailboxIds: { inbox: true } } as unknown as Email;
function toolbar(pinToFirstLine?: boolean) {
render(<EmailHoverActions email={email} onDelete={vi.fn()} pinToFirstLine={pinToFirstLine} />);
return screen.getByTestId('hover-action-delete').closest('.absolute') as HTMLElement;
}
beforeEach(() => {
useSettingsStore.setState({ hoverActions: ['delete'], interfaceLayout: 'mountain-view', hoverActionsMode: 'inline' } as never);
});
describe('row quick action placement', () => {
it('stays on the first line of a Mountain View row', () => {
const el = toolbar(true);
expect(el.className).toContain('h-10');
expect(el.className).not.toContain('bottom-0');
});
it('spans the whole row otherwise', () => {
const el = toolbar();
expect(el.className).toContain('bottom-0');
expect(el.className).not.toContain('h-10');
});
});
+7 -1
View File
@@ -32,6 +32,11 @@ interface EmailHoverActionsProps {
spamApplicable?: boolean;
/** Role of the folder the row is listed in; plugin buttons can be limited to some. */
mailboxRole?: string | null;
/**
* Keep the actions on the row's first 40px line rather than centred on the
* whole row, as Gmail does when attachment chips make the row taller.
*/
pinToFirstLine?: boolean;
}
const ACTION_CONFIG: Record<HoverAction, {
@@ -111,6 +116,7 @@ export function EmailHoverActions({
onUndoSpam,
spamApplicable = true,
mailboxRole = null,
pinToFirstLine = false,
}: EmailHoverActionsProps) {
const tagScope = useEmailTagScope(email);
const configuredHoverActions = useSettingsStore((state) => state.hoverActions);
@@ -337,7 +343,7 @@ export function EmailHoverActions({
return (
<div
className={cn("absolute end-0 top-0 bottom-0 z-10 items-center", visibilityClass)}
className={cn("absolute end-0 top-0 z-10 items-center", pinToFirstLine ? "h-10" : "bottom-0", visibilityClass)}
>
<div
className={cn(
+6 -4
View File
@@ -115,7 +115,7 @@ function RowChips({ email, loadAttachments, onOpenAttachment, mountainView, mvRo
const code = useListVerificationCode(email);
const leading = code ? <VerificationCodeChip code={code} variant={variant} /> : undefined;
// The sender column is w-32 (lg:w-40) and the row line has gap-3 after it.
const className = cn(variant === "mountain-view" ? "mt-2" : "mt-1.5", mvRow && "mt-1 pb-2.5 ps-[8.75rem] lg:ps-[10.75rem]");
const className = cn(variant === "mountain-view" ? "mt-2" : "mt-1.5", mvRow && "-mt-0.5 pb-2.5 ps-[8.75rem] lg:ps-[10.75rem]");
// Most rows have no attachment: no loader for them, nor an empty chip row.
const mayHaveAttachments = email.hasAttachment || (email.attachments?.length ?? 0) > 0;
if (onOpenAttachment && mayHaveAttachments) {
@@ -155,7 +155,7 @@ function MvRowLead({
starLabel: string;
}) {
return (
<div className="flex items-center gap-0.5 flex-shrink-0 self-center">
<div className="flex h-10 items-center gap-0.5 flex-shrink-0 self-start">
<button
type="button"
role="checkbox"
@@ -664,7 +664,7 @@ const SingleEmailItem = React.forwardRef<HTMLDivElement, SingleEmailItemProps>(
<div className="flex-1 min-w-0">
{oneLineRow ? (
<>
<div className="flex items-center justify-between gap-3">
<div className={cn("flex items-center justify-between gap-3", mvRow && "min-h-10")}>
<div className="flex min-w-0 flex-1 items-center gap-3">
{/* In columns the dot sits inside the From cell, so a row
with it starts its columns where the header does. */}
@@ -904,6 +904,7 @@ const SingleEmailItem = React.forwardRef<HTMLDivElement, SingleEmailItemProps>(
isInJunk={currentMailboxRole === 'junk'}
spamApplicable={!['sent', 'drafts', 'scheduled'].includes(currentMailboxRole || '')}
mailboxRole={currentMailboxRole ?? null}
pinToFirstLine={mvRow}
/>
)}
</div>
@@ -1339,7 +1340,7 @@ export const ThreadListItem = React.forwardRef<HTMLDivElement, ThreadListItemPro
<div className="flex-1 min-w-0">
{oneLineRow ? (
<>
<div className="flex items-center justify-between gap-3">
<div className={cn("flex items-center justify-between gap-3", mvRow && "min-h-10")}>
<div className="flex min-w-0 flex-1 items-center gap-3">
{!columns && isUnifiedView && latestEmail.accountId && threadAccountColor && showAccountDot && (
<span
@@ -1632,6 +1633,7 @@ export const ThreadListItem = React.forwardRef<HTMLDivElement, ThreadListItemPro
isInJunk={currentMailboxRole === 'junk'}
spamApplicable={!['sent', 'drafts', 'scheduled'].includes(currentMailboxRole || '')}
mailboxRole={currentMailboxRole ?? null}
pinToFirstLine={mvRow}
/>
)}
</div>