Skip to content

Commit 3096f83

Browse files
committed
enhance(repo): fix overflow and scrolling on search results in sidebar search
1 parent 14f4452 commit 3096f83

File tree

3 files changed

+92
-29
lines changed

3 files changed

+92
-29
lines changed

templates/repo/view_file_tree.tmpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
</div>
1313

1414
{{/* TODO: Dynamically move components such as refSelector and createPR here */}}
15-
<div id="view-file-tree" class="tw-overflow-auto tw-h-full is-loading"
15+
<div id="view-file-tree" class="tw-overflow-y-auto tw-overflow-x-visible tw-h-full is-loading"
1616
data-repo-link="{{.RepoLink}}"
1717
data-tree-path="{{$.TreePath}}"
1818
data-current-ref-name-sub-url="{{.RefTypeNameSubURL}}"

web_src/css/repo/home.css

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@
6363
bottom: 0;
6464
height: 100%;
6565
overflow-y: hidden;
66+
overflow-x: visible;
67+
z-index: 10;
6668
}
6769

6870
.repo-view-content {

web_src/js/components/ViewFileTree.vue

Lines changed: 89 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
<script lang="ts" setup>
22
import ViewFileTreeItem from './ViewFileTreeItem.vue';
3-
import {onMounted, onUnmounted, useTemplateRef, ref, computed} from 'vue';
3+
import {onMounted, onUnmounted, useTemplateRef, ref, computed, watch, nextTick} from 'vue';
44
import {createViewFileTreeStore} from './ViewFileTreeStore.ts';
55
import {GET} from '../modules/fetch.ts';
66
import {filterRepoFilesWeighted} from '../features/repo-findfile.ts';
77
import {pathEscapeSegments} from '../utils/url.ts';
88
99
const elRoot = useTemplateRef('elRoot');
10+
const searchResults = useTemplateRef('searchResults');
1011
const searchQuery = ref('');
1112
const allFiles = ref<string[]>([]);
1213
const selectedIndex = ref(0);
@@ -39,9 +40,11 @@ const handleKeyDown = (e: KeyboardEvent) => {
3940
if (e.key === 'ArrowDown') {
4041
e.preventDefault();
4142
selectedIndex.value = Math.min(selectedIndex.value + 1, filteredFiles.value.length - 1);
43+
scrollSelectedIntoView();
4244
} else if (e.key === 'ArrowUp') {
4345
e.preventDefault();
4446
selectedIndex.value = Math.max(selectedIndex.value - 1, 0);
47+
scrollSelectedIntoView();
4548
} else if (e.key === 'Enter') {
4649
e.preventDefault();
4750
const selectedFile = filteredFiles.value[selectedIndex.value];
@@ -54,6 +57,32 @@ const handleKeyDown = (e: KeyboardEvent) => {
5457
}
5558
};
5659
60+
const scrollSelectedIntoView = () => {
61+
nextTick(() => {
62+
const resultsEl = searchResults.value;
63+
if (!resultsEl) return;
64+
65+
const selectedEl = resultsEl.querySelector('.file-tree-search-result-item.selected');
66+
if (selectedEl) {
67+
selectedEl.scrollIntoView({block: 'nearest', behavior: 'smooth'});
68+
}
69+
});
70+
};
71+
72+
const handleClickOutside = (e: MouseEvent) => {
73+
if (!searchQuery.value) return;
74+
75+
const target = e.target as HTMLElement;
76+
const resultsEl = searchResults.value;
77+
78+
// Check if click is outside search input and results
79+
if (searchInputElement && !searchInputElement.contains(target) &&
80+
resultsEl && !resultsEl.contains(target)) {
81+
searchQuery.value = '';
82+
if (searchInputElement) searchInputElement.value = '';
83+
}
84+
};
85+
5786
onMounted(async () => {
5887
store.rootFiles = await store.loadChildren('', props.treePath);
5988
elRoot.value.closest('.is-loading')?.classList?.remove('is-loading');
@@ -72,17 +101,34 @@ onMounted(async () => {
72101
searchInputElement.addEventListener('keydown', handleKeyDown);
73102
}
74103
104+
// Add click outside listener
105+
document.addEventListener('click', handleClickOutside);
106+
75107
window.addEventListener('popstate', (e) => {
76108
store.selectedItem = e.state?.treePath || '';
77109
if (e.state?.url) store.loadViewContent(e.state.url);
78110
});
79111
});
80112
113+
// Position search results below the input
114+
watch(searchQuery, async () => {
115+
if (searchQuery.value && searchInputElement) {
116+
await nextTick();
117+
const resultsEl = searchResults.value;
118+
if (resultsEl) {
119+
const rect = searchInputElement.getBoundingClientRect();
120+
resultsEl.style.top = `${rect.bottom + 4}px`;
121+
resultsEl.style.left = `${rect.left}px`;
122+
}
123+
}
124+
});
125+
81126
onUnmounted(() => {
82127
if (searchInputElement) {
83128
searchInputElement.removeEventListener('input', handleSearchInput);
84129
searchInputElement.removeEventListener('keydown', handleKeyDown);
85130
}
131+
document.removeEventListener('click', handleClickOutside);
86132
});
87133
88134
function handleSearchResultClick(filePath: string) {
@@ -93,35 +139,42 @@ function handleSearchResultClick(filePath: string) {
93139
</script>
94140

95141
<template>
96-
<div ref="elRoot">
97-
<div v-if="searchQuery && filteredFiles.length > 0" class="file-tree-search-results">
98-
<div
99-
v-for="(result, idx) in filteredFiles"
100-
:key="result.matchResult.join('')"
101-
:class="['file-tree-search-result-item', {'selected': idx === selectedIndex}]"
102-
@click="handleSearchResultClick(result.matchResult.join(''))"
103-
@mouseenter="selectedIndex = idx"
104-
>
105-
<svg class="svg octicon-file" width="16" height="16" aria-hidden="true"><use href="#octicon-file"/></svg>
106-
<span class="file-tree-search-result-path">
107-
<span
108-
v-for="(part, index) in result.matchResult"
109-
:key="index"
110-
:class="{'search-match': index % 2 === 1}"
111-
>{{ part }}</span>
112-
</span>
142+
<div ref="elRoot" class="file-tree-root">
143+
<Teleport to="body">
144+
<div v-if="searchQuery && filteredFiles.length > 0" ref="searchResults" class="file-tree-search-results">
145+
<div
146+
v-for="(result, idx) in filteredFiles"
147+
:key="result.matchResult.join('')"
148+
:class="['file-tree-search-result-item', {'selected': idx === selectedIndex}]"
149+
@click="handleSearchResultClick(result.matchResult.join(''))"
150+
@mouseenter="selectedIndex = idx"
151+
:title="result.matchResult.join('')"
152+
>
153+
<svg class="svg octicon-file" width="16" height="16" aria-hidden="true"><use href="#octicon-file"/></svg>
154+
<span class="file-tree-search-result-path">
155+
<span
156+
v-for="(part, index) in result.matchResult"
157+
:key="index"
158+
:class="{'search-match': index % 2 === 1}"
159+
>{{ part }}</span>
160+
</span>
161+
</div>
113162
</div>
114-
</div>
115-
<div v-else-if="searchQuery && filteredFiles.length === 0" class="file-tree-search-no-results">
116-
No matching file found
117-
</div>
118-
<div v-else class="view-file-tree-items">
163+
<div v-if="searchQuery && filteredFiles.length === 0" class="file-tree-search-no-results">
164+
No matching file found
165+
</div>
166+
</Teleport>
167+
<div class="view-file-tree-items">
119168
<ViewFileTreeItem v-for="item in store.rootFiles" :key="item.name" :item="item" :store="store"/>
120169
</div>
121170
</div>
122171
</template>
123172

124173
<style scoped>
174+
.file-tree-root {
175+
position: relative;
176+
}
177+
125178
.view-file-tree-items {
126179
display: flex;
127180
flex-direction: column;
@@ -130,27 +183,36 @@ function handleSearchResultClick(filePath: string) {
130183
}
131184
132185
.file-tree-search-results {
186+
position: fixed;
133187
display: flex;
134188
flex-direction: column;
135-
margin: 0 0.5rem 0.5rem;
136189
max-height: 400px;
137190
overflow-y: auto;
138191
background: var(--color-box-body);
139192
border: 1px solid var(--color-secondary);
140193
border-radius: 6px;
141194
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
195+
min-width: 300px;
196+
width: max-content;
197+
max-width: 600px;
198+
z-index: 99999;
142199
}
143200
144201
.file-tree-search-result-item {
145202
display: flex;
146-
align-items: center;
203+
align-items: flex-start;
147204
gap: 0.5rem;
148205
padding: 0.5rem 0.75rem;
149206
cursor: pointer;
150207
transition: background-color 0.1s;
151208
border-bottom: 1px solid var(--color-secondary);
152209
}
153210
211+
.file-tree-search-result-item svg {
212+
flex-shrink: 0;
213+
margin-top: 0.125rem;
214+
}
215+
154216
.file-tree-search-result-item:last-child {
155217
border-bottom: none;
156218
}
@@ -162,10 +224,9 @@ function handleSearchResultClick(filePath: string) {
162224
163225
.file-tree-search-result-path {
164226
flex: 1;
165-
overflow: hidden;
166-
text-overflow: ellipsis;
167-
white-space: nowrap;
168227
font-size: 14px;
228+
word-break: break-all;
229+
overflow-wrap: break-word;
169230
}
170231
171232
.search-match {

0 commit comments

Comments
 (0)