feat(voyage): implement J/K keyboard navigation + Esc + aria-live announces

Step 20 of v4.3 playground plan. Document-level keydown handler:
  - J = next annotation (next sorted-by-line draft, wraps)
  - K = prev annotation (wraps)
  - ] = toggle sidebar visibility
  - Escape = clear active anchor + sidebar list selection

Active annotation gets yellow-tint (Step 18 setActiveAnchor) and the
matching gutter-badge receives focus + scrollIntoView. Aria-live region
announces position + target: "Annotering 3 av 7: <target> — <snippet>".

Skips input/textarea/select/contenteditable so playground never steals
keystrokes from form fields. Modifier keys (Ctrl/Alt/Meta) pass through
to browser shortcuts. Wired in init() after dashboard nav.

Trace: SC2 (WCAG AA keyboard), SC6, research/04 Dim 2 + Insight 5 +
Recommendation keyboard-navigation.
This commit is contained in:
Kjell Tore Guttormsen 2026-05-10 17:10:59 +02:00
commit 224517f205
2 changed files with 81 additions and 0 deletions

View file

@ -279,3 +279,23 @@ test('voyage-playground.html declares renderAnnotationList JS function (v4.3 Ste
const text = readFileSync(HTML, 'utf-8');
assert.match(text, /function\s+renderAnnotationList\s*\(\s*\)/, 'renderAnnotationList() function required');
});
test('voyage-playground.html declares wireKeyboardNav with j/k/]/Escape (v4.3 Step 20)', () => {
const text = readFileSync(HTML, 'utf-8');
assert.match(text, /function\s+wireKeyboardNav\s*\(\s*\)/, 'wireKeyboardNav() function required');
assert.match(text, /e\.key === 'j'/, "'j' key handler required");
assert.match(text, /e\.key === 'k'/, "'k' key handler required");
assert.match(text, /e\.key === '\]'/, "']' key (toggle-sidebar) required");
assert.match(text, /e\.key === 'Escape'/, "'Escape' key handler required");
});
test('voyage-playground.html keyboard nav skips inputs/textareas (v4.3 Step 20)', () => {
const text = readFileSync(HTML, 'utf-8');
assert.match(text, /matches\([^)]*input[^)]*textarea/, 'input/textarea matches() guard required');
});
test('voyage-playground.html keyboard nav announces via aria-live region (v4.3 Step 20)', () => {
const text = readFileSync(HTML, 'utf-8');
// The wireKeyboardNav body contains announce(... ' av ' ...) for nav-position announce
assert.match(text, /announce\('Annotering '/, 'aria-live announce on annotation navigation required');
});