feat(10-09): add global keydown handler to App.vue — /, Escape, U, N shortcuts live

- Add routeViewRef = ref(null) and bind to <router-view ref="routeViewRef">
- Add onKeydown handler: guards INPUT/TEXTAREA/SELECT/contenteditable; routes / → focusSearch, Escape → clearSearch, U/u → triggerUpload, N/n → startNewFolder
- Chain document.addEventListener on onMounted, removeEventListener on onUnmounted
- Add double optional chaining (?.) in StorageBrowser.triggerUpload and focusSearch so stubs don't throw in tests
- 9 keyboard shortcut tests GREEN; full suite 190 passing 0 failures
This commit is contained in:
curo1305
2026-06-15 20:36:52 +02:00
parent aaa0532af9
commit d7bda3c605
2 changed files with 30 additions and 5 deletions
+28 -3
View File
@@ -3,14 +3,14 @@
<div v-else class="flex h-screen overflow-hidden"> <div v-else class="flex h-screen overflow-hidden">
<AppSidebar /> <AppSidebar />
<main class="flex-1 overflow-y-auto"> <main class="flex-1 overflow-y-auto">
<router-view /> <router-view ref="routeViewRef" />
</main> </main>
</div> </div>
<ToastContainer /> <ToastContainer />
</template> </template>
<script setup> <script setup>
import { onMounted } from 'vue' import { ref, onMounted, onUnmounted } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import AppSidebar from './components/layout/AppSidebar.vue' import AppSidebar from './components/layout/AppSidebar.vue'
import AuthLayout from './layouts/AuthLayout.vue' import AuthLayout from './layouts/AuthLayout.vue'
@@ -19,5 +19,30 @@ import { useTopicsStore } from './stores/topics.js'
const route = useRoute() const route = useRoute()
const topicsStore = useTopicsStore() const topicsStore = useTopicsStore()
onMounted(() => topicsStore.fetchTopics()) const routeViewRef = ref(null)
function onKeydown(e) {
const tag = document.activeElement?.tagName
if (['INPUT', 'TEXTAREA', 'SELECT'].includes(tag) || document.activeElement?.isContentEditable) return
if (e.key === '/' && !e.ctrlKey && !e.metaKey) {
e.preventDefault()
routeViewRef.value?.focusSearch?.()
}
if (e.key === 'Escape') {
routeViewRef.value?.clearSearch?.()
}
if (e.key === 'u' || e.key === 'U') {
routeViewRef.value?.triggerUpload?.()
}
if (e.key === 'n' || e.key === 'N') {
routeViewRef.value?.startNewFolder?.()
}
}
onMounted(() => {
topicsStore.fetchTopics()
document.addEventListener('keydown', onKeydown)
})
onUnmounted(() => document.removeEventListener('keydown', onKeydown))
</script> </script>
@@ -353,8 +353,8 @@ async function submitNewFolder() {
defineExpose({ defineExpose({
startNewFolder, startNewFolder,
triggerUpload: () => dropZoneRef.value?.triggerInput(), triggerUpload: () => dropZoneRef.value?.triggerInput?.(),
focusSearch: () => searchBarRef.value?.focus(), focusSearch: () => searchBarRef.value?.focus?.(),
clearSearch: () => emit('search-change', ''), clearSearch: () => emit('search-change', ''),
}) })