diff --git a/components/email/__tests__/email-list-item.test.tsx b/components/email/__tests__/email-list-item.test.tsx index 2a6101fd..72d41e35 100644 --- a/components/email/__tests__/email-list-item.test.tsx +++ b/components/email/__tests__/email-list-item.test.tsx @@ -121,3 +121,34 @@ describe('EmailListItem tag badge', () => { expect(container.querySelector('p')).toBeNull(); }); }); + +describe('EmailListItem shift-range checkbox', () => { + beforeEach(() => { + useSettingsStore.setState({ emailKeywords: [...DEFAULT_KEYWORDS], showPreview: false, mailLayout: 'split' }); + }); + + it('shift-clicking the checkbox 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), anchor on e1 + useEmailStore.setState({ + emails: [e1, e2, e3], + selectedEmailIds: new Set(['e1']), + lastSelectedEmailId: 'e1', + selectedMailbox: 'inbox', + }); + + render(); + // the checkbox is the first button in the row (shown in selection mode) + const checkbox = screen.getAllByRole('button')[0]; + act(() => { + checkbox.dispatchEvent(new MouseEvent('click', { bubbles: true, shiftKey: true })); + }); + + const sel = useEmailStore.getState().selectedEmailIds; + expect(sel.has('e1')).toBe(true); + expect(sel.has('e2')).toBe(true); // the in-between row got filled in + expect(sel.has('e3')).toBe(true); + }); +}); diff --git a/components/email/email-list-item.tsx b/components/email/email-list-item.tsx index b6549346..2ee2ca84 100644 --- a/components/email/email-list-item.tsx +++ b/components/email/email-list-item.tsx @@ -87,7 +87,14 @@ export function EmailListItem({ email, selected, onClick, onDoubleClick, onConte const handleCheckboxClick = (e: React.MouseEvent) => { e.stopPropagation(); - toggleEmailSelection(email.id); + if (e.shiftKey) { + // Shift-click extends the selection from the anchor to here, like + // shift-clicking the row (the checkbox stops propagation, so the + // row's shift handler never runs — replicate it here). + selectRangeEmails(email.id); + } else { + toggleEmailSelection(email.id); + } }; const handleContextMenu = (e: React.MouseEvent) => { diff --git a/components/email/thread-list-item.tsx b/components/email/thread-list-item.tsx index ebd8ceb0..2e9caac6 100644 --- a/components/email/thread-list-item.tsx +++ b/components/email/thread-list-item.tsx @@ -134,7 +134,11 @@ const SingleEmailItem = React.forwardRef( const handleCheckboxClick = (e: React.MouseEvent) => { e.stopPropagation(); - toggleEmailSelection(email.id); + if (e.shiftKey) { + selectRangeEmails(email.id); + } else { + toggleEmailSelection(email.id); + } }; const handleContextMenu = (e: React.MouseEvent) => { @@ -513,6 +517,10 @@ export const ThreadListItem = React.forwardRef { e.stopPropagation(); + if (e.shiftKey) { + selectRangeEmails(latestEmail.id); + return; + } // Toggle selection for all emails in this thread const allSelected = thread.emails.every(em => selectedEmailIds.has(em.id)); const newSelection = new Set(selectedEmailIds);