Refine (regression): grid navigation is broken

This commit is contained in:
aleksey-hoffman committed 2026-06-30 13:53:56 +03:00
1 parent 195ad1cf4e
commit a3ead6ff10
6 files changed
+163 -8

No files matched your search

@@ -6,6 +6,7 @@ import { describe, expect, it } from 'vitest';
import type { DirEntry } from '@/types/dir-entry';
import {
getFileBrowserGridEntryOrder,
getFileBrowserVisualEntryOrder,
groupFileBrowserEntries,
} from '../file-browser-entry-groups';
import { sortFileBrowserEntries, type DirSizesStore } from '../utils/file-browser-sort';
@@ -119,4 +120,26 @@ describe('file browser entry groups', () => {
'notes.txt',
]);
});
it('returns flat order for list layout and grid order for grid layout', () => {
const entries = [
createEntry('photo.png', { ext: 'png' }),
createEntry('clips', {
is_dir: true,
is_file: false,
}),
createEntry('notes.txt', { ext: 'txt' }),
];
expect(getFileBrowserVisualEntryOrder(entries, 'list').map(entry => entry.name)).toEqual([
'photo.png',
'clips',
'notes.txt',
]);
expect(getFileBrowserVisualEntryOrder(entries, 'grid').map(entry => entry.name)).toEqual([
'clips',
'photo.png',
'notes.txt',
]);
});
});
@@ -0,0 +1,118 @@
// 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 { ref } from 'vue';
import { describe, expect, it, vi } from 'vitest';
import type { DirEntry } from '@/types/dir-entry';
import { useFileBrowserKeyboardNavigation } from '../use-file-browser-keyboard-navigation';
function createEntry(name: string, options: Partial<DirEntry> = {}): DirEntry {
const isDirectory = options.is_dir ?? false;
return {
name,
path: `/${name}`,
is_dir: isDirectory,
is_file: !isDirectory,
is_hidden: false,
is_symlink: false,
size: 100,
created_time: 0,
modified_time: 0,
accessed_time: 0,
item_count: isDirectory ? 0 : null,
ext: null,
mime: null,
...options,
};
}
function createKeyboardNavigationHarness(options: {
entries: DirEntry[];
layout?: () => 'list' | 'grid' | undefined;
initialSelection?: DirEntry;
}) {
const entries = ref(options.entries);
const selectedEntries = ref<DirEntry[]>(
options.initialSelection ? [options.initialSelection] : [],
);
const selectEntryByPath = vi.fn((path: string) => {
const entry = entries.value.find(item => item.path === path);
if (!entry) {
return false;
}
selectedEntries.value = [entry];
return true;
});
const navigation = useFileBrowserKeyboardNavigation({
entries,
selectedEntries,
layout: options.layout ?? (() => 'grid'),
selectEntryByPath,
scrollToPath: vi.fn(async () => true),
goBack: vi.fn(),
openEntry: vi.fn(),
entriesContainerRef: ref(null),
});
return {
navigation,
selectedEntries,
selectEntryByPath,
};
}
describe('useFileBrowserKeyboardNavigation', () => {
it('moves through grid visual order with arrow left and right', () => {
const clips = createEntry('clips', {
is_dir: true,
is_file: false,
});
const photo = createEntry('photo.png', {
ext: 'png',
mime: 'image/png',
});
const movie = createEntry('movie.mp4', {
ext: 'mp4',
mime: 'video/mp4',
});
const notes = createEntry('notes.txt', {
ext: 'txt',
mime: 'text/plain',
});
const { navigation, selectedEntries } = createKeyboardNavigationHarness({
entries: [clips, movie, notes, photo],
initialSelection: clips,
});
navigation.navigateRight();
expect(selectedEntries.value.map(entry => entry.name)).toEqual(['photo.png']);
navigation.navigateRight();
expect(selectedEntries.value.map(entry => entry.name)).toEqual(['movie.mp4']);
navigation.navigateRight();
expect(selectedEntries.value.map(entry => entry.name)).toEqual(['notes.txt']);
navigation.navigateLeft();
expect(selectedEntries.value.map(entry => entry.name)).toEqual(['movie.mp4']);
});
it('uses flat entry order in list layout', () => {
const first = createEntry('alpha.txt', { ext: 'txt' });
const second = createEntry('beta.png', { ext: 'png', mime: 'image/png' });
const { navigation, selectedEntries } = createKeyboardNavigationHarness({
entries: [first, second],
layout: () => 'list',
initialSelection: first,
});
navigation.navigateRight();
expect(selectedEntries.value.map(entry => entry.name)).toEqual(['beta.png']);
});
});
@@ -4,7 +4,7 @@
import { nextTick, type Ref } from 'vue';
import type { DirEntry } from '@/types/dir-entry';
import { getFileBrowserGridEntryOrder } from '../file-browser-entry-groups';
import { getFileBrowserVisualEntryOrder } from '../file-browser-entry-groups';
const ROW_TOLERANCE_PX = 30;
const OVERLAP_TOLERANCE_PX = 2;
@@ -53,11 +53,7 @@ export function useFileBrowserKeyboardNavigation(options: {
}
function getNavigationEntries(): DirEntry[] {
if (options.layout() === 'grid') {
return getFileBrowserGridEntryOrder(options.entries.value);
}
return options.entries.value;
return getFileBrowserVisualEntryOrder(options.entries.value, options.layout());
}
async function selectAndFocusEntry(entry: DirEntry) {
@@ -34,6 +34,7 @@ import { usePlatformStore } from '@/stores/runtime/platform';
import { isContextMenuActionVisible } from '@/modules/navigator/components/file-browser/utils/context-menu-action-visibility';
import { resolveNavigableItemTarget } from '@/utils/resolve-navigable-item-target';
import type { CreateLinksResult, LinkCreationKind } from '@/utils/link-operations';
import { getFileBrowserVisualEntryOrder } from '../file-browser-entry-groups';
export const FILE_BROWSER_REVEAL_STALE_FOCUS_GUARD_MS = 500;
@@ -44,6 +45,7 @@ export function useFileBrowserSelection(
onOpen: (entry: DirEntry) => void,
onOpenProperties: (entries: DirEntry[]) => void,
onRefresh: () => void,
layout?: () => 'list' | 'grid' | undefined,
) {
const { t } = useI18n();
const platformStore = usePlatformStore();
@@ -158,12 +160,16 @@ export function useFileBrowserSelection(
return selectedEntries.value.some(selectedEntry => selectedEntry.path === entry.path);
}
function getOrderedEntries(): DirEntry[] {
return getFileBrowserVisualEntryOrder(entriesRef.value, layout?.());
}
function getEntryIndex(entry: DirEntry): number {
return entriesRef.value.findIndex(item => item.path === entry.path);
return getOrderedEntries().findIndex(item => item.path === entry.path);
}
function selectRange(fromEntry: DirEntry, toEntry: DirEntry) {
const entries = entriesRef.value;
const entries = getOrderedEntries();
let startIndex = getEntryIndex(fromEntry);
let endIndex = getEntryIndex(toEntry);
@@ -288,6 +288,7 @@ export function useFileBrowser(options: UseFileBrowserOptions) {
openPropertiesForSelection(entries);
},
dataSource.silentRefresh,
options.layout,
);
const virtualLayout = useFileBrowserVirtualLayout({
@@ -56,3 +56,14 @@ export function getFileBrowserGridEntryOrder(entries: readonly DirEntry[]): DirE
...groupedEntries.others,
];
}
export function getFileBrowserVisualEntryOrder(
entries: readonly DirEntry[],
layout: 'list' | 'grid' | undefined,
): DirEntry[] {
if (layout === 'grid') {
return getFileBrowserGridEntryOrder(entries);
}
return [...entries];
}