From 3a5e2cba7e79d2ae339649357f8cb57237141504 Mon Sep 17 00:00:00 2001 From: pandadev <70103896+0PandaDEV@users.noreply.github.com> Date: Sun, 16 Mar 2025 23:00:48 +0100 Subject: [PATCH] feat: refactor ActionsMenu for improved accessibility and keyboard navigation, including focus management and enhanced keyboard shortcut handling --- components/ActionsMenu.vue | 205 +++++++++++++++++++------------------ pages/index.vue | 53 ++++++---- pages/settings.vue | 1 - plugins/keyboard.ts | 127 ++++++++++++++--------- 4 files changed, 220 insertions(+), 166 deletions(-) diff --git a/components/ActionsMenu.vue b/components/ActionsMenu.vue index 57d8225..37fbae8 100644 --- a/components/ActionsMenu.vue +++ b/components/ActionsMenu.vue @@ -1,110 +1,119 @@ diff --git a/pages/index.vue b/pages/index.vue index b6a8464..774b0a0 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -125,6 +125,14 @@ const toggleActionsMenu = () => { $keyboard.disableContext('actionsMenu'); $keyboard.enableContext('main'); } + + nextTick(() => { + if (isActionsMenuVisible.value) { + document.getElementById('actions-menu')?.focus(); + } else { + focusSearchInput(); + } + }); }; const closeActionsMenu = () => { @@ -545,34 +553,18 @@ const setupEventListeners = async (): Promise => { } focusSearchInput(); - $keyboard.clearAll(); - $keyboard.setupAppShortcuts({ - onNavigateDown: selectNext, - onNavigateUp: selectPrevious, - onSelect: pasteSelectedItem, - onEscape: () => { - if (isActionsMenuVisible.value) { - closeActionsMenu(); - } else { - hideApp(); - } - }, - onToggleActions: toggleActionsMenu, - contextName: 'main', - priority: $keyboard.PRIORITY.HIGH - }); - + $keyboard.disableContext('actionsMenu'); + $keyboard.disableContext('settings'); + $keyboard.enableContext('main'); if (isActionsMenuVisible.value) { $keyboard.enableContext('actionsMenu'); - } else { - $keyboard.enableContext('main'); } }); await listen("tauri://blur", () => { searchInput.value?.blur(); - $keyboard.clearAll(); $keyboard.disableContext('main'); + $keyboard.disableContext('actionsMenu'); }); $keyboard.setupAppShortcuts({ @@ -588,8 +580,9 @@ const setupEventListeners = async (): Promise => { }, onToggleActions: toggleActionsMenu, contextName: 'main', - priority: $keyboard.PRIORITY.LOW + priority: $keyboard.PRIORITY.HIGH }); + $keyboard.disableContext('settings'); $keyboard.enableContext('main'); }; @@ -625,6 +618,24 @@ onMounted(async () => { ?.viewport?.addEventListener("scroll", handleScroll); await setupEventListeners(); + + $keyboard.setupAppShortcuts({ + onNavigateDown: selectNext, + onNavigateUp: selectPrevious, + onSelect: pasteSelectedItem, + onEscape: () => { + if (isActionsMenuVisible.value) { + closeActionsMenu(); + } else { + hideApp(); + } + }, + onToggleActions: toggleActionsMenu, + contextName: 'main', + priority: $keyboard.PRIORITY.HIGH + }); + $keyboard.disableContext('settings'); + $keyboard.enableContext('main'); } catch (error) { console.error("Error during onMounted:", error); } diff --git a/pages/settings.vue b/pages/settings.vue index d982118..5f0c7c5 100644 --- a/pages/settings.vue +++ b/pages/settings.vue @@ -238,7 +238,6 @@ onMounted(async () => { onUnmounted(() => { $keyboard.disableContext("settings"); - $keyboard.clearAll(); }); diff --git a/plugins/keyboard.ts b/plugins/keyboard.ts index a1f3ab0..1bb6847 100644 --- a/plugins/keyboard.ts +++ b/plugins/keyboard.ts @@ -1,5 +1,4 @@ import { Key, keyboard } from "wrdu-keyboard"; -import { platform } from "@tauri-apps/plugin-os"; type KeyboardHandler = (event: KeyboardEvent) => void; @@ -21,9 +20,6 @@ const PRIORITY = { }; let currentOS = "windows"; -const initOS = async () => { - currentOS = await platform(); -}; const useKeyboard = { PRIORITY, @@ -58,12 +54,29 @@ const useKeyboard = { if (!handlersByContext[contextName]) { useKeyboard.registerContext(contextName); } - handlersByContext[contextName].push({ - keys, - callback, - prevent: options.prevent ?? true, - priority: options.priority ?? PRIORITY.LOW, - }); + + const existingHandlerIndex = handlersByContext[contextName].findIndex( + (handler) => + handler.keys.length === keys.length && + handler.keys.every((key, i) => key === keys[i]) && + handler.callback.toString() === callback.toString() + ); + + if (existingHandlerIndex !== -1) { + handlersByContext[contextName][existingHandlerIndex] = { + keys, + callback, + prevent: options.prevent ?? true, + priority: options.priority ?? PRIORITY.LOW, + }; + } else { + handlersByContext[contextName].push({ + keys, + callback, + prevent: options.prevent ?? true, + priority: options.priority ?? PRIORITY.LOW, + }); + } if (activeContexts.has(contextName)) { initKeyboardHandlers(); @@ -118,8 +131,8 @@ const useKeyboard = { } if (onToggleActions) { - const togglePriority = PRIORITY.HIGH; - + const togglePriority = Math.max(priority, PRIORITY.HIGH); + if (currentOS === "macos") { useKeyboard.on( contextName, @@ -169,52 +182,71 @@ const useKeyboard = { const initKeyboardHandlers = () => { keyboard.clear(); - let allHandlers: Array<{ keys: Key[], callback: KeyboardHandler, prevent: boolean, priority: number, contextName: string }> = []; - + let allHandlers: Array<{ + keys: Key[]; + callback: KeyboardHandler; + prevent: boolean; + priority: number; + contextName: string; + }> = []; + for (const contextName of activeContexts) { const handlers = handlersByContext[contextName] || []; - allHandlers = [...allHandlers, ...handlers.map(handler => ({ - ...handler, - priority: handler.priority ?? PRIORITY.LOW, - contextName - }))]; + allHandlers = [ + ...allHandlers, + ...handlers.map((handler) => ({ + ...handler, + priority: handler.priority ?? PRIORITY.LOW, + contextName, + })), + ]; } - + allHandlers.sort((a, b) => b.priority - a.priority); - - const handlersByKeyCombination: Record> = {}; - - allHandlers.forEach(handler => { - const keyCombo = handler.keys.join('+'); + + const handlersByKeyCombination: Record< + string, + Array<(typeof allHandlers)[0]> + > = {}; + + allHandlers.forEach((handler) => { + const keyCombo = handler.keys.sort().join("+"); if (!handlersByKeyCombination[keyCombo]) { handlersByKeyCombination[keyCombo] = []; } handlersByKeyCombination[keyCombo].push(handler); }); - - Object.values(handlersByKeyCombination).forEach(handlers => { + + Object.entries(handlersByKeyCombination).forEach(([_keyCombo, handlers]) => { + handlers.sort((a, b) => b.priority - a.priority); const handler = handlers[0]; - + const wrappedCallback: KeyboardHandler = (event) => { - const isMetaCombo = handler.keys.length > 1 && - (handler.keys.includes(Key.LeftMeta) || - handler.keys.includes(Key.RightMeta) || - handler.keys.includes(Key.LeftControl) || - handler.keys.includes(Key.RightControl)); - - const isNavigationKey = event.key === 'ArrowUp' || - event.key === 'ArrowDown' || - event.key === 'Enter' || - event.key === 'Escape'; - - const isInInput = event.target instanceof HTMLInputElement || - event.target instanceof HTMLTextAreaElement; - - if (isMetaCombo || isNavigationKey || !isInInput) { + const isMetaCombo = + handler.keys.length > 1 && + (handler.keys.includes(Key.LeftMeta) || + handler.keys.includes(Key.RightMeta) || + handler.keys.includes(Key.LeftControl) || + handler.keys.includes(Key.RightControl)); + + const isNavigationKey = + event.key === "ArrowUp" || + event.key === "ArrowDown" || + event.key === "Enter" || + event.key === "Escape"; + + const isInInput = + event.target instanceof HTMLInputElement || + event.target instanceof HTMLTextAreaElement; + + if ( + (isMetaCombo || isNavigationKey || !isInInput) && + activeContexts.has(handler.contextName) + ) { handler.callback(event); } }; - + if (handler.prevent) { keyboard.prevent.down(handler.keys, wrappedCallback); } else { @@ -223,11 +255,14 @@ const initKeyboardHandlers = () => { }); }; -export default defineNuxtPlugin(async () => { - await initOS(); +export default defineNuxtPlugin(async (nuxtApp) => { initKeyboardHandlers(); + nuxtApp.hook("page:finish", () => { + initKeyboardHandlers(); + }); + return { provide: { keyboard: {