From 2ad19a714b6c28f7f6c364dc28168ab1485334f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Krela?= Date: Tue, 31 Mar 2026 08:30:34 +0200 Subject: [PATCH 01/13] Implemented filtering pull request diff files by file extension There is new button in pull request diff template, which opens dropbox containing list of all unique extensions that the diff contains. List is created with checkmarks that can be checked/unchecked to filter out files with specific extension. There is also "select all" and "deselect all" buttons. To apply the change and filter out the files user have to click on "Apply" button. If any extensions were filtered out, then button which opens the dropbox is outlined to signal some files were filtered out. --- options/locale/locale_en-US.json | 4 + templates/repo/diff/box.tmpl | 6 + .../js/components/DiffFileExtensionFilter.vue | 242 ++++++++++++++++++ .../js/features/repo-diff-extensionfilter.ts | 10 + web_src/js/features/repo-diff.ts | 2 + 5 files changed, 264 insertions(+) create mode 100644 web_src/js/components/DiffFileExtensionFilter.vue create mode 100644 web_src/js/features/repo-diff-extensionfilter.ts diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index 2ffa130751..429ea1eda1 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -1777,6 +1777,10 @@ "repo.pulls.select_commit_hold_shift_for_range": "Select commit. Hold Shift and click to select a range.", "repo.pulls.review_only_possible_for_full_diff": "Review is only possible when viewing the full diff", "repo.pulls.filter_changes_by_commit": "Filter by commit", + "repo.pulls.filter_by_file_extension": "Filter by file extension", + "repo.pulls.select_all_file_extensions": "Select all", + "repo.pulls.deselect_all_file_extensions": "Deselect all", + "repo.pulls.apply_file_extension_filter": "Apply", "repo.pulls.nothing_to_compare": "These branches are equal. There is no need to create a pull request.", "repo.pulls.nothing_to_compare_have_tag": "The selected branches/tags are equal.", "repo.pulls.nothing_to_compare_and_allow_empty_pr": "These branches are equal. This PR will be empty.", diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl index 390e41ec34..4649f9a45a 100644 --- a/templates/repo/diff/box.tmpl +++ b/templates/repo/diff/box.tmpl @@ -35,6 +35,12 @@ {{template "repo/diff/whitespace_dropdown" .}} {{template "repo/diff/options_dropdown" .}} {{if .PageIsPullFiles}} +
+ {{/* the following will be replaced by vue component, but this avoids any loading artifacts till the vue component is initialized */}} + +
{{/* the following will be replaced by vue component, but this avoids any loading artifacts till the vue component is initialized */}} -
+
@@ -209,6 +210,12 @@ export default defineComponent({ overflow-x: hidden; max-height: 450px; padding: 0.75rem; + padding-top: 0.5rem; + } + + .ui.dropdown.diff-file-extension-filter .menu > .header { + margin-top: 0; + padding-top: 0; } .ui.dropdown.diff-file-extension-filter .menu .ui.form { @@ -223,12 +230,25 @@ export default defineComponent({ margin-bottom: 0; } - .ui.dropdown.diff-file-extension-filter .button.red { + .ui.dropdown.diff-file-extension-filter .diff-ext-filter-btn-active { color: var(--color-red-700); border-color: var(--color-red-300); background: var(--color-red-50); } + .ui.dropdown.diff-file-extension-filter .diff-ext-text-btn { + background: none; + border: none; + padding: 0; + color: var(--color-primary); + cursor: pointer; + font-size: inherit; + } + + .ui.dropdown.diff-file-extension-filter .diff-ext-text-btn:hover { + text-decoration: underline; + } + .ui.dropdown.diff-file-extension-filter .filter-indicator-dot { position: absolute; top: 0.15rem; From f66a78a5c963e13e2fd38ed048250b654dafd03d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Krela?= Date: Tue, 31 Mar 2026 13:32:55 +0200 Subject: [PATCH 03/13] Fix event listener handling and improve button click behavior in DiffFileExtensionFilter --- web_src/js/components/DiffFileExtensionFilter.vue | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/web_src/js/components/DiffFileExtensionFilter.vue b/web_src/js/components/DiffFileExtensionFilter.vue index 35c1a4a73e..1234d7afac 100644 --- a/web_src/js/components/DiffFileExtensionFilter.vue +++ b/web_src/js/components/DiffFileExtensionFilter.vue @@ -27,10 +27,10 @@ export default defineComponent({ }; }, mounted() { - document.body.addEventListener('click', this.onBodyClick); + document.body.addEventListener('click', this.onBodyClick, true); }, unmounted() { - document.body.removeEventListener('click', this.onBodyClick); + document.body.removeEventListener('click', this.onBodyClick, true); }, methods: { onBodyClick(event: MouseEvent) { @@ -155,7 +155,7 @@ export default defineComponent({ ref="expandBtn" class="ui tiny basic button tw-relative" :class="{'diff-ext-filter-btn-active': isFiltering}" - @click.stop="toggleMenu()" + @click="toggleMenu()" :data-tooltip-content="locale.filter_by_file_extension" aria-haspopup="true" :aria-label="locale.filter_by_file_extension" @@ -181,7 +181,7 @@ export default defineComponent({ />
From d939757580d74ed5297ca53aab919c33f8e2c4ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Krela?= Date: Tue, 31 Mar 2026 13:52:25 +0200 Subject: [PATCH 04/13] Small visual fix for buttons in dropdown --- web_src/js/components/DiffFileExtensionFilter.vue | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/web_src/js/components/DiffFileExtensionFilter.vue b/web_src/js/components/DiffFileExtensionFilter.vue index 1234d7afac..8b08a0a41f 100644 --- a/web_src/js/components/DiffFileExtensionFilter.vue +++ b/web_src/js/components/DiffFileExtensionFilter.vue @@ -191,9 +191,9 @@ export default defineComponent({
-
- - +
+ +
@@ -243,6 +243,7 @@ export default defineComponent({ color: var(--color-primary); cursor: pointer; font-size: inherit; + text-align: center; } .ui.dropdown.diff-file-extension-filter .diff-ext-text-btn:hover { From 402c244e2b039515d30d0b081db8c0790fc7e9c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Krela?= Date: Tue, 31 Mar 2026 14:27:27 +0200 Subject: [PATCH 05/13] Enhance keyboard navigation and focus management in DiffFileExtensionFilter --- .../js/components/DiffFileExtensionFilter.vue | 70 ++++++++++++++----- 1 file changed, 54 insertions(+), 16 deletions(-) diff --git a/web_src/js/components/DiffFileExtensionFilter.vue b/web_src/js/components/DiffFileExtensionFilter.vue index 8b08a0a41f..ee4b09ef47 100644 --- a/web_src/js/components/DiffFileExtensionFilter.vue +++ b/web_src/js/components/DiffFileExtensionFilter.vue @@ -28,9 +28,11 @@ export default defineComponent({ }, mounted() { document.body.addEventListener('click', this.onBodyClick, true); + this.$el.addEventListener('keydown', this.onKeyDown); }, unmounted() { document.body.removeEventListener('click', this.onBodyClick, true); + this.$el.removeEventListener('keydown', this.onKeyDown); }, methods: { onBodyClick(event: MouseEvent) { @@ -40,6 +42,49 @@ export default defineComponent({ this.toggleMenu(); } }, + onKeyDown(event: KeyboardEvent) { + if (!this.menuVisible) return; + const currentFocused = document.activeElement as HTMLElement; + if (!this.$el.contains(currentFocused)) return; + + // Get all focusable menu items (checkboxes and buttons) + const menu = this.$el.querySelector('.menu') as HTMLElement; + const focusableItems = Array.from(menu.querySelectorAll('[role="menuitem"]')) as HTMLElement[]; + + if (!focusableItems.length) return; + + const currentIndex = focusableItems.indexOf(currentFocused.closest('[role="menuitem"]') as HTMLElement); + + switch (event.key) { + case 'ArrowDown': // select next element + event.preventDefault(); + const nextIndex = currentIndex === -1 ? 0 : Math.min(currentIndex + 1, focusableItems.length - 1); + this.focusElem(focusableItems[nextIndex], currentIndex >= 0 ? focusableItems[currentIndex] : null); + break; + case 'ArrowUp': // select previous element + event.preventDefault(); + const prevIndex = currentIndex === -1 ? focusableItems.length - 1 : Math.max(currentIndex - 1, 0); + this.focusElem(focusableItems[prevIndex], currentIndex >= 0 ? focusableItems[currentIndex] : null); + break; + case 'Escape': // close menu + event.preventDefault(); + if (currentIndex >= 0) { + focusableItems[currentIndex].tabIndex = -1; + } + this.toggleMenu(); + break; + } + }, + /** Focus given element */ + focusElem(elem: HTMLElement | null, prevElem: HTMLElement | null) { + if (elem) { + elem.tabIndex = 0; + if (prevElem) prevElem.tabIndex = -1; + // Focus the input/button inside the menuitem if it exists, otherwise focus the item itself + const focusTarget = elem.querySelector('input, button') as HTMLElement || elem; + focusTarget.focus(); + } + }, /** * Extract file extension from filename * Returns the extension with dot (e.g., ".ts", ".go") @@ -101,6 +146,11 @@ export default defineComponent({ this.menuVisible = !this.menuVisible; if (this.menuVisible) { this.scanExtensions(); + } else { + // when closing menu, restore focus to the button + const button = this.$refs.expandBtn as HTMLElement; + button.tabIndex = 0; + button.focus(); } }, /** @@ -162,7 +212,6 @@ export default defineComponent({ :aria-controls="uniqueIdMenu" > -