fix: drop duplicate selection checkbox in message list

This commit is contained in:
Linus Rath
2026-08-29 17:18:49 +02:00
parent 1807b2df6b
commit 40cbe82848
3 changed files with 18 additions and 15 deletions
@@ -210,7 +210,7 @@ describe('ThreadListItem row content', () => {
});
});
describe('ThreadListItem shift-range checkbox', () => {
describe('ThreadListItem shift-range avatar selection', () => {
beforeEach(() => {
useSettingsStore.setState({
emailKeywords: [...DEFAULT_KEYWORDS],
@@ -219,11 +219,11 @@ describe('ThreadListItem shift-range checkbox', () => {
});
});
it('shift-clicking the checkbox extends the selection from the anchor', () => {
it('shift-clicking the avatar extends the selection from the anchor', () => {
const e1 = makeEmail({ id: 'e1', threadId: 't1' });
const e2 = makeEmail({ id: 'e2', threadId: 't2' });
const e3 = makeEmail({ id: 'e3', threadId: 't3' });
// Selection mode active so the checkbox renders, with the anchor on e1.
// Selection mode active, with the anchor on e1.
useEmailStore.setState({
emails: [e1, e2, e3],
selectedEmailIds: new Set(['e1']),
@@ -232,9 +232,9 @@ describe('ThreadListItem shift-range checkbox', () => {
});
renderRow(e3);
const checkbox = screen.getAllByRole('button')[0];
const avatar = screen.getByRole('checkbox');
act(() => {
checkbox.dispatchEvent(new MouseEvent('click', { bubbles: true, shiftKey: true }));
avatar.dispatchEvent(new MouseEvent('click', { bubbles: true, shiftKey: true }));
});
const selected = useEmailStore.getState().selectedEmailIds;
+7 -4
View File
@@ -1,6 +1,6 @@
"use client";
import type { ComponentProps } from "react";
import type { ComponentProps, MouseEvent } from "react";
import { Check } from "lucide-react";
import { Avatar } from "@/components/ui/avatar";
import { cn } from "@/lib/utils";
@@ -8,8 +8,11 @@ import { cn } from "@/lib/utils";
type SelectableAvatarProps = ComponentProps<typeof Avatar> & {
/** Whether the underlying message/thread is currently selected. */
checked: boolean;
/** Toggle selection. The wrapper stops propagation so the row is not opened. */
onToggle: () => void;
/**
* Toggle selection. The wrapper stops propagation so the row is not opened.
* Receives the click event so callers can honour shift-click range selection.
*/
onToggle: (e: MouseEvent<HTMLButtonElement>) => void;
/** Accessible label for the selection control. */
selectLabel?: string;
};
@@ -35,7 +38,7 @@ export function SelectableAvatar({
aria-label={selectLabel}
onClick={(e) => {
e.stopPropagation();
onToggle();
onToggle(e);
}}
className={cn(
"group/select relative shrink-0 rounded-full outline-none",
+6 -6
View File
@@ -294,8 +294,8 @@ const SingleEmailItem = React.forwardRef<HTMLDivElement, SingleEmailItemProps>(
className={cn('px-3', isFocusedMailLayout ? 'flex items-center' : 'flex items-start', swipeEnabled && 'relative z-10 bg-inherit')}
style={{ gap: 'var(--density-item-gap)', paddingBlock: 'var(--density-item-py)', transform: swipeEnabled && offsetX ? `translateX(${offsetX}px)` : undefined, transition: swipeEnabled && offsetX === 0 ? 'transform 200ms ease-out' : undefined }}
>
{/* Checkbox - only visible when in selection mode */}
{selectedEmailIds.size > 0 && (
{/* Checkbox - only for extra-compact density (no avatar) while in selection mode */}
{density === 'extra-compact' && selectedEmailIds.size > 0 && (
<button
onClick={handleCheckboxClick}
className={cn(
@@ -329,7 +329,7 @@ const SingleEmailItem = React.forwardRef<HTMLDivElement, SingleEmailItemProps>(
className="flex-shrink-0 shadow-sm"
disableImages={hideJunkAvatarImages}
checked={isChecked}
onToggle={() => toggleEmailSelection(email.id)}
onToggle={handleCheckboxClick}
selectLabel={tBatch('select')}
/>
)}
@@ -733,8 +733,8 @@ export const ThreadListItem = React.forwardRef<HTMLDivElement, ThreadListItemPro
className={cn('px-3', isFocusedMailLayout ? 'flex items-center' : 'flex items-start')}
style={{ gap: 'var(--density-item-gap)', paddingBlock: 'var(--density-item-py)' }}
>
{/* Checkbox for thread selection - only visible when in selection mode */}
{selectedEmailIds.size > 0 && (
{/* Checkbox for thread selection - only for extra-compact density (no avatar) while in selection mode */}
{density === 'extra-compact' && selectedEmailIds.size > 0 && (
<button
onClick={handleThreadCheckboxClick}
className={cn(
@@ -769,7 +769,7 @@ export const ThreadListItem = React.forwardRef<HTMLDivElement, ThreadListItemPro
className="shadow-sm"
disableImages={hideJunkAvatarImages}
checked={isChecked}
onToggle={toggleThreadSelection}
onToggle={handleThreadCheckboxClick}
selectLabel={tBatch('select')}
/>
{!isMobile && (