fix: show the full folder name as a tooltip when the sidebar cuts it

This commit is contained in:
Linus Rath
2026-10-28 10:59:44 +01:00
parent 62169427b9
commit 9eaf7ca754
2 changed files with 80 additions and 1 deletions
@@ -0,0 +1,63 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { Mailbox } from '@/lib/jmap/types';
// R11: text cut with an ellipsis gets a title with the full text. A folder
// name the sidebar had no shorter form for was cut by CSS with no title
// ("Gemeinsamer Postein…").
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 { useAuthStore } = await import('@/stores/auth-store');
const { useSettingsStore } = await import('@/stores/settings-store');
const box = (id: string, name: string) => ({
id, name, role: null, parentId: null, totalEmails: 0, unreadEmails: 0, myRights: {},
}) as unknown as Mailbox;
const mailboxes: Mailbox[] = [box('a', 'A very long folder name that does not fit'), box('b', 'Short')];
function label(name: string): HTMLElement {
return screen.getByText(name);
}
function setWidths(el: HTMLElement, scroll: number, client: number) {
Object.defineProperty(el, 'scrollWidth', { configurable: true, value: scroll });
Object.defineProperty(el, 'clientWidth', { configurable: true, value: client });
}
describe('cut folder names', () => {
beforeEach(() => {
useAuthStore.setState({ activeAccountId: 'login-a', client: { getAccountId: () => 'me', getCapabilities: () => ({}) } } as never);
useSettingsStore.setState({ hideAccountSwitcher: true } as never);
});
afterEach(() => vi.restoreAllMocks());
it('show the full name as a title when the ellipsis cuts them', () => {
render(<Sidebar mailboxes={mailboxes} selectedMailbox="b" />);
const long = label('A very long folder name that does not fit');
setWidths(long, 300, 120);
fireEvent.pointerEnter(long);
expect(long.title).toBe('A very long folder name that does not fit');
});
it('get no title when they fit', () => {
render(<Sidebar mailboxes={mailboxes} selectedMailbox="b" />);
const short = label('Short');
setWidths(short, 40, 120);
fireEvent.pointerEnter(short);
expect(short.title).toBe('');
});
});
+17 -1
View File
@@ -304,6 +304,15 @@ function SidebarRowCounts({
* What a row's menu handler needs from an event, for a menu opened by a
* long-press (at the finger) or from the row's ⋮ (under the button).
*/
/**
* R11: a name cut with an ellipsis gets a title with the full text. Measured
* when the pointer arrives, since only then is the title shown: a shortened
* form, or one the row's width still cuts.
*/
function titleIfCut(element: HTMLElement, full: string, shortened: boolean) {
element.title = shortened || element.scrollWidth > element.clientWidth ? full : "";
}
function menuEventAt(element: HTMLElement, point?: { clientX: number; clientY: number }): React.MouseEvent {
return {
type: point ? "contextmenu" : "click",
@@ -460,7 +469,14 @@ function SidebarRow({
</span>
{!isCollapsed && (
<>
<span ref={labelRef} className="flex-1 truncate" title={shortenedLabel !== label ? label : undefined}>{shortenedLabel}</span>
<span
ref={labelRef}
className="flex-1 truncate"
title={shortenedLabel !== label ? label : undefined}
onPointerEnter={(e) => titleIfCut(e.currentTarget, label, shortenedLabel !== label)}
>
{shortenedLabel}
</span>
{muted && (
<BellOff
className="w-3.5 h-3.5 flex-shrink-0 text-muted-foreground"