From 427954ba6ed3a553f75eb1d01fb2f4597e3eda1c Mon Sep 17 00:00:00 2001 From: Micah Kepe Date: Mon, 23 Feb 2026 01:20:56 -0800 Subject: [PATCH] Add keyboard shortcuts for repository file and code search (#36416) Resolves #36417: Add GitHub-like keyboard shortcuts for repository navigation: - Press `T` to focus the "Go to file" search input - Press `S` to focus the "Search code" input - Press `Escape` to clear and unfocus search inputs --------- Signed-off-by: Micah Kepe Signed-off-by: silverwind Signed-off-by: wxiaoguang Co-authored-by: silverwind Co-authored-by: wxiaoguang --- eslint.config.ts | 1 + templates/devtest/keyboard-shortcut.tmpl | 23 ++++++ templates/repo/home_sidebar_top.tmpl | 10 ++- web_src/css/index.css | 1 + web_src/css/modules/input.css | 3 +- web_src/css/modules/shortcut.css | 20 ++++++ web_src/js/components/RepoFileSearch.vue | 5 +- .../features/repo-settings-branches.test.ts | 1 - web_src/js/index-domready.ts | 2 + web_src/js/modules/shortcut.ts | 71 +++++++++++++++++++ 10 files changed, 130 insertions(+), 7 deletions(-) create mode 100644 templates/devtest/keyboard-shortcut.tmpl create mode 100644 web_src/css/modules/shortcut.css create mode 100644 web_src/js/modules/shortcut.ts diff --git a/eslint.config.ts b/eslint.config.ts index 28508c52a6..53edfc364d 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -961,6 +961,7 @@ export default defineConfig([ 'vitest/no-interpolation-in-snapshots': [0], 'vitest/no-large-snapshots': [0], 'vitest/no-mocks-import': [0], + 'vitest/no-importing-vitest-globals': [2], 'vitest/no-restricted-matchers': [0], 'vitest/no-restricted-vi-methods': [0], 'vitest/no-standalone-expect': [0], diff --git a/templates/devtest/keyboard-shortcut.tmpl b/templates/devtest/keyboard-shortcut.tmpl new file mode 100644 index 0000000000..4b55c0a1e9 --- /dev/null +++ b/templates/devtest/keyboard-shortcut.tmpl @@ -0,0 +1,23 @@ +{{template "devtest/devtest-header"}} +
+

Keyboard Shortcut

+ +
+
+ + S +
+
+ +
+
+
+ + T +
+ +
+
+
+ +{{template "devtest/devtest-footer"}} diff --git a/templates/repo/home_sidebar_top.tmpl b/templates/repo/home_sidebar_top.tmpl index edbf01db09..1560e054d8 100644 --- a/templates/repo/home_sidebar_top.tmpl +++ b/templates/repo/home_sidebar_top.tmpl @@ -1,7 +1,11 @@
-
-
- {{template "shared/search/button"}} + +
+
+ + S +
+ {{template "shared/search/button"}}
diff --git a/web_src/css/index.css b/web_src/css/index.css index c09d309149..c02651d520 100644 --- a/web_src/css/index.css +++ b/web_src/css/index.css @@ -20,6 +20,7 @@ @import "./modules/modal.css"; @import "./modules/tab.css"; @import "./modules/form.css"; +@import "./modules/shortcut.css"; @import "./modules/tippy.css"; @import "./modules/breadcrumb.css"; diff --git a/web_src/css/modules/input.css b/web_src/css/modules/input.css index d39377b4e1..abf2d21492 100644 --- a/web_src/css/modules/input.css +++ b/web_src/css/modules/input.css @@ -124,7 +124,8 @@ margin: 0; } -.ui.action.input:not([class*="left action"]) > input { +.ui.action.input:not([class*="left action"]) > input, +.ui.action.input:not([class*="left action"]) > .ui.input > input { border-top-right-radius: 0; border-bottom-right-radius: 0; border-right-color: transparent; diff --git a/web_src/css/modules/shortcut.css b/web_src/css/modules/shortcut.css new file mode 100644 index 0000000000..f8e62b8de5 --- /dev/null +++ b/web_src/css/modules/shortcut.css @@ -0,0 +1,20 @@ +.global-shortcut-wrapper { + position: relative; +} + +.global-shortcut-wrapper > kbd { + position: absolute; + right: 6px; + top: 50%; + transform: translateY(-50%); + display: inline-block; + padding: 2px 6px; + font-size: 11px; + line-height: 14px; + color: var(--color-text-light-2); + background-color: var(--color-box-body); + border: 1px solid var(--color-secondary); + border-radius: var(--border-radius); + box-shadow: inset 0 -1px 0 var(--color-secondary); + pointer-events: none; +} diff --git a/web_src/js/components/RepoFileSearch.vue b/web_src/js/components/RepoFileSearch.vue index f0c63267bc..4b006c9cde 100644 --- a/web_src/js/components/RepoFileSearch.vue +++ b/web_src/js/components/RepoFileSearch.vue @@ -45,8 +45,8 @@ const handleKeyDown = (e: KeyboardEvent) => { if (e.isComposing) return; if (e.key === 'Escape') { - e.preventDefault(); clearSearch(); + nextTick(() => refElemInput.value.blur()); return; } if (!searchQuery.value || filteredFiles.value.length === 0) return; @@ -145,12 +145,13 @@ watch([searchQuery, filteredFiles], async () => {