diff --git a/src/components/FolderTree.test.tsx b/src/components/FolderTree.test.tsx index 5fbf4cb8..7136c601 100644 --- a/src/components/FolderTree.test.tsx +++ b/src/components/FolderTree.test.tsx @@ -3,6 +3,7 @@ import { act, fireEvent, render, screen, within } from '@testing-library/react' import { describe, it, expect, vi } from 'vitest' import { FolderTree } from './FolderTree' import { FOLDER_ROW_SINGLE_CLICK_DELAY_MS } from './folder-tree/useFolderRowInteractions' +import { FOLDER_ROW_NESTING_INDENT, getFolderConnectorLeft } from './folder-tree/folderTreeLayout' import type { FolderNode, SidebarSelection } from '../types' const mockFolders: FolderNode[] = [ @@ -67,7 +68,8 @@ describe('FolderTree', () => { expect(screen.getByText('Laputa')).toBeInTheDocument() }) - it('lets the vault root collapse and expand its nested folders', () => { + it('lets the vault root collapse and expand from the row', () => { + vi.useFakeTimers() render( { />, ) - fireEvent.click(screen.getByLabelText('Collapse Laputa')) + fireEvent.click(screen.getByTestId('folder-row:')) + act(() => { + vi.advanceTimersByTime(FOLDER_ROW_SINGLE_CLICK_DELAY_MS) + }) expect(screen.queryByText('projects')).not.toBeInTheDocument() - fireEvent.click(screen.getByLabelText('Expand Laputa')) + fireEvent.click(screen.getByTestId('folder-row:')) + act(() => { + vi.advanceTimersByTime(FOLDER_ROW_SINGLE_CLICK_DELAY_MS) + }) expect(screen.getByText('projects')).toBeInTheDocument() + vi.useRealTimers() }) - it('expands children when clicking the folder chevron', () => { + it('expands children when clicking a folder row', () => { + vi.useFakeTimers() render() expect(screen.queryByText('laputa')).not.toBeInTheDocument() - fireEvent.click(screen.getByLabelText('Expand projects')) + fireEvent.click(screen.getByTestId('folder-row:projects')) + act(() => { + vi.advanceTimersByTime(FOLDER_ROW_SINGLE_CLICK_DELAY_MS) + }) expect(screen.getByText('laputa')).toBeInTheDocument() expect(screen.getByText('portfolio')).toBeInTheDocument() + vi.useRealTimers() }) it('calls onSelect with folder kind when clicking a folder row', () => { @@ -182,32 +196,24 @@ describe('FolderTree', () => { expect(onStartRenameFolder).toHaveBeenCalledWith('projects') }) - it('shows inline rename and delete actions for folders', () => { - const onDeleteFolder = vi.fn() - const onStartRenameFolder = vi.fn() - const onSelect = vi.fn() + it('keeps rename and delete out of row hover actions', () => { render( , ) - fireEvent.click(screen.getByTestId('rename-folder-btn:projects')) - fireEvent.click(screen.getByTestId('delete-folder-btn:projects')) - - expect(onSelect).toHaveBeenNthCalledWith(1, { kind: 'folder', path: 'projects' }) - expect(onStartRenameFolder).toHaveBeenCalledWith('projects') - expect(onSelect).toHaveBeenNthCalledWith(2, { kind: 'folder', path: 'projects' }) - expect(onDeleteFolder).toHaveBeenCalledWith('projects') + expect(screen.queryByTestId('rename-folder-btn:projects')).not.toBeInTheDocument() + expect(screen.queryByTestId('delete-folder-btn:projects')).not.toBeInTheDocument() }) - it('does not reserve a disclosure slot for leaf folders', () => { + it('does not render folder-level disclosure buttons', () => { render() const leafRowContainer = screen.getByTestId('folder-row:areas').parentElement @@ -216,7 +222,23 @@ describe('FolderTree', () => { expect(leafRowContainer).not.toBeNull() expect(parentRowContainer).not.toBeNull() expect(within(leafRowContainer as HTMLElement).queryAllByRole('button')).toHaveLength(1) - expect(within(parentRowContainer as HTMLElement).queryAllByRole('button')).toHaveLength(2) + expect(within(parentRowContainer as HTMLElement).queryAllByRole('button')).toHaveLength(1) + expect(screen.queryByLabelText('Expand projects')).not.toBeInTheDocument() + }) + + it('aligns nested folders with the parent folder name and centers connectors on parent icons', () => { + render( + , + ) + + expect(screen.getByTestId('folder-row:').parentElement).toHaveStyle({ paddingLeft: '0px' }) + expect(screen.getByTestId('folder-row:projects').parentElement).toHaveStyle({ paddingLeft: `${FOLDER_ROW_NESTING_INDENT}px` }) + expect(screen.getByTestId('folder-connector:')).toHaveStyle({ left: `${getFolderConnectorLeft(0)}px` }) }) it('shows the rename input when a folder is being renamed', () => { diff --git a/src/components/Sidebar.test.tsx b/src/components/Sidebar.test.tsx index 9d6ba224..0bfcd761 100644 --- a/src/components/Sidebar.test.tsx +++ b/src/components/Sidebar.test.tsx @@ -1170,7 +1170,7 @@ describe('Sidebar', () => { const favoritesHeader = screen.getByText('FAVORITES').closest('div') as HTMLElement const countChip = within(favoritesHeader).getByTestId('sidebar-count-chip') - expect(favoritesHeader).toHaveStyle({ padding: '8px 8px 8px 16px' }) + expect(favoritesHeader).toHaveStyle({ padding: '8px 8px 8px 12px' }) expect(countChip).toHaveStyle({ background: 'var(--muted)', height: '18px', @@ -1441,7 +1441,7 @@ describe('Sidebar', () => { const navItem = viewLabel.closest('[class*="cursor-pointer"]') as HTMLElement const countChip = within(navItem).getByTestId('view-count-chip') - expect(navItem).toHaveStyle({ padding: '6px 8px 6px 16px' }) + expect(navItem).toHaveStyle({ padding: '6px 8px 6px 12px' }) expect(countChip).toHaveStyle({ background: 'var(--muted)', height: '20px', @@ -1460,8 +1460,8 @@ describe('Sidebar', () => { const viewItem = screen.getByText('Active Projects').closest('[class*="cursor-pointer"]') as HTMLElement const viewCount = within(viewItem).getByTestId('view-count-chip') - expect(topNavItem).toHaveStyle({ padding: '6px 8px 6px 16px' }) - expect(viewItem).toHaveStyle({ padding: '6px 8px 6px 16px' }) + expect(topNavItem).toHaveStyle({ padding: '6px 8px 6px 12px' }) + expect(viewItem).toHaveStyle({ padding: '6px 8px 6px 12px' }) expect(topNavCount).toHaveStyle({ background: 'var(--muted)', height: '20px', diff --git a/src/components/SidebarParts.tsx b/src/components/SidebarParts.tsx index 221a7a59..8a112f8c 100644 --- a/src/components/SidebarParts.tsx +++ b/src/components/SidebarParts.tsx @@ -491,7 +491,7 @@ function SectionHeader({ label, type, Icon, sectionColor, sectionLightColor, ite return (
void onOpenMenu: (node: FolderNode, event: ReactMouseEvent) => void onSelect: () => void onStartRenameFolder?: (folderPath: string) => void onToggle: (path: string) => void - locale?: AppLocale } export function FolderItemRow({ @@ -33,16 +26,12 @@ export function FolderItemRow({ isExpanded, isSelected, node, - onDeleteFolder, onOpenMenu, onSelect, onStartRenameFolder, onToggle, - locale = 'en', }: FolderItemRowProps) { const hasChildren = node.children.length > 0 - const expandLabel = translate(locale, isExpanded ? 'sidebar.folder.collapse' : 'sidebar.folder.expand', { name: node.name }) - const hasActions = !!onStartRenameFolder || !!onDeleteFolder const { handleRenameDoubleClick, handleSelectClick } = useFolderRowInteractions({ hasChildren, onRenameFolder: onStartRenameFolder ? () => onStartRenameFolder(node.path) : undefined, @@ -64,15 +53,8 @@ export function FolderItemRow({ onOpenMenu(node, event) }} > - onToggle(node.path)} - /> - {hasActions && ( -
- {onStartRenameFolder && ( - { - onSelect() - onStartRenameFolder(node.path) - }} - > - - - )} - {onDeleteFolder && ( - { - onSelect() - onDeleteFolder(node.path) - }} - > - - - )} -
- )}
) } -function FolderToggleButton({ - expandLabel, - hasChildren, - isExpanded, - onToggle, -}: { - expandLabel: string - hasChildren: boolean - isExpanded: boolean - onToggle: () => void -}) { - if (!hasChildren) return null - - return ( - - ) -} - -function FolderActionButton({ - ariaLabel, - children, - destructive = false, - onClick, - testId, - title, -}: { - ariaLabel: string - children: ReactNode - destructive?: boolean - onClick: () => void - testId: string - title: string -}) { - return ( - - ) -} - function FolderSelectButton({ contentInset, - hasActions, hasChildren, isExpanded, isSelected, @@ -193,7 +76,6 @@ function FolderSelectButton({ onDoubleClick, }: { contentInset: number - hasActions: boolean hasChildren: boolean isExpanded: boolean isSelected: boolean @@ -212,10 +94,11 @@ function FolderSelectButton({ style={{ paddingTop: 6, paddingBottom: 6, - paddingLeft: hasChildren ? 0 : contentInset, - paddingRight: hasActions ? 48 : 16, + paddingLeft: contentInset, + paddingRight: 16, }} title={node.path || node.name} + aria-expanded={hasChildren ? isExpanded : undefined} onClick={(event) => onClick(event.detail)} onDoubleClick={onDoubleClick} data-testid={`folder-row:${node.path}`} diff --git a/src/components/folder-tree/FolderTreeRow.tsx b/src/components/folder-tree/FolderTreeRow.tsx index c71d2cb0..80fc61ca 100644 --- a/src/components/folder-tree/FolderTreeRow.tsx +++ b/src/components/folder-tree/FolderTreeRow.tsx @@ -2,6 +2,7 @@ import { memo, useCallback, type MouseEvent as ReactMouseEvent } from 'react' import type { FolderNode, SidebarSelection } from '../../types' import { FolderNameInput } from './FolderNameInput' import { FolderItemRow } from './FolderItemRow' +import { FOLDER_ROW_CONTENT_INSET, getFolderConnectorLeft, getFolderDepthIndent } from './folderTreeLayout' import { translate, type AppLocale } from '../../lib/i18n' interface FolderTreeRowProps { @@ -73,10 +74,11 @@ function FolderChildren({ if (!isExpanded || !hasChildren) return null return ( -
+
{node.children.map((child) => ( 0 - const depthIndent = depth * 16 - const contentInset = 16 + const depthIndent = getFolderDepthIndent(depth) + const contentInset = FOLDER_ROW_CONTENT_INSET const selectFolder = useCallback(() => { onSelect(node.path === '' ? { kind: 'folder', path: '', rootPath } : { kind: 'folder', path: node.path }) }, [node.path, onSelect, rootPath]) @@ -133,12 +135,10 @@ export const FolderTreeRow = memo(function FolderTreeRow({ isExpanded={isExpanded} isSelected={isSelected} node={node} - onDeleteFolder={canMutateFolder ? onDeleteFolder : undefined} onOpenMenu={onOpenMenu} onSelect={selectFolder} onStartRenameFolder={canMutateFolder ? onStartRenameFolder : undefined} onToggle={onToggle} - locale={locale} /> ) diff --git a/src/components/folder-tree/folderTreeLayout.ts b/src/components/folder-tree/folderTreeLayout.ts new file mode 100644 index 00000000..84cbdbd4 --- /dev/null +++ b/src/components/folder-tree/folderTreeLayout.ts @@ -0,0 +1,17 @@ +export const FOLDER_ROW_CONTENT_INSET = 12 +export const FOLDER_ROW_ICON_SIZE = 17 +export const FOLDER_ROW_ICON_GAP = 8 +export const FOLDER_ROW_NESTING_INDENT = FOLDER_ROW_ICON_SIZE + FOLDER_ROW_ICON_GAP + +const FOLDER_CONNECTOR_WIDTH = 1 + +export function getFolderDepthIndent(depth: number) { + return depth * FOLDER_ROW_NESTING_INDENT +} + +export function getFolderConnectorLeft(depth: number) { + return FOLDER_ROW_CONTENT_INSET + + getFolderDepthIndent(depth) + + FOLDER_ROW_ICON_SIZE / 2 + - FOLDER_CONNECTOR_WIDTH / 2 +} diff --git a/src/components/sidebar/SidebarLoadingSections.tsx b/src/components/sidebar/SidebarLoadingSections.tsx index 44208c72..6a0969cc 100644 --- a/src/components/sidebar/SidebarLoadingSections.tsx +++ b/src/components/sidebar/SidebarLoadingSections.tsx @@ -4,6 +4,7 @@ import { SlidersHorizontal } from 'lucide-react' import { Button } from '@/components/ui/button' import { translate, type AppLocale } from '../../lib/i18n' import { SidebarGroupHeader } from './SidebarGroupHeader' +import { SIDEBAR_ITEM_PADDING } from './sidebarStyles' interface SidebarLoadingSectionsProps { collapsed: boolean @@ -157,7 +158,7 @@ function SidebarLoadingRow({ return (
diff --git a/src/components/sidebar/sidebarStyles.ts b/src/components/sidebar/sidebarStyles.ts index ec79e2b7..b8454761 100644 --- a/src/components/sidebar/sidebarStyles.ts +++ b/src/components/sidebar/sidebarStyles.ts @@ -1,13 +1,13 @@ export const SIDEBAR_ITEM_PADDING = { - regular: '6px 16px', - withCount: '6px 8px 6px 16px', - compact: '4px 16px', - compactWithCount: '4px 8px 4px 16px', + regular: '6px 16px 6px 12px', + withCount: '6px 8px 6px 12px', + compact: '4px 16px 4px 12px', + compactWithCount: '4px 8px 4px 12px', } as const export const SIDEBAR_GROUP_HEADER_PADDING = { - regular: '8px 14px 8px 16px', - withCount: '8px 8px 8px 16px', + regular: '8px 14px 8px 12px', + withCount: '8px 8px 8px 12px', } as const export const SIDEBAR_SECTION_CONTENT_PADDING_BOTTOM = 8