diff --git a/components/email/__tests__/thread-list-item.test.tsx b/components/email/__tests__/thread-list-item.test.tsx index c0935c261..f9b312e39 100644 --- a/components/email/__tests__/thread-list-item.test.tsx +++ b/components/email/__tests__/thread-list-item.test.tsx @@ -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; diff --git a/components/email/selectable-avatar.tsx b/components/email/selectable-avatar.tsx index 02076a968..8a5ef7924 100644 --- a/components/email/selectable-avatar.tsx +++ b/components/email/selectable-avatar.tsx @@ -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 & { /** 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) => 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", diff --git a/components/email/thread-list-item.tsx b/components/email/thread-list-item.tsx index eef2d6442..a9005d12d 100644 --- a/components/email/thread-list-item.tsx +++ b/components/email/thread-list-item.tsx @@ -294,8 +294,8 @@ const SingleEmailItem = React.forwardRef( 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 && (