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"
>