mirror of
https://github.com/0PandaDEV/Qopy.git
synced 2025-04-21 13:14:04 +02:00
feat: refactor ActionsMenu for improved accessibility and keyboard navigation, including focus management and enhanced keyboard shortcut handling
This commit is contained in:
parent
b8238d01ca
commit
3a5e2cba7e
4 changed files with 220 additions and 166 deletions
|
@ -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">
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -238,7 +238,6 @@ onMounted(async () => {
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
$keyboard.disableContext("settings");
|
$keyboard.disableContext("settings");
|
||||||
$keyboard.clearAll();
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue