Refine: right mouse click on navigator pane removes file selection

This commit is contained in:
aleksey-hoffman committed 2026-07-12 19:30:26 +03:00
1 parent f35d1b505c
commit a6139049cb
8 files changed
+219 -29

No files matched your search

@@ -2,7 +2,9 @@
// License: GNU GPLv3 or later. See the license file in the project root for more information.
// Copyright © 2021 - present Aleksey Hoffman. All rights reserved.
import { describe, expect, it } from 'vitest';
import {
afterEach, describe, expect, it, vi,
} from 'vitest';
import type { DirEntry } from '@/types/dir-entry';
import {
createFileBrowserVirtualRows,
@@ -152,7 +154,11 @@ describe('getGridColumnCount', () => {
});
describe('resolveViewportContentWidth', () => {
it('prefers entries container width over viewport width', () => {
afterEach(() => {
vi.restoreAllMocks();
});
it('prefers entries container content-box width over viewport width', () => {
const viewport = document.createElement('div');
const contentInner = document.createElement('div');
const entriesContainer = document.createElement('div');
@@ -178,6 +184,39 @@ describe('resolveViewportContentWidth', () => {
expect(resolveViewportContentWidth(viewport)).toBe(850);
});
it('subtracts horizontal padding from the entries container width', () => {
const viewport = document.createElement('div');
const contentInner = document.createElement('div');
const entriesContainer = document.createElement('div');
contentInner.className = 'file-browser__content-inner';
entriesContainer.className = 'file-browser__entries-container';
contentInner.appendChild(entriesContainer);
viewport.appendChild(contentInner);
Object.defineProperty(entriesContainer, 'clientWidth', {
value: 900,
configurable: true,
});
vi.spyOn(window, 'getComputedStyle').mockImplementation((element) => {
if (element === entriesContainer) {
return {
paddingLeft: '20px',
paddingRight: '20px',
} as CSSStyleDeclaration;
}
return {
paddingLeft: '0px',
paddingRight: '0px',
} as CSSStyleDeclaration;
});
expect(resolveViewportContentWidth(viewport)).toBe(860);
expect(getGridColumnCount(resolveViewportContentWidth(viewport))).toBe(4);
expect(getGridColumnCount(entriesContainer.clientWidth)).toBe(5);
});
it('falls back to content inner width while entries container is absent', () => {
const viewport = document.createElement('div');
const contentInner = document.createElement('div');
@@ -26,6 +26,7 @@ import {
collectFileBrowserBoxSelectionEntries,
type FileBrowserBoxSelectionBox,
} from '../utils/file-browser-box-selection-hit-test';
import { getElementContentBoxClientRect } from '../utils/file-browser-content-box';
import type { FileBrowserVirtualRow } from '../utils/file-browser-virtual-rows';
import { clearDocumentTextSelection } from '@/utils/document-selection';
@@ -162,7 +163,7 @@ export function useFileBrowserBoxSelection(options: {
}
const viewportRect = viewport.getBoundingClientRect();
const contentRect = entriesContainer.getBoundingClientRect();
const contentRect = getElementContentBoxClientRect(entriesContainer);
const boxSelectionEntries = collectFileBrowserBoxSelectionEntries({
rows: options.virtualRows.value,
selectionBox,
@@ -19,7 +19,7 @@ import { useDeleteJobsStore } from '@/stores/runtime/delete-jobs';
import { toast, ToastProgress, ToastStatic } from '@/components/ui/toaster';
import { useLanShare } from '@/composables/use-lan-share';
import { useCopyMoveWithConflicts } from '@/composables/use-copy-move-with-conflicts';
import normalizePath, { getPathDisplayName, joinPath } from '@/utils/normalize-path';
import normalizePath, { joinPath } from '@/utils/normalize-path';
import { isVirtualLocationPath } from '@/utils/virtual-locations';
import {
getSharedSourceDirectory,
@@ -32,6 +32,7 @@ import { basenameFromPath } from '@/utils/source-display-name';
import { usePermanentDeleteConfirm } from '@/composables/use-permanent-delete-confirm';
import { usePlatformStore } from '@/stores/runtime/platform';
import { isContextMenuActionVisible } from '@/modules/navigator/components/file-browser/utils/context-menu-action-visibility';
import { applyBackgroundContextMenu } from '@/modules/navigator/components/file-browser/utils/file-browser-background-context-menu';
import { resolveNavigableItemTarget } from '@/utils/resolve-navigable-item-target';
import type { CreateLinksResult, LinkCreationKind } from '@/utils/link-operations';
import { getFileBrowserVisualEntryOrder } from '../file-browser-entry-groups';
@@ -300,28 +301,10 @@ export function useFileBrowserSelection(
}
function handleBackgroundContextMenu() {
clearSelection();
const currentDirEntry: DirEntry = {
name: getPathDisplayName(currentPathRef.value) || currentPathRef.value,
path: currentPathRef.value,
is_dir: true,
is_file: false,
is_hidden: false,
is_symlink: false,
size: 0,
created_time: 0,
modified_time: 0,
accessed_time: 0,
item_count: null,
ext: null,
mime: null,
};
contextMenu.value = {
targetEntry: currentDirEntry,
selectedEntries: [currentDirEntry],
};
contextMenu.value = applyBackgroundContextMenu({
clearFileSelection: clearSelection,
currentPath: currentPathRef.value,
});
}
function closeContextMenu() {
@@ -15,6 +15,7 @@ import type { DirEntry } from '@/types/dir-entry';
import normalizePath from '@/utils/normalize-path';
import { groupFileBrowserEntries } from '../file-browser-entry-groups';
import { getFileBrowserGridGap } from '../utils/file-browser-layout-gaps';
import { getElementContentBoxWidth } from '../utils/file-browser-content-box';
import {
getGridColumnCount,
type FileBrowserGridEntryVariant,
@@ -173,16 +174,16 @@ export function resolveViewportContentWidth(viewport: HTMLElement): number {
const entriesContainer = viewport.querySelector<HTMLElement>(ENTRIES_CONTAINER_SELECTOR);
if (entriesContainer) {
return entriesContainer.clientWidth;
return getElementContentBoxWidth(entriesContainer);
}
const contentInner = viewport.querySelector<HTMLElement>(CONTENT_INNER_SELECTOR);
if (contentInner) {
return contentInner.clientWidth;
return getElementContentBoxWidth(contentInner);
}
return viewport.clientWidth;
return getElementContentBoxWidth(viewport);
}
function createListRows(
@@ -0,0 +1,39 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// License: GNU GPLv3 or later. See the license file in the project root for more information.
// Copyright © 2021 - present Aleksey Hoffman. All rights reserved.
import { describe, expect, it, vi } from 'vitest';
import {
applyBackgroundContextMenu,
createBackgroundContextMenuState,
createCurrentDirectoryContextMenuEntry,
} from '../file-browser-background-context-menu';
describe('file-browser-background-context-menu', () => {
it('builds a directory entry for the current path', () => {
expect(createCurrentDirectoryContextMenuEntry('/tmp/photos')).toMatchObject({
name: 'photos',
path: '/tmp/photos',
is_dir: true,
is_file: false,
});
});
it('clears file selection and opens the directory context menu', () => {
const clearFileSelection = vi.fn();
const contextMenu = applyBackgroundContextMenu({
clearFileSelection,
currentPath: '/tmp/photos',
});
expect(clearFileSelection).toHaveBeenCalledOnce();
expect(contextMenu).toEqual(createBackgroundContextMenuState('/tmp/photos'));
expect(contextMenu.selectedEntries).toEqual([
expect.objectContaining({
path: '/tmp/photos',
is_dir: true,
}),
]);
expect(contextMenu.targetEntry?.path).toBe('/tmp/photos');
});
});
@@ -0,0 +1,56 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// License: GNU GPLv3 or later. See the license file in the project root for more information.
// Copyright © 2021 - present Aleksey Hoffman. All rights reserved.
import {
afterEach, describe, expect, it, vi,
} from 'vitest';
import {
getElementContentBoxClientRect,
getElementContentBoxWidth,
} from '../file-browser-content-box';
describe('file-browser-content-box', () => {
afterEach(() => {
vi.restoreAllMocks();
});
it('returns content-box width excluding horizontal padding', () => {
const element = document.createElement('div');
Object.defineProperty(element, 'clientWidth', {
value: 900,
configurable: true,
});
vi.spyOn(window, 'getComputedStyle').mockReturnValue({
paddingLeft: '20px',
paddingRight: '20px',
} as CSSStyleDeclaration);
expect(getElementContentBoxWidth(element)).toBe(860);
});
it('returns a content-box client rect inset by padding and border', () => {
const element = document.createElement('div');
Object.defineProperty(element, 'clientWidth', {
value: 900,
configurable: true,
});
Object.defineProperty(element, 'clientHeight', {
value: 600,
configurable: true,
});
vi.spyOn(element, 'getBoundingClientRect').mockReturnValue(new DOMRect(100, 200, 940, 620));
vi.spyOn(window, 'getComputedStyle').mockReturnValue({
paddingLeft: '20px',
paddingRight: '20px',
paddingTop: '0px',
paddingBottom: '0px',
borderLeftWidth: '0px',
borderTopWidth: '0px',
} as CSSStyleDeclaration);
expect(getElementContentBoxClientRect(element)).toEqual(new DOMRect(120, 200, 860, 600));
});
});
@@ -0,0 +1,42 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// License: GNU GPLv3 or later. See the license file in the project root for more information.
// Copyright © 2021 - present Aleksey Hoffman. All rights reserved.
import type { DirEntry } from '@/types/dir-entry';
import type { ContextMenuState } from '@/modules/navigator/components/file-browser/types';
import { getPathDisplayName } from '@/utils/normalize-path';
export function createCurrentDirectoryContextMenuEntry(currentPath: string): DirEntry {
return {
name: getPathDisplayName(currentPath) || currentPath,
path: currentPath,
is_dir: true,
is_file: false,
is_hidden: false,
is_symlink: false,
size: 0,
created_time: 0,
modified_time: 0,
accessed_time: 0,
item_count: null,
ext: null,
mime: null,
};
}
export function createBackgroundContextMenuState(currentPath: string): ContextMenuState {
const currentDirEntry = createCurrentDirectoryContextMenuEntry(currentPath);
return {
targetEntry: currentDirEntry,
selectedEntries: [currentDirEntry],
};
}
export function applyBackgroundContextMenu(options: {
clearFileSelection: () => void;
currentPath: string;
}): ContextMenuState {
options.clearFileSelection();
return createBackgroundContextMenuState(options.currentPath);
}
@@ -0,0 +1,29 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// License: GNU GPLv3 or later. See the license file in the project root for more information.
// Copyright © 2021 - present Aleksey Hoffman. All rights reserved.
export function getElementContentBoxWidth(element: HTMLElement): number {
const style = getComputedStyle(element);
const paddingLeft = Number.parseFloat(style.paddingLeft) || 0;
const paddingRight = Number.parseFloat(style.paddingRight) || 0;
return Math.max(0, element.clientWidth - paddingLeft - paddingRight);
}
export function getElementContentBoxClientRect(element: HTMLElement): DOMRect {
const rect = element.getBoundingClientRect();
const style = getComputedStyle(element);
const paddingLeft = Number.parseFloat(style.paddingLeft) || 0;
const paddingRight = Number.parseFloat(style.paddingRight) || 0;
const paddingTop = Number.parseFloat(style.paddingTop) || 0;
const paddingBottom = Number.parseFloat(style.paddingBottom) || 0;
const borderLeft = Number.parseFloat(style.borderLeftWidth) || 0;
const borderTop = Number.parseFloat(style.borderTopWidth) || 0;
return new DOMRect(
rect.left + borderLeft + paddingLeft,
rect.top + borderTop + paddingTop,
Math.max(0, element.clientWidth - paddingLeft - paddingRight),
Math.max(0, element.clientHeight - paddingTop - paddingBottom),
);
}