Refine: right mouse click on navigator pane removes file selection

This commit is contained in:
aleksey-hoffman committed 2026-07-12 19:36:27 +03:00
1 parent a6139049cb
commit 00190b6d8d
5 files changed
+159 -26

No files matched your search

@@ -5,12 +5,17 @@
import {
afterEach, describe, expect, it, vi,
} from 'vitest';
import {
computed, defineComponent, h, nextTick, ref, type Ref,
} from 'vue';
import { mount } from '@vue/test-utils';
import type { DirEntry } from '@/types/dir-entry';
import {
createFileBrowserVirtualRows,
getFileBrowserGridNavigationEntry,
getGridColumnCount,
resolveViewportContentWidth,
useFileBrowserVirtualLayout,
} from '../use-file-browser-virtual-layout';
function createEntry(name: string, options: Partial<DirEntry> = {}): DirEntry {
@@ -237,4 +242,63 @@ describe('resolveViewportContentWidth', () => {
expect(getGridColumnCount(resolveViewportContentWidth(viewport))).toBe(4);
expect(getGridColumnCount(viewport.clientWidth)).toBe(5);
});
it('updates content width when increased gaps change entry padding', async () => {
const viewport = document.createElement('div');
const contentInner = document.createElement('div');
const entriesContainer = document.createElement('div');
let increaseFileViewGaps!: Ref<boolean>;
let viewportWidth!: Ref<number>;
contentInner.className = 'file-browser__content-inner';
entriesContainer.className = 'file-browser__entries-container';
contentInner.appendChild(entriesContainer);
viewport.appendChild(contentInner);
Object.defineProperty(viewport, 'clientHeight', {
value: 600,
configurable: true,
});
Object.defineProperty(entriesContainer, 'clientWidth', {
value: 900,
configurable: true,
});
vi.spyOn(window, 'getComputedStyle').mockImplementation((element) => {
const padding = element === entriesContainer && increaseFileViewGaps.value ? '20px' : '0px';
return {
paddingLeft: padding,
paddingRight: padding,
} as CSSStyleDeclaration;
});
vi.spyOn(window, 'requestAnimationFrame').mockImplementation((callback) => {
callback(0);
return 1;
});
const wrapper = mount(defineComponent({
setup() {
increaseFileViewGaps = ref(false);
const virtualLayout = useFileBrowserVirtualLayout({
entries: computed(() => []),
layout: () => 'list',
increaseFileViewGaps: () => increaseFileViewGaps.value,
});
viewportWidth = virtualLayout.viewportWidth;
virtualLayout.setScrollViewportRef(viewport);
return () => h('div');
},
}));
expect(viewportWidth.value).toBe(900);
increaseFileViewGaps.value = true;
await nextTick();
await nextTick();
expect(viewportWidth.value).toBe(860);
wrapper.unmount();
});
});
@@ -26,7 +26,11 @@ import {
collectFileBrowserBoxSelectionEntries,
type FileBrowserBoxSelectionBox,
} from '../utils/file-browser-box-selection-hit-test';
import { getElementContentBoxClientRect } from '../utils/file-browser-content-box';
import {
getElementContentBoxClientRect,
getElementContentBoxInsets,
type ElementContentBoxInsets,
} from '../utils/file-browser-content-box';
import type { FileBrowserVirtualRow } from '../utils/file-browser-virtual-rows';
import { clearDocumentTextSelection } from '@/utils/document-selection';
@@ -104,6 +108,7 @@ export function useFileBrowserBoxSelection(options: {
let pointerOriginContent: BoxSelectionContentPoint | null = null;
let latestPointer: BoxSelectionPointerPosition | null = null;
let scrollViewportElement: HTMLElement | null = null;
let entriesContainerContentBoxInsets: ElementContentBoxInsets | null = null;
let selectionMode: BoxSelectionMode = 'replace';
let baseSelection: DirEntry[] = [];
let isActive = false;
@@ -154,16 +159,12 @@ export function useFileBrowserBoxSelection(options: {
overlay.style.height = style.height;
}
function applyBoxSelection(selectionBox: FileBrowserBoxSelectionBox, scrollTop: number) {
const viewport = options.scrollViewportRef.value;
const entriesContainer = options.entriesContainerRef.value;
if (!viewport || !entriesContainer) {
return;
}
const viewportRect = viewport.getBoundingClientRect();
const contentRect = getElementContentBoxClientRect(entriesContainer);
function applyBoxSelection(
selectionBox: FileBrowserBoxSelectionBox,
scrollTop: number,
viewportRect: DOMRect,
contentRect: DOMRect,
) {
const boxSelectionEntries = collectFileBrowserBoxSelectionEntries({
rows: options.virtualRows.value,
selectionBox,
@@ -184,7 +185,13 @@ export function useFileBrowserBoxSelection(options: {
function tick() {
animationFrameId = null;
if (!isActive || !pointerOriginContent || !pointerOriginClient || !latestPointer) {
if (
!isActive
|| !pointerOriginContent
|| !pointerOriginClient
|| !latestPointer
|| !entriesContainerContentBoxInsets
) {
return;
}
@@ -200,7 +207,10 @@ export function useFileBrowserBoxSelection(options: {
const paneRect = paneElement.getBoundingClientRect();
const overlayHostRect = resolveOverlayHostElement(paneElement).getBoundingClientRect();
const viewportRect = viewport.getBoundingClientRect();
const contentRect = entriesContainer.getBoundingClientRect();
const contentRect = getElementContentBoxClientRect(
entriesContainer,
entriesContainerContentBoxInsets,
);
const virtualContentOffset = options.virtualContentOffset.value;
const deltaX = Math.abs(latestPointer.clientX - pointerOriginClient.clientX);
const deltaY = Math.abs(latestPointer.clientY - pointerOriginClient.clientY);
@@ -227,7 +237,7 @@ export function useFileBrowserBoxSelection(options: {
paneRect,
);
updateOverlay(selectionBox, overlayHostRect);
applyBoxSelection(selectionBox, scrollTop);
applyBoxSelection(selectionBox, scrollTop, viewportRect, contentRect);
}
function scheduleTick() {
@@ -268,6 +278,7 @@ export function useFileBrowserBoxSelection(options: {
pointerOriginClient = null;
pointerOriginContent = null;
latestPointer = null;
entriesContainerContentBoxInsets = null;
selectionMode = 'replace';
baseSelection = [];
activationThresholdReached = false;
@@ -378,10 +389,11 @@ export function useFileBrowserBoxSelection(options: {
clientY: event.clientY,
};
latestPointer = { ...pointerOriginClient };
entriesContainerContentBoxInsets = getElementContentBoxInsets(entriesContainer);
pointerOriginContent = clientPointToBoxSelectionContentPoint(
pointerOriginClient,
viewport.getBoundingClientRect(),
entriesContainer.getBoundingClientRect(),
getElementContentBoxClientRect(entriesContainer, entriesContainerContentBoxInsets),
viewport.scrollTop,
viewport.scrollLeft,
options.virtualContentOffset.value,
@@ -703,6 +703,10 @@ export function useFileBrowserVirtualLayout(options: {
scheduleViewportSizeUpdate();
});
watch(() => options.increaseFileViewGaps?.(), () => {
scheduleViewportSizeUpdate();
});
onBeforeUnmount(() => {
disconnectViewportResizeObserver();
});
@@ -7,6 +7,7 @@ import {
} from 'vitest';
import {
getElementContentBoxClientRect,
getElementContentBoxInsets,
getElementContentBoxWidth,
} from '../file-browser-content-box';
@@ -53,4 +54,32 @@ describe('file-browser-content-box', () => {
expect(getElementContentBoxClientRect(element)).toEqual(new DOMRect(120, 200, 860, 600));
});
it('reuses supplied content-box insets without reading computed style', () => {
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));
const getComputedStyleSpy = vi.spyOn(window, 'getComputedStyle').mockReturnValue({
paddingLeft: '20px',
paddingRight: '20px',
paddingTop: '0px',
paddingBottom: '0px',
borderLeftWidth: '0px',
borderTopWidth: '0px',
} as CSSStyleDeclaration);
const insets = getElementContentBoxInsets(element);
getComputedStyleSpy.mockClear();
expect(getElementContentBoxClientRect(element, insets)).toEqual(new DOMRect(120, 200, 860, 600));
expect(getComputedStyleSpy).not.toHaveBeenCalled();
});
});
@@ -2,23 +2,47 @@
// 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 {
export interface ElementContentBoxInsets {
borderLeft: number;
borderTop: number;
paddingBottom: number;
paddingLeft: number;
paddingRight: number;
paddingTop: number;
}
export function getElementContentBoxInsets(element: HTMLElement): ElementContentBoxInsets {
const style = getComputedStyle(element);
const paddingLeft = Number.parseFloat(style.paddingLeft) || 0;
const paddingRight = Number.parseFloat(style.paddingRight) || 0;
return {
borderLeft: Number.parseFloat(style.borderLeftWidth) || 0,
borderTop: Number.parseFloat(style.borderTopWidth) || 0,
paddingBottom: Number.parseFloat(style.paddingBottom) || 0,
paddingLeft: Number.parseFloat(style.paddingLeft) || 0,
paddingRight: Number.parseFloat(style.paddingRight) || 0,
paddingTop: Number.parseFloat(style.paddingTop) || 0,
};
}
export function getElementContentBoxWidth(element: HTMLElement): number {
const { paddingLeft, paddingRight } = getElementContentBoxInsets(element);
return Math.max(0, element.clientWidth - paddingLeft - paddingRight);
}
export function getElementContentBoxClientRect(element: HTMLElement): DOMRect {
export function getElementContentBoxClientRect(
element: HTMLElement,
insets: ElementContentBoxInsets = getElementContentBoxInsets(element),
): 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;
const {
borderLeft,
borderTop,
paddingBottom,
paddingLeft,
paddingRight,
paddingTop,
} = insets;
return new DOMRect(
rect.left + borderLeft + paddingLeft,