Moves phases 08–11 execution artifacts from .planning/phases/ to .planning/milestones/v0.2-phases/ to keep .planning/phases/ clean for the next milestone. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
6.4 KiB
phase, plan, subsystem, tags, dependency_graph, tech_stack, key_files, decisions, metrics
| phase | plan | subsystem | tags | dependency_graph | tech_stack | key_files | decisions | metrics | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 10-ux-interaction | 09 | frontend/ux |
|
|
|
|
|
|
Phase 10 Plan 09: Global Keyboard Shortcuts Summary
One-liner: Four global keyboard shortcuts (/, Escape, U, N) wired via App.vue routeViewRef through a defineExpose ref chain: FileManagerView → StorageBrowser → DropZone/SearchBar.
Tasks Completed
| Task | Name | Commit | Files |
|---|---|---|---|
| 1 | Promote keyboard.test.js stubs to 9 RED failing tests | 089af90 |
src/__tests__/keyboard.test.js |
| 2 | Plumb ref chain — DropZone + SearchBar + StorageBrowser + FileManagerView | aaa0532 |
DropZone.vue, SearchBar.vue, StorageBrowser.vue, FileManagerView.vue |
| 3 | Add global keydown handler to App.vue + routeViewRef | d7bda3c |
App.vue, StorageBrowser.vue |
What Was Built
keyboard.test.js: Replaced 11 it.todo stubs with 9 real it() tests grouped across 4 describe blocks (UX-05..UX-08). Tests mount FileManagerView with mocked child components and assert the exposed methods exist and do not throw. All 9 were RED before Task 2 and GREEN after Task 3.
DropZone.vue: Added defineExpose({ triggerInput }) after the existing triggerInput function definition. No other changes.
SearchBar.vue: Added import { ref } from 'vue', declared const inputEl = ref(null), bound ref="inputEl" to the <input> element, and added defineExpose({ focus() { inputEl.value?.focus() } }).
StorageBrowser.vue:
- Added
const dropZoneRef = ref(null)andconst searchBarRef = ref(null) - Added
ref="dropZoneRef"on<DropZone>andref="searchBarRef"on<SearchBar> - Replaced
defineExpose({ startNewFolder })with:defineExpose({ startNewFolder, triggerUpload: () => dropZoneRef.value?.triggerInput?.(), focusSearch: () => searchBarRef.value?.focus?.(), clearSearch: () => emit('search-change', ''), })
FileManagerView.vue: Added defineExpose block delegating all four methods to browserRef via optional chaining:
defineExpose({
focusSearch: () => browserRef.value?.focusSearch?.(),
triggerUpload: () => browserRef.value?.triggerUpload?.(),
startNewFolder: () => browserRef.value?.startNewFolder?.(),
clearSearch: () => browserRef.value?.clearSearch?.(),
})
App.vue:
- Added
refandonUnmountedto the imports - Added
const routeViewRef = ref(null) - Added
ref="routeViewRef"on<router-view> - Added
onKeydownhandler with activeElement guard and four shortcut branches - Chained
document.addEventListener('keydown', onKeydown)into the existingonMounted - Added
onUnmounted(() => document.removeEventListener('keydown', onKeydown))
Verification Results
| Check | Result |
|---|---|
vitest run keyboard — 9 tests |
PASS (all GREEN) |
vitest run FileManagerView — 20 tests |
PASS (no regression) |
vitest run StorageBrowser — 4 tests (9 todo) |
PASS (no regression) |
| Full suite: 190 tests, 0 failures, 20 todo | PASS |
ref="routeViewRef" in App.vue template |
PASS |
const routeViewRef = ref(null) in App.vue |
PASS |
function onKeydown in App.vue |
PASS |
document.activeElement?.tagName guard |
PASS |
isContentEditable guard |
PASS |
e.preventDefault() in / branch |
PASS |
addEventListener('keydown') + removeEventListener |
PASS |
| All 14 defineExpose acceptance criteria greps | PASS |
Deviations from Plan
Auto-fixed Issues
1. [Rule 1 - Bug] Added double optional chaining on StorageBrowser.triggerUpload and focusSearch
- Found during: Task 3
- Issue:
dropZoneRef.value?.triggerInput()throwsTypeError: triggerInput is not a functionin tests when the DropZone stub doesn't exposetriggerInput. The?.guard only prevents calling on null/undefined ref, buttriggerInputbeingundefinedon the stub still causes a throw when invoked with(). - Fix: Changed to
dropZoneRef.value?.triggerInput?.()andsearchBarRef.value?.focus?.()— double optional chaining silently no-ops when the method is absent. - Files modified:
StorageBrowser.vue - Commit:
d7bda3c
Known Stubs
None.
Threat Flags
None. This plan adds only client-side keyboard event handling and component defineExpose plumbing. No new network endpoints, auth paths, file access, or schema changes.
Self-Check: PASSED
frontend/src/__tests__/keyboard.test.js— exists, 9 real testsfrontend/src/App.vue— contains routeViewRef + onKeydown + addEventListener/removeEventListenerfrontend/src/views/FileManagerView.vue— contains defineExpose with all 4 methodsfrontend/src/components/storage/StorageBrowser.vue— contains dropZoneRef, searchBarRef, expanded defineExposefrontend/src/components/upload/DropZone.vue— contains defineExpose({ triggerInput })frontend/src/components/documents/SearchBar.vue— contains inputEl ref + defineExpose({ focus })- Commit
089af90— confirmed in git log (RED tests) - Commit
aaa0532— confirmed in git log (ref chain) - Commit
d7bda3c— confirmed in git log (App.vue handler) - No unexpected file deletions