diff --git a/templates/repo/home_sidebar_top.tmpl b/templates/repo/home_sidebar_top.tmpl index edbf01db09..ba8b904c0a 100644 --- a/templates/repo/home_sidebar_top.tmpl +++ b/templates/repo/home_sidebar_top.tmpl @@ -1,7 +1,9 @@
-
- {{template "shared/search/button"}} +
+ + S + {{template "shared/search/button"}}
diff --git a/tests/e2e/repo-shortcuts.test.e2e.ts b/tests/e2e/repo-shortcuts.test.e2e.ts new file mode 100644 index 0000000000..1a05781b95 --- /dev/null +++ b/tests/e2e/repo-shortcuts.test.e2e.ts @@ -0,0 +1,146 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +import {test, expect} from '@playwright/test'; +import {login_user, load_logged_in_context} from './utils_e2e.ts'; + +test.beforeAll(async ({browser}, workerInfo) => { + await login_user(browser, workerInfo, 'user2'); +}); + +test.describe('Repository Keyboard Shortcuts', () => { + test('T key focuses file search input', async ({browser}, workerInfo) => { + const context = await load_logged_in_context(browser, workerInfo, 'user2'); + const page = await context.newPage(); + + // Navigate to a repository page with file listing + await page.goto('/user2/repo1'); + await page.waitForLoadState('networkidle'); // eslint-disable-line playwright/no-networkidle + + // Verify the file search input exists and has the keyboard hint + const fileSearchInput = page.locator('.repo-file-search-container input'); + await expect(fileSearchInput).toBeVisible(); + + // Verify the keyboard hint is visible + const kbdHint = page.locator('.repo-file-search-input-wrapper kbd'); + await expect(kbdHint).toBeVisible(); + await expect(kbdHint).toHaveText('T'); + + // Press T key to focus the file search input + await page.keyboard.press('t'); + + // Verify the input is focused + await expect(fileSearchInput).toBeFocused(); + }); + + test('T key does not trigger when typing in input', async ({browser}, workerInfo) => { + const context = await load_logged_in_context(browser, workerInfo, 'user2'); + const page = await context.newPage(); + + // Navigate to a repository page + await page.goto('/user2/repo1'); + await page.waitForLoadState('networkidle'); // eslint-disable-line playwright/no-networkidle + + // Focus on file search first + const fileSearchInput = page.locator('.repo-file-search-container input'); + await fileSearchInput.click(); + + // Type something including 't' + await page.keyboard.type('test'); + + // Verify the input still has focus and contains the typed text + await expect(fileSearchInput).toBeFocused(); + await expect(fileSearchInput).toHaveValue('test'); + }); + + test('S key focuses code search input on repo home', async ({browser}, workerInfo) => { + const context = await load_logged_in_context(browser, workerInfo, 'user2'); + const page = await context.newPage(); + + // Navigate to repo home page where code search is available + await page.goto('/user2/repo1'); + await page.waitForLoadState('networkidle'); // eslint-disable-line playwright/no-networkidle + + // The code search input is in the sidebar + const codeSearchInput = page.locator('.code-search-input'); + await expect(codeSearchInput).toBeVisible(); + + // Verify the keyboard hint is visible + const kbdHint = page.locator('.repo-code-search-input-wrapper .repo-search-shortcut-hint'); + await expect(kbdHint).toBeVisible(); + await expect(kbdHint).toHaveText('S'); + + // Press S key to focus the code search input + await page.keyboard.press('s'); + + // Verify the input is focused + await expect(codeSearchInput).toBeFocused(); + }); + + test('File search keyboard hint hides when input has value', async ({browser}, workerInfo) => { + const context = await load_logged_in_context(browser, workerInfo, 'user2'); + const page = await context.newPage(); + + // Navigate to a repository page + await page.goto('/user2/repo1'); + await page.waitForLoadState('networkidle'); // eslint-disable-line playwright/no-networkidle + + // Check file search kbd hint + const fileSearchInput = page.locator('.repo-file-search-container input'); + const fileKbdHint = page.locator('.repo-file-search-input-wrapper kbd'); + + // Initially the hint should be visible + await expect(fileKbdHint).toBeVisible(); + + // Focus and type in the file search + await fileSearchInput.click(); + await page.keyboard.type('test'); + + // The hint should now be hidden (Vue component handles this with v-show) + await expect(fileKbdHint).toBeHidden(); + }); + + test('Code search keyboard hint hides when input has value', async ({browser}, workerInfo) => { + const context = await load_logged_in_context(browser, workerInfo, 'user2'); + const page = await context.newPage(); + + // Navigate to a repository page + await page.goto('/user2/repo1'); + await page.waitForLoadState('networkidle'); // eslint-disable-line playwright/no-networkidle + + const codeSearchInput = page.locator('.code-search-input'); + await expect(codeSearchInput).toBeVisible(); + + const codeKbdHint = page.locator('.repo-code-search-input-wrapper .repo-search-shortcut-hint'); + + // Initially the hint should be visible + await expect(codeKbdHint).toBeVisible(); + + // Focus and type in the code search + await codeSearchInput.click(); + await page.keyboard.type('search'); + + // The hint should now be hidden + await expect(codeKbdHint).toBeHidden(); + }); + + test('Shortcuts do not trigger with modifier keys', async ({browser}, workerInfo) => { + const context = await load_logged_in_context(browser, workerInfo, 'user2'); + const page = await context.newPage(); + + // Navigate to a repository page + await page.goto('/user2/repo1'); + await page.waitForLoadState('networkidle'); // eslint-disable-line playwright/no-networkidle + + const fileSearchInput = page.locator('.repo-file-search-container input'); + + // Click somewhere else first to ensure nothing is focused + await page.locator('body').click(); + + // Press Ctrl+T (should not focus file search - this is typically "new tab" in browsers) + await page.keyboard.press('Control+t'); + + // The file search input should NOT be focused + await expect(fileSearchInput).not.toBeFocused(); + }); +}); diff --git a/web_src/css/repo.css b/web_src/css/repo.css index 83df3e5c29..b4caba9905 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -2064,3 +2064,42 @@ tbody.commit-list { .branch-selector-dropdown .scrolling.menu .loading-indicator { height: 4em; } + +/* Keyboard shortcut hint styles for repo search inputs */ +.repo-code-search-input-wrapper { + position: relative; +} + +.repo-code-search-input-wrapper input { + padding-right: 32px !important; +} + +.repo-search-shortcut-hint { + position: absolute; + right: 40px; /* account for the search button */ + 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; + z-index: 1; +} + +/* Override Fomantic UI action input styles for file search - need high specificity */ +.repo-file-search-input-wrapper.ui.input input, +.repo-file-search-input-wrapper.ui.input input:hover { + border-right: 1px solid var(--color-input-border) !important; + border-top-right-radius: 0.28571429rem !important; + border-bottom-right-radius: 0.28571429rem !important; +} + +.repo-file-search-input-wrapper.ui.input input:focus { + border-color: var(--color-primary) !important; +} diff --git a/web_src/js/components/RepoFileSearch.vue b/web_src/js/components/RepoFileSearch.vue index f0c63267bc..ddbeb4e958 100644 --- a/web_src/js/components/RepoFileSearch.vue +++ b/web_src/js/components/RepoFileSearch.vue @@ -23,6 +23,7 @@ const allFiles = ref([]); const selectedIndex = ref(0); const isLoadingFileList = ref(false); const hasLoadedFileList = ref(false); +const isInputFocused = ref(false); const showPopup = computed(() => searchQuery.value.length > 0); @@ -45,8 +46,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 +146,14 @@ watch([searchQuery, filteredFiles], async () => {