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);