Skip to content

Commit c124d19

Browse files
committed
Fix file list multiselect and keyboard cursor interaction
- fix dated file list logic - meta + up/down to move cursor only - replace `onKeyDown` with simpler `onAction` focusables - add a `use:focusable` to codegen page - file list cursor follows selection - `focusParent()` can traverse all ancestors
1 parent 19ae345 commit c124d19

File tree

7 files changed

+119
-68
lines changed

7 files changed

+119
-68
lines changed

apps/desktop/src/components/BranchHeader.svelte

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,7 @@
5858
<div
5959
class="header-wrapper"
6060
use:focusable={{
61-
onKeydown: (e) => {
62-
if (e.key === 'Enter' || e.key === ' ') {
63-
e.stopPropagation();
64-
onclick?.();
65-
}
66-
},
61+
onAction: () => onclick?.(),
6762
onActive: (value) => (active = value)
6863
}}
6964
>

apps/desktop/src/components/CommitRow.svelte

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -100,15 +100,7 @@
100100
class:disabled
101101
{onclick}
102102
use:focusable={{
103-
onKeydown: (e) => {
104-
if (disabled) return false;
105-
106-
if (e.key === 'Enter' || e.key === ' ') {
107-
e.stopPropagation();
108-
onclick?.();
109-
return true;
110-
}
111-
}
103+
onAction: () => onclick?.()
112104
}}
113105
>
114106
{#if selected}

apps/desktop/src/components/FileList.svelte

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import { chunk } from '$lib/utils/array';
2020
import { inject, injectOptional } from '@gitbutler/core/context';
2121
import { FileListItem } from '@gitbutler/ui';
22+
import { FOCUS_MANAGER } from '@gitbutler/ui/focus/focusManager';
2223
import { focusable } from '@gitbutler/ui/focus/focusable';
2324
2425
import type { ConflictEntriesObj } from '$lib/files/conflicts';
@@ -52,6 +53,7 @@
5253
}: Props = $props();
5354
5455
const idSelection = inject(ID_SELECTION);
56+
const focusManager = inject(FOCUS_MANAGER);
5557
const aiService = inject(AI_SERVICE);
5658
const actionService = inject(ACTION_SERVICE);
5759
const modeService = injectOptional(MODE_SERVICE, undefined);
@@ -91,6 +93,7 @@
9193
const aiGenEnabled = $derived(projectAiGenEnabled(projectId));
9294
9395
const canUseGBAI = $derived(aiGenEnabled && aiConfigurationValid);
96+
const selectedFileIds = $derived(idSelection.values(selectionId));
9497
9598
$effect(() => {
9699
aiService.validateGitButlerAPIConfiguration().then((value) => {
@@ -177,7 +180,7 @@
177180
function handleKeyDown(change: TreeChange, idx: number, e: KeyboardEvent) {
178181
if (e.key === 'Enter' || e.key === ' ' || e.key === 'l') {
179182
e.stopPropagation();
180-
selectFilesInList(e, change, changes, idSelection, true, idx, selectionId);
183+
selectFilesInList(e, change, changes, idSelection, selectedFileIds, true, idx, selectionId);
181184
onselect?.();
182185
return true;
183186
}
@@ -194,25 +197,26 @@
194197
return;
195198
}
196199
197-
// If we want to keep the behavior where focus can change while
198-
// not automatically selecting the item, then we should remove
199-
// that code from `updateSelection` rather than checkinf for
200-
// modifier keys here.
201-
if (e.shiftKey || e.metaKey) {
200+
if (!e.metaKey) {
202201
updateSelection({
203202
allowMultiple: true,
204203
metaKey: e.metaKey,
205204
shiftKey: e.shiftKey,
206205
key: e.key,
207206
targetElement: e.currentTarget as HTMLElement,
208207
files: changes,
209-
selectedFileIds: idSelection.values(selectionId),
208+
selectedFileIds,
210209
fileIdSelection: idSelection,
211210
selectionId: selectionId,
212211
preventDefault: () => e.preventDefault()
213212
});
213+
return true;
214214
}
215215
}
216+
const lastAdded = $derived(idSelection.getById(selectionId).lastAdded);
217+
$effect(() => {
218+
if ($lastAdded) focusManager.focusNthSibling($lastAdded.index);
219+
});
216220
</script>
217221

218222
{#snippet fileTemplate(change: TreeChange, idx: number, depth: number = 0)}
@@ -231,10 +235,10 @@
231235
draggable={draggableFiles}
232236
executable={!!isExecutable}
233237
showCheckbox={showCheckboxes}
234-
focusableOpts={{ onKeydown: (e) => handleKeyDown(change, idx, e) }}
238+
focusableOpts={{ onKeydown: (e) => handleKeyDown(change, idx, e), autoAction: true }}
235239
onclick={(e) => {
236240
e.stopPropagation();
237-
selectFilesInList(e, change, changes, idSelection, true, idx, selectionId);
241+
selectFilesInList(e, change, changes, idSelection, selectedFileIds, true, idx, selectionId);
238242
onselect?.();
239243
}}
240244
{conflictEntries}

apps/desktop/src/components/SnapshotCard.svelte

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -230,13 +230,7 @@
230230
{#each files as file, idx}
231231
<div
232232
use:focusable={{
233-
onKeydown: (e) => {
234-
if (e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowRight') {
235-
onDiffClick(file.path);
236-
e.stopPropagation();
237-
return true;
238-
}
239-
}
233+
onAction: () => onDiffClick(file.path)
240234
}}
241235
>
242236
<FileListItem

apps/desktop/src/components/codegen/CodegenSidebarEntry.svelte

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script lang="ts">
22
import { Badge, Icon, TimeAgo, Tooltip, InfoButton } from '@gitbutler/ui';
3+
import { focusable } from '@gitbutler/ui/focus/focusable';
34
import { slide } from 'svelte/transition';
45
import type { ClaudeStatus } from '$lib/codegen/types';
56
import type { Snippet } from 'svelte';
@@ -35,7 +36,7 @@
3536
let isOpen = $state(false);
3637
</script>
3738

38-
<div class="codegen-entry-wrapper">
39+
<div class="codegen-entry-wrapper" use:focusable>
3940
<div class="codegen-entry">
4041
<button class="codegen-entry-header" class:selected type="button" {onclick}>
4142
{#if selected}

apps/desktop/src/lib/selection/idSelectionUtils.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,14 @@ function getNextFile(files: TreeChange[], currentId: string): TreeChange | undef
1919
const fileIndex = files.findIndex((f) => f.path === currentId);
2020
if (fileIndex === -1) return undefined;
2121

22-
const nextFileIndex = (fileIndex + 1) % files.length;
22+
const nextFileIndex = fileIndex + 1;
2323
return files[nextFileIndex];
2424
}
2525

2626
function getPreviousFile(files: TreeChange[], currentId: string): TreeChange | undefined {
2727
const fileIndex = files.findIndex((f) => f.path === currentId);
2828
if (fileIndex === -1) return undefined;
29-
const previousFileIndex = (fileIndex - 1 + files.length) % files.length;
29+
const previousFileIndex = fileIndex - 1;
3030
return files[previousFileIndex];
3131
}
3232

@@ -96,11 +96,6 @@ export function updateSelection({
9696
) {
9797
const file = getFileFunc?.(files, id) ?? getFile(files, id);
9898
if (file) {
99-
// if file is already selected, do nothing
100-
if (selectedFileIds.find((f) => f.path === file.path)) {
101-
return;
102-
}
103-
10499
const fileIndex = files.findIndex((f) => f.path === file.path);
105100
if (fileIndex === -1) return; // should never happen
106101
fileIdSelection.add(file.path, selectionId, fileIndex);
@@ -192,6 +187,7 @@ export function selectFilesInList(
192187
change: TreeChange,
193188
sortedFiles: TreeChange[],
194189
idSelection: IdSelection,
190+
selectedFileIds: SelectedFile[],
195191
allowMultiple: boolean,
196192
index: number,
197193
selectionId: SelectionId,
@@ -205,6 +201,11 @@ export function selectFilesInList(
205201
if (e.ctrlKey || e.metaKey) {
206202
if (isAlreadySelected) {
207203
idSelection.remove(change.path, selectionId);
204+
selectedFileIds.splice(selectedFileIds.findIndex((f) => f.path === change.path));
205+
const previous = selectedFileIds.at(-1);
206+
if (previous) {
207+
idSelection.add(previous.path, selectionId, selectedFileIds.length - 1);
208+
}
208209
} else {
209210
idSelection.add(change.path, selectionId, index);
210211
}

0 commit comments

Comments
 (0)