Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: File key navigation and selection #4876

Merged
merged 1 commit into from
Sep 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 16 additions & 24 deletions apps/desktop/src/lib/file/BranchFilesList.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import { copyToClipboard } from '$lib/utils/clipboard';
import { getContext } from '$lib/utils/context';
import { selectFilesInList } from '$lib/utils/selectFilesInList';
import { maybeMoveSelection } from '$lib/utils/selection';
import { updateSelection } from '$lib/utils/selection';
import { getCommitStore } from '$lib/vbranches/contexts';
import { FileIdSelection, stringifyFileKey } from '$lib/vbranches/fileIdSelection';
import { sortLikeFileTree } from '$lib/vbranches/filetree';
Expand Down Expand Up @@ -81,6 +81,21 @@
loadMore();
}}
role="listbox"
onkeydown={(e) => {
e.preventDefault();
updateSelection(
{
allowMultiple,
shiftKey: e.shiftKey,
key: e.key,
targetElement: e.currentTarget as HTMLElement,
files: displayedFiles,
selectedFileIds: $fileIdSelection,
fileIdSelection,
commitId: $commit?.id
}
);
}}
>
{#each displayedFiles as file (file.id)}
<FileListItem
Expand All @@ -92,29 +107,6 @@
onclick={(e) => {
selectFilesInList(e, file, fileIdSelection, displayedFiles, allowMultiple, $commit);
}}
onkeydown={(e) => {
e.preventDefault();
maybeMoveSelection(
{
allowMultiple,
shiftKey: e.shiftKey,
key: e.key,
targetElement: e.currentTarget as HTMLElement,
file,
files: displayedFiles,
selectedFileIds: $fileIdSelection,
fileIdSelection,
commitId: $commit?.id
}
);

if (e.key === 'Escape') {
fileIdSelection.clear();

const targetEl = e.target as HTMLElement;
targetEl.blur();
}
}}
/>
{/each}
</LazyloadContainer>
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/lib/file/FileListItem.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
showCheckbox: boolean;
readonly: boolean;
onclick: (e: MouseEvent) => void;
onkeydown: (e: KeyboardEvent) => void;
onkeydown?: (e: KeyboardEvent) => void;
}

const { file, isUnapplied, selected, showCheckbox, readonly, onclick, onkeydown }: Props =
Expand Down
5 changes: 3 additions & 2 deletions apps/desktop/src/lib/shared/LazyloadContainer.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
minTriggerCount: number;
role?: AriaRole | undefined | null;
ontrigger: (lastChild: Element) => void;
onkeydown?: (e: KeyboardEvent) => void;
}

let { children, minTriggerCount, role, ontrigger }: Props = $props();
let { children, minTriggerCount, role, ontrigger, onkeydown }: Props = $props();

let lazyContainerEl: HTMLDivElement;

Expand Down Expand Up @@ -47,7 +48,7 @@
});
</script>

<div class="lazy-container" {role} bind:this={lazyContainerEl}>
<div class="lazy-container" {role} bind:this={lazyContainerEl} {onkeydown}>
{@render children()}
</div>

Expand Down
5 changes: 4 additions & 1 deletion apps/desktop/src/lib/utils/getSelectionDirection.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
export function getSelectionDirection(firstFileIndex: number, lastFileIndex: number) {
export function getSelectionDirection(
firstFileIndex: number,
lastFileIndex: number
): 'up' | 'down' {
// detect the direction of the selection
const selectionDirection = lastFileIndex < firstFileIndex ? 'down' : 'up';

Expand Down
97 changes: 62 additions & 35 deletions apps/desktop/src/lib/utils/selection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,79 +2,104 @@
* Shared helper functions for manipulating selected files with keyboard.
*/
import { getSelectionDirection } from './getSelectionDirection';
import { KeyName } from './hotkeys';
import { stringifyFileKey, unstringifyFileKey } from '$lib/vbranches/fileIdSelection';
import type { FileIdSelection } from '$lib/vbranches/fileIdSelection';
import type { AnyFile } from '$lib/vbranches/types';

export function getNextFile(files: AnyFile[], currentId: string): AnyFile | undefined {
function getFile(files: AnyFile[], id: string): AnyFile | undefined {
return files.find((f) => f.id === id);
}

function getNextFile(files: AnyFile[], currentId: string): AnyFile | undefined {
const fileIndex = files.findIndex((f) => f.id === currentId);
return fileIndex !== -1 && fileIndex + 1 < files.length ? files[fileIndex + 1] : undefined;
}

export function getPreviousFile(files: AnyFile[], currentId: string): AnyFile | undefined {
function getPreviousFile(files: AnyFile[], currentId: string): AnyFile | undefined {
const fileIndex = files.findIndex((f) => f.id === currentId);
return fileIndex > 0 ? files[fileIndex - 1] : undefined;
}

interface MoveSelectionParams {
function getTopFile(files: AnyFile[], selectedFileIds: string[]): AnyFile | undefined {
for (const file of files) {
if (selectedFileIds.includes(stringifyFileKey(file.id))) {
return file;
}
}
return undefined;
}

function getBottomFile(files: AnyFile[], selectedFileIds: string[]): AnyFile | undefined {
for (let i = files.length - 1; i >= 0; i--) {
const file = files[i];
if (selectedFileIds.includes(stringifyFileKey(file!.id))) {
return file;
}
}
return undefined;
}

interface UpdateSelectionParams {
allowMultiple: boolean;
shiftKey: boolean;
key: string;
targetElement: HTMLElement;
file: AnyFile;
files: AnyFile[];
selectedFileIds: string[];
fileIdSelection: FileIdSelection;
commitId?: string;
}

export function maybeMoveSelection({
export function updateSelection({
allowMultiple,
shiftKey,
key,
targetElement,
file,
files,
selectedFileIds,
fileIdSelection,
commitId
}: MoveSelectionParams) {
}: UpdateSelectionParams) {
if (!selectedFileIds[0] || selectedFileIds.length === 0) return;

const firstFileId = unstringifyFileKey(selectedFileIds[0]);
const lastFileId = unstringifyFileKey(selectedFileIds.at(-1)!);

const topFileId = getTopFile(files, selectedFileIds)?.id;
const bottomFileId = getBottomFile(files, selectedFileIds)?.id;

let selectionDirection = getSelectionDirection(
files.findIndex((f) => f.id === lastFileId),
files.findIndex((f) => f.id === firstFileId)
);

function getAndAddFile(
getFileFunc: (files: AnyFile[], id: string) => AnyFile | undefined,
id: string
id: string,
getFileFunc?: (files: AnyFile[], id: string) => AnyFile | undefined
) {
const file = getFileFunc(files, id);
const file = getFileFunc?.(files, id) ?? getFile(files, id);
if (file) {
// if file is already selected, do nothing

if (selectedFileIds.includes(stringifyFileKey(file.id, commitId))) return;

fileIdSelection.add(file.id, commitId);
}
}

function getAndClearAndAddFile(
getFileFunc: (files: AnyFile[], id: string) => AnyFile | undefined,
id: string
function getAndClearExcept(
id: string,
getFileFunc?: (files: AnyFile[], id: string) => AnyFile | undefined
) {
const file = getFileFunc(files, id);
const file = getFileFunc?.(files, id) ?? getFile(files, id);

if (file) {
fileIdSelection.clearExcept(file.id, commitId);
}
}

switch (key) {
case 'ArrowUp':
case KeyName.Up:
if (shiftKey && allowMultiple) {
// Handle case if only one file is selected
// we should update the selection direction
Expand All @@ -83,22 +108,21 @@ export function maybeMoveSelection({
} else if (selectionDirection === 'down') {
fileIdSelection.remove(lastFileId, commitId);
}
getAndAddFile(getPreviousFile, lastFileId);
getAndAddFile(lastFileId, getPreviousFile);
} else {
// focus previous file
const previousElement = targetElement.previousElementSibling as HTMLElement;
if (previousElement) previousElement.focus();

// Handle reset of selection
if (selectedFileIds.length > 1) {
getAndClearAndAddFile(getPreviousFile, lastFileId);
} else {
getAndClearAndAddFile(getPreviousFile, file.id);
if (selectedFileIds.length > 1 && topFileId !== undefined) {
getAndClearExcept(topFileId);
}

// Handle navigation
if (selectedFileIds.length === 1) {
getAndClearExcept(firstFileId, getPreviousFile);
}
}
break;

case 'ArrowDown':
case KeyName.Down:
if (shiftKey && allowMultiple) {
// Handle case if only one file is selected
// we should update the selection direction
Expand All @@ -108,19 +132,22 @@ export function maybeMoveSelection({
fileIdSelection.remove(lastFileId, commitId);
}

getAndAddFile(getNextFile, lastFileId);
getAndAddFile(lastFileId, getNextFile);
} else {
// focus next file
const nextElement = targetElement.nextElementSibling as HTMLElement;
if (nextElement) nextElement.focus();

// Handle reset of selection
if (selectedFileIds.length > 1) {
getAndClearAndAddFile(getNextFile, lastFileId);
} else {
getAndClearAndAddFile(getNextFile, file.id);
if (selectedFileIds.length > 1 && bottomFileId !== undefined) {
getAndClearExcept(bottomFileId);
}

// Handle navigation
if (selectedFileIds.length === 1) {
getAndClearExcept(firstFileId, getNextFile);
}
}
break;
case KeyName.Escape:
fileIdSelection.clear();
targetElement.blur();
break;
}
}