fix: open a folder from anywhere in its sidebar row #1501

This commit is contained in:
Linus Rath
2026-11-02 19:57:58 +01:00
parent 3530e056ce
commit 3241f17c44
2 changed files with 95 additions and 0 deletions
@@ -0,0 +1,85 @@
import { render, fireEvent } from '@testing-library/react';
import { describe, it, expect, beforeEach, vi } from 'vitest';
import type { Mailbox } from '@/lib/jmap/types';
// A folder row opens its folder wherever it is clicked, not only on the
// button inside it. Mountain View draws a 32px pill around a ~20px button,
// and clicks on the pill's padding, the chevron slot or the count did
// nothing (#1501). The row's own controls keep their click.
vi.mock('@/components/plugins/plugin-slot', () => ({ PluginSlot: () => null }));
vi.mock('@/components/tour/tour-provider', () => ({
useTour: () => ({ startTour: vi.fn(), resetTourCompletion: vi.fn(), activeTour: null, endTour: vi.fn(), registerStep: vi.fn() }),
}));
vi.mock('@/contexts/drag-drop-context', () => ({
useDragDropContext: () => ({ isDragging: false }),
}));
vi.mock('@/hooks/use-mailbox-drop', () => ({
useMailboxDrop: () => ({ dropHandlers: {}, isValidDropTarget: false, isInvalidDropTarget: false }),
}));
vi.mock('@/hooks/use-tag-drop', () => ({
useTagDrop: () => ({ dropHandlers: {}, isValidDropTarget: false }),
}));
vi.mock('../account-switcher', () => ({ AccountSwitcher: () => null }));
const { Sidebar } = await import('../sidebar');
const { useSettingsStore } = await import('@/stores/settings-store');
const { useUIStore } = await import('@/stores/ui-store');
const mailboxes = [
{ id: 'inbox', name: 'Inbox', role: 'inbox', parentId: null, totalEmails: 9, unreadEmails: 4, sortOrder: 0, myRights: { mayAddItems: true } },
{ id: 'receipts', name: 'Receipts', role: null, parentId: null, totalEmails: 5, unreadEmails: 3, sortOrder: 1, myRights: { mayAddItems: true } },
{ id: 'shops', name: 'Shops', role: null, parentId: 'receipts', totalEmails: 0, unreadEmails: 0, sortOrder: 0, myRights: { mayAddItems: true } },
] as unknown as Mailbox[];
const row = (id: string) => document.querySelector<HTMLElement>(`[data-testid="folder-row"][data-mailbox-id="${id}"]`)!;
describe.each([
['Mountain View', 'mountain-view', true],
['the default layout', 'default', false],
] as const)('folder row click area in %s', (_name, interfaceLayout, mvShell) => {
beforeEach(() => {
useUIStore.setState({ sidebarCollapsed: false } as never);
useSettingsStore.setState({ interfaceLayout, hideAccountSwitcher: true, enableUnifiedMailbox: false } as never);
});
it('opens the folder from a click on the row outside its button', () => {
const onMailboxSelect = vi.fn();
render(<Sidebar mailboxes={mailboxes} selectedMailbox="inbox" onMailboxSelect={onMailboxSelect} mvShell={mvShell} />);
fireEvent.click(row('receipts'));
expect(onMailboxSelect).toHaveBeenCalledWith('receipts');
});
it('opens the folder from a click on its count', () => {
const onMailboxSelect = vi.fn();
render(<Sidebar mailboxes={mailboxes} selectedMailbox="inbox" onMailboxSelect={onMailboxSelect} mvShell={mvShell} />);
fireEvent.click(row('receipts').querySelector('[data-testid="folder-counts"]')!);
expect(onMailboxSelect).toHaveBeenCalledWith('receipts');
});
it('shows the pointer over the whole row', () => {
render(<Sidebar mailboxes={mailboxes} selectedMailbox="inbox" onMailboxSelect={() => {}} mvShell={mvShell} />);
expect(row('receipts').className).toContain('cursor-pointer');
});
it('only expands or collapses the folder from its chevron', () => {
const onMailboxSelect = vi.fn();
render(<Sidebar mailboxes={mailboxes} selectedMailbox="inbox" onMailboxSelect={onMailboxSelect} mvShell={mvShell} />);
const shownBefore = row('shops') !== null;
fireEvent.click(row('receipts').querySelector('[data-testid="folder-expand-toggle"]')!);
expect(row('shops') !== null).toBe(!shownBefore);
expect(onMailboxSelect).not.toHaveBeenCalled();
});
});
describe('folder row click area with the unread filter', () => {
it('filters to unread from the unread count without also opening the folder', () => {
useSettingsStore.setState({ interfaceLayout: 'default', hideAccountSwitcher: true, enableUnifiedMailbox: false } as never);
const onMailboxSelect = vi.fn();
const onUnreadFilterClick = vi.fn();
render(<Sidebar mailboxes={mailboxes} selectedMailbox="inbox" onMailboxSelect={onMailboxSelect} onUnreadFilterClick={onUnreadFilterClick} />);
fireEvent.click(row('receipts').querySelector('[data-testid="folder-counts"] button')!);
expect(onUnreadFilterClick).toHaveBeenCalledWith('receipts');
expect(onMailboxSelect).not.toHaveBeenCalled();
});
});
+10
View File
@@ -430,11 +430,20 @@ function SidebarRow({
const longPress = useLongPress((point) => {
if (rowRef.current && onContextMenu) onContextMenu(menuEventAt(rowRef.current, point));
}, hasMenu);
// The whole row opens the folder, not just its button: the row's padding,
// the chevron slot and the space around the count were dead (#1501). The
// button stays the keyboard target; controls of their own keep their click.
const openFromRow = !isVirtual && onClick
? (e: React.MouseEvent) => {
if (!(e.target as Element).closest('button, a, input, [role="button"]')) onClick();
}
: undefined;
return (
<div
ref={rowRef}
{...(dropHandlers || {})}
onClick={openFromRow}
onContextMenu={onContextMenu}
onTouchStart={hasMenu ? longPress.onTouchStart : undefined}
onTouchMove={hasMenu ? longPress.onTouchMove : undefined}
@@ -452,6 +461,7 @@ function SidebarRow({
className={cn(
"group w-full flex items-center max-lg:min-h-[44px] text-sm transition-colors duration-150",
isCollapsed ? "justify-center px-1" : "pe-2",
openFromRow && "cursor-pointer select-none",
isVirtual
? "text-muted-foreground"
: isSelected