Skip to content

Commit 457f17f

Browse files
committed
enhance(repo): fix overflow and scrolling on search results in sidebar search
1 parent 332a24c commit 457f17f

File tree

4 files changed

+99
-30
lines changed

4 files changed

+99
-30
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/file-actions.css

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
/* Repository file actions dropdown and centered content */
2-
.repo-file-actions-dropdown .menu {
2+
.ui.dropdown.repo-add-file > .menu {
3+
margin-top: 4px !important;
4+
}
5+
6+
.ui.dropdown.repo-file-actions-dropdown > .menu {
7+
margin-top: 4px !important;
38
min-width: 200px;
49
}
510

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: 90 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
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';
8+
import {svg} from '../svg.ts';
89
910
const elRoot = useTemplateRef('elRoot');
11+
const searchResults = useTemplateRef('searchResults');
1012
const searchQuery = ref('');
1113
const allFiles = ref<string[]>([]);
1214
const selectedIndex = ref(0);
@@ -39,9 +41,11 @@ const handleKeyDown = (e: KeyboardEvent) => {
3941
if (e.key === 'ArrowDown') {
4042
e.preventDefault();
4143
selectedIndex.value = Math.min(selectedIndex.value + 1, filteredFiles.value.length - 1);
44+
scrollSelectedIntoView();
4245
} else if (e.key === 'ArrowUp') {
4346
e.preventDefault();
4447
selectedIndex.value = Math.max(selectedIndex.value - 1, 0);
48+
scrollSelectedIntoView();
4549
} else if (e.key === 'Enter') {
4650
e.preventDefault();
4751
const selectedFile = filteredFiles.value[selectedIndex.value];
@@ -54,6 +58,32 @@ const handleKeyDown = (e: KeyboardEvent) => {
5458
}
5559
};
5660
61+
const scrollSelectedIntoView = () => {
62+
nextTick(() => {
63+
const resultsEl = searchResults.value;
64+
if (!resultsEl) return;
65+
66+
const selectedEl = resultsEl.querySelector('.file-tree-search-result-item.selected');
67+
if (selectedEl) {
68+
selectedEl.scrollIntoView({block: 'nearest', behavior: 'smooth'});
69+
}
70+
});
71+
};
72+
73+
const handleClickOutside = (e: MouseEvent) => {
74+
if (!searchQuery.value) return;
75+
76+
const target = e.target as HTMLElement;
77+
const resultsEl = searchResults.value;
78+
79+
// Check if click is outside search input and results
80+
if (searchInputElement && !searchInputElement.contains(target) &&
81+
resultsEl && !resultsEl.contains(target)) {
82+
searchQuery.value = '';
83+
if (searchInputElement) searchInputElement.value = '';
84+
}
85+
};
86+
5787
onMounted(async () => {
5888
store.rootFiles = await store.loadChildren('', props.treePath);
5989
elRoot.value.closest('.is-loading')?.classList?.remove('is-loading');
@@ -72,17 +102,34 @@ onMounted(async () => {
72102
searchInputElement.addEventListener('keydown', handleKeyDown);
73103
}
74104
105+
// Add click outside listener
106+
document.addEventListener('click', handleClickOutside);
107+
75108
window.addEventListener('popstate', (e) => {
76109
store.selectedItem = e.state?.treePath || '';
77110
if (e.state?.url) store.loadViewContent(e.state.url);
78111
});
79112
});
80113
114+
// Position search results below the input
115+
watch(searchQuery, async () => {
116+
if (searchQuery.value && searchInputElement) {
117+
await nextTick();
118+
const resultsEl = searchResults.value;
119+
if (resultsEl) {
120+
const rect = searchInputElement.getBoundingClientRect();
121+
resultsEl.style.top = `${rect.bottom + 4}px`;
122+
resultsEl.style.left = `${rect.left}px`;
123+
}
124+
}
125+
});
126+
81127
onUnmounted(() => {
82128
if (searchInputElement) {
83129
searchInputElement.removeEventListener('input', handleSearchInput);
84130
searchInputElement.removeEventListener('keydown', handleKeyDown);
85131
}
132+
document.removeEventListener('click', handleClickOutside);
86133
});
87134
88135
function handleSearchResultClick(filePath: string) {
@@ -93,35 +140,42 @@ function handleSearchResultClick(filePath: string) {
93140
</script>
94141

95142
<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>
143+
<div ref="elRoot" class="file-tree-root">
144+
<Teleport to="body">
145+
<div v-if="searchQuery && filteredFiles.length > 0" ref="searchResults" class="file-tree-search-results">
146+
<div
147+
v-for="(result, idx) in filteredFiles"
148+
:key="result.matchResult.join('')"
149+
:class="['file-tree-search-result-item', {'selected': idx === selectedIndex}]"
150+
@click="handleSearchResultClick(result.matchResult.join(''))"
151+
@mouseenter="selectedIndex = idx"
152+
:title="result.matchResult.join('')"
153+
>
154+
<span v-html="svg('octicon-file', 16)"></span>
155+
<span class="file-tree-search-result-path">
156+
<span
157+
v-for="(part, index) in result.matchResult"
158+
:key="index"
159+
:class="{'search-match': index % 2 === 1}"
160+
>{{ part }}</span>
161+
</span>
162+
</div>
113163
</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">
164+
<div v-if="searchQuery && filteredFiles.length === 0" class="file-tree-search-no-results">
165+
No matching file found
166+
</div>
167+
</Teleport>
168+
<div class="view-file-tree-items">
119169
<ViewFileTreeItem v-for="item in store.rootFiles" :key="item.name" :item="item" :store="store"/>
120170
</div>
121171
</div>
122172
</template>
123173

124174
<style scoped>
175+
.file-tree-root {
176+
position: relative;
177+
}
178+
125179
.view-file-tree-items {
126180
display: flex;
127181
flex-direction: column;
@@ -130,27 +184,36 @@ function handleSearchResultClick(filePath: string) {
130184
}
131185
132186
.file-tree-search-results {
187+
position: fixed;
133188
display: flex;
134189
flex-direction: column;
135-
margin: 0 0.5rem 0.5rem;
136190
max-height: 400px;
137191
overflow-y: auto;
138192
background: var(--color-box-body);
139193
border: 1px solid var(--color-secondary);
140194
border-radius: 6px;
141195
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
196+
min-width: 300px;
197+
width: max-content;
198+
max-width: 600px;
199+
z-index: 99999;
142200
}
143201
144202
.file-tree-search-result-item {
145203
display: flex;
146-
align-items: center;
204+
align-items: flex-start;
147205
gap: 0.5rem;
148206
padding: 0.5rem 0.75rem;
149207
cursor: pointer;
150208
transition: background-color 0.1s;
151209
border-bottom: 1px solid var(--color-secondary);
152210
}
153211
212+
.file-tree-search-result-item svg {
213+
flex-shrink: 0;
214+
margin-top: 0.125rem;
215+
}
216+
154217
.file-tree-search-result-item:last-child {
155218
border-bottom: none;
156219
}
@@ -162,10 +225,9 @@ function handleSearchResultClick(filePath: string) {
162225
163226
.file-tree-search-result-path {
164227
flex: 1;
165-
overflow: hidden;
166-
text-overflow: ellipsis;
167-
white-space: nowrap;
168228
font-size: 14px;
229+
word-break: break-all;
230+
overflow-wrap: break-word;
169231
}
170232
171233
.search-match {

0 commit comments

Comments
 (0)