docs(10-09): add plan SUMMARY — keyboard shortcuts via ref chain complete
- 3 tasks executed: RED tests → ref chain plumbing → App.vue handler GREEN - 9 keyboard tests passing; 190 total tests, 0 failures - Documents double optional chaining deviation (auto-fixed bug in StorageBrowser)
This commit is contained in:
@@ -0,0 +1,131 @@
|
|||||||
|
---
|
||||||
|
phase: 10-ux-interaction
|
||||||
|
plan: "09"
|
||||||
|
subsystem: frontend/ux
|
||||||
|
tags: [keyboard-shortcuts, ref-chain, defineExpose, vue3, ux, vitest, tdd]
|
||||||
|
dependency_graph:
|
||||||
|
requires: [10-04, 10-05, 10-06]
|
||||||
|
provides: [keyboard-shortcuts-UX-05-06-07-08, App.vue-routeViewRef, FileManagerView-defineExpose, StorageBrowser-defineExpose-extended]
|
||||||
|
affects: [App.vue, FileManagerView.vue, StorageBrowser.vue, DropZone.vue, SearchBar.vue]
|
||||||
|
tech_stack:
|
||||||
|
added: []
|
||||||
|
patterns: [defineExpose-ref-chain, optional-chaining-delegation, global-keydown-handler, TDD-red-green]
|
||||||
|
key_files:
|
||||||
|
created: []
|
||||||
|
modified:
|
||||||
|
- frontend/src/__tests__/keyboard.test.js
|
||||||
|
- frontend/src/App.vue
|
||||||
|
- frontend/src/views/FileManagerView.vue
|
||||||
|
- frontend/src/components/storage/StorageBrowser.vue
|
||||||
|
- frontend/src/components/upload/DropZone.vue
|
||||||
|
- frontend/src/components/documents/SearchBar.vue
|
||||||
|
decisions:
|
||||||
|
- "Double optional chaining (?.) used in StorageBrowser.triggerUpload and focusSearch to guard against stub components lacking the method in tests"
|
||||||
|
- "Testing strategy: mount FileManagerView directly and assert defineExpose methods exist/do not throw — avoids complexity of mounting App.vue with router+stores"
|
||||||
|
- "onMounted callback in App.vue chains both topicsStore.fetchTopics() and document.addEventListener('keydown', onKeydown) — single mount lifecycle call"
|
||||||
|
metrics:
|
||||||
|
duration: "6m"
|
||||||
|
completed: "2026-06-15T20:37:00Z"
|
||||||
|
tasks_completed: 3
|
||||||
|
files_changed: 6
|
||||||
|
---
|
||||||
|
|
||||||
|
# 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)` and `const searchBarRef = ref(null)`
|
||||||
|
- Added `ref="dropZoneRef"` on `<DropZone>` and `ref="searchBarRef"` on `<SearchBar>`
|
||||||
|
- Replaced `defineExpose({ startNewFolder })` with:
|
||||||
|
```js
|
||||||
|
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:
|
||||||
|
```js
|
||||||
|
defineExpose({
|
||||||
|
focusSearch: () => browserRef.value?.focusSearch?.(),
|
||||||
|
triggerUpload: () => browserRef.value?.triggerUpload?.(),
|
||||||
|
startNewFolder: () => browserRef.value?.startNewFolder?.(),
|
||||||
|
clearSearch: () => browserRef.value?.clearSearch?.(),
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**App.vue:**
|
||||||
|
- Added `ref` and `onUnmounted` to the imports
|
||||||
|
- Added `const routeViewRef = ref(null)`
|
||||||
|
- Added `ref="routeViewRef"` on `<router-view>`
|
||||||
|
- Added `onKeydown` handler with activeElement guard and four shortcut branches
|
||||||
|
- Chained `document.addEventListener('keydown', onKeydown)` into the existing `onMounted`
|
||||||
|
- 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()` throws `TypeError: triggerInput is not a function` in tests when the DropZone stub doesn't expose `triggerInput`. The `?.` guard only prevents calling on null/undefined ref, but `triggerInput` being `undefined` on the stub still causes a throw when invoked with `()`.
|
||||||
|
- **Fix:** Changed to `dropZoneRef.value?.triggerInput?.()` and `searchBarRef.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 tests
|
||||||
|
- `frontend/src/App.vue` — contains routeViewRef + onKeydown + addEventListener/removeEventListener
|
||||||
|
- `frontend/src/views/FileManagerView.vue` — contains defineExpose with all 4 methods
|
||||||
|
- `frontend/src/components/storage/StorageBrowser.vue` — contains dropZoneRef, searchBarRef, expanded defineExpose
|
||||||
|
- `frontend/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
|
||||||
Reference in New Issue
Block a user