feat: refactor ActionsMenu for improved accessibility and keyboard navigation, including focus management and enhanced keyboard shortcut handling

This commit is contained in:
pandadev 2025-03-16 23:00:48 +01:00
parent b8238d01ca
commit 3a5e2cba7e
No known key found for this signature in database
GPG key ID: C39629DACB8E762F
4 changed files with 220 additions and 166 deletions

View file

@ -1,4 +1,11 @@
<template> <template>
<div
id="actions-menu"
class="actions-menu-overlay"
:class="{ visible: isVisible }"
@click="$emit('close')"
tabindex="-1">
<div class="actions-menu" @click.stop>
<div v-if="isVisible" class="actions" ref="menuRef"> <div v-if="isVisible" class="actions" ref="menuRef">
<OverlayScrollbarsComponent ref="scrollbarsRef" class="actions-scrollable" <OverlayScrollbarsComponent ref="scrollbarsRef" class="actions-scrollable"
:options="{ scrollbars: { autoHide: 'scroll' } }"> :options="{ scrollbars: { autoHide: 'scroll' } }">
@ -106,6 +113,8 @@
<input type="text" v-model="searchQuery" class="search-input" placeholder="Search..." @keydown="handleSearchKeydown" <input type="text" v-model="searchQuery" class="search-input" placeholder="Search..." @keydown="handleSearchKeydown"
ref="searchInput" /> ref="searchInput" />
</div> </div>
</div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">

View file

@ -125,6 +125,14 @@ const toggleActionsMenu = () => {
$keyboard.disableContext('actionsMenu'); $keyboard.disableContext('actionsMenu');
$keyboard.enableContext('main'); $keyboard.enableContext('main');
} }
nextTick(() => {
if (isActionsMenuVisible.value) {
document.getElementById('actions-menu')?.focus();
} else {
focusSearchInput();
}
});
}; };
const closeActionsMenu = () => { const closeActionsMenu = () => {
@ -545,7 +553,20 @@ const setupEventListeners = async (): Promise<void> => {
} }
focusSearchInput(); focusSearchInput();
$keyboard.clearAll(); $keyboard.disableContext('actionsMenu');
$keyboard.disableContext('settings');
$keyboard.enableContext('main');
if (isActionsMenuVisible.value) {
$keyboard.enableContext('actionsMenu');
}
});
await listen("tauri://blur", () => {
searchInput.value?.blur();
$keyboard.disableContext('main');
$keyboard.disableContext('actionsMenu');
});
$keyboard.setupAppShortcuts({ $keyboard.setupAppShortcuts({
onNavigateDown: selectNext, onNavigateDown: selectNext,
onNavigateUp: selectPrevious, onNavigateUp: selectPrevious,
@ -561,35 +582,7 @@ const setupEventListeners = async (): Promise<void> => {
contextName: 'main', contextName: 'main',
priority: $keyboard.PRIORITY.HIGH priority: $keyboard.PRIORITY.HIGH
}); });
$keyboard.disableContext('settings');
if (isActionsMenuVisible.value) {
$keyboard.enableContext('actionsMenu');
} else {
$keyboard.enableContext('main');
}
});
await listen("tauri://blur", () => {
searchInput.value?.blur();
$keyboard.clearAll();
$keyboard.disableContext('main');
});
$keyboard.setupAppShortcuts({
onNavigateDown: selectNext,
onNavigateUp: selectPrevious,
onSelect: pasteSelectedItem,
onEscape: () => {
if (isActionsMenuVisible.value) {
closeActionsMenu();
} else {
hideApp();
}
},
onToggleActions: toggleActionsMenu,
contextName: 'main',
priority: $keyboard.PRIORITY.LOW
});
$keyboard.enableContext('main'); $keyboard.enableContext('main');
}; };
@ -625,6 +618,24 @@ onMounted(async () => {
?.viewport?.addEventListener("scroll", handleScroll); ?.viewport?.addEventListener("scroll", handleScroll);
await setupEventListeners(); 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) { } catch (error) {
console.error("Error during onMounted:", error); console.error("Error during onMounted:", error);
} }

View file

@ -238,7 +238,6 @@ onMounted(async () => {
onUnmounted(() => { onUnmounted(() => {
$keyboard.disableContext("settings"); $keyboard.disableContext("settings");
$keyboard.clearAll();
}); });
</script> </script>

View file

@ -1,5 +1,4 @@
import { Key, keyboard } from "wrdu-keyboard"; import { Key, keyboard } from "wrdu-keyboard";
import { platform } from "@tauri-apps/plugin-os";
type KeyboardHandler = (event: KeyboardEvent) => void; type KeyboardHandler = (event: KeyboardEvent) => void;
@ -21,9 +20,6 @@ const PRIORITY = {
}; };
let currentOS = "windows"; let currentOS = "windows";
const initOS = async () => {
currentOS = await platform();
};
const useKeyboard = { const useKeyboard = {
PRIORITY, PRIORITY,
@ -58,12 +54,29 @@ const useKeyboard = {
if (!handlersByContext[contextName]) { if (!handlersByContext[contextName]) {
useKeyboard.registerContext(contextName); useKeyboard.registerContext(contextName);
} }
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({ handlersByContext[contextName].push({
keys, keys,
callback, callback,
prevent: options.prevent ?? true, prevent: options.prevent ?? true,
priority: options.priority ?? PRIORITY.LOW, priority: options.priority ?? PRIORITY.LOW,
}); });
}
if (activeContexts.has(contextName)) { if (activeContexts.has(contextName)) {
initKeyboardHandlers(); initKeyboardHandlers();
@ -118,7 +131,7 @@ const useKeyboard = {
} }
if (onToggleActions) { if (onToggleActions) {
const togglePriority = PRIORITY.HIGH; const togglePriority = Math.max(priority, PRIORITY.HIGH);
if (currentOS === "macos") { if (currentOS === "macos") {
useKeyboard.on( useKeyboard.on(
@ -169,48 +182,67 @@ const useKeyboard = {
const initKeyboardHandlers = () => { const initKeyboardHandlers = () => {
keyboard.clear(); 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) { for (const contextName of activeContexts) {
const handlers = handlersByContext[contextName] || []; const handlers = handlersByContext[contextName] || [];
allHandlers = [...allHandlers, ...handlers.map(handler => ({ allHandlers = [
...allHandlers,
...handlers.map((handler) => ({
...handler, ...handler,
priority: handler.priority ?? PRIORITY.LOW, priority: handler.priority ?? PRIORITY.LOW,
contextName contextName,
}))]; })),
];
} }
allHandlers.sort((a, b) => b.priority - a.priority); allHandlers.sort((a, b) => b.priority - a.priority);
const handlersByKeyCombination: Record<string, Array<typeof allHandlers[0]>> = {}; const handlersByKeyCombination: Record<
string,
Array<(typeof allHandlers)[0]>
> = {};
allHandlers.forEach(handler => { allHandlers.forEach((handler) => {
const keyCombo = handler.keys.join('+'); const keyCombo = handler.keys.sort().join("+");
if (!handlersByKeyCombination[keyCombo]) { if (!handlersByKeyCombination[keyCombo]) {
handlersByKeyCombination[keyCombo] = []; handlersByKeyCombination[keyCombo] = [];
} }
handlersByKeyCombination[keyCombo].push(handler); 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 handler = handlers[0];
const wrappedCallback: KeyboardHandler = (event) => { const wrappedCallback: KeyboardHandler = (event) => {
const isMetaCombo = handler.keys.length > 1 && const isMetaCombo =
handler.keys.length > 1 &&
(handler.keys.includes(Key.LeftMeta) || (handler.keys.includes(Key.LeftMeta) ||
handler.keys.includes(Key.RightMeta) || handler.keys.includes(Key.RightMeta) ||
handler.keys.includes(Key.LeftControl) || handler.keys.includes(Key.LeftControl) ||
handler.keys.includes(Key.RightControl)); handler.keys.includes(Key.RightControl));
const isNavigationKey = event.key === 'ArrowUp' || const isNavigationKey =
event.key === 'ArrowDown' || event.key === "ArrowUp" ||
event.key === 'Enter' || event.key === "ArrowDown" ||
event.key === 'Escape'; event.key === "Enter" ||
event.key === "Escape";
const isInInput = event.target instanceof HTMLInputElement || const isInInput =
event.target instanceof HTMLInputElement ||
event.target instanceof HTMLTextAreaElement; event.target instanceof HTMLTextAreaElement;
if (isMetaCombo || isNavigationKey || !isInInput) { if (
(isMetaCombo || isNavigationKey || !isInInput) &&
activeContexts.has(handler.contextName)
) {
handler.callback(event); handler.callback(event);
} }
}; };
@ -223,11 +255,14 @@ const initKeyboardHandlers = () => {
}); });
}; };
export default defineNuxtPlugin(async () => { export default defineNuxtPlugin(async (nuxtApp) => {
await initOS();
initKeyboardHandlers(); initKeyboardHandlers();
nuxtApp.hook("page:finish", () => {
initKeyboardHandlers();
});
return { return {
provide: { provide: {
keyboard: { keyboard: {