feat: keyboard shortcuts — backtick, number keys, arrow navigation in handleGlobalKeydown
- Backtick (Backquote) toggles view dropdown in grid mode only (not fullscreen) - Number keys 1-9 directly switch views (1=all, 9=hidden, 2-8=user views) - Arrow keys navigate within open dropdown using [role="menuitem"] elements - Enter activates the focused dropdown item - Escape closes open dropdown when one is visible - Shortcuts disabled when typing in INPUT/TEXTAREA/SELECT - Input guard applied globally (not just for comma shortcut) Tests added: - test_handle_global_keydown_has_backtick_handler - test_handle_global_keydown_has_number_key_shortcuts - test_backtick_only_on_grid_not_fullscreen All 229 tests pass (226 existing + 3 new)
This commit is contained in:
+65
-8
@@ -1953,25 +1953,82 @@ function switchSettingsTab(tabName) {
|
||||
|
||||
/**
|
||||
* Global keydown handler.
|
||||
* If settings are open: Escape closes settings.
|
||||
* Comma key (not in inputs) opens settings.
|
||||
* In fullscreen: Escape returns to grid.
|
||||
* Settings open: Escape closes settings, return.
|
||||
* Ignore shortcuts when typing in INPUT/TEXTAREA/SELECT.
|
||||
* Comma key (not in inputs, no ctrl/meta) opens settings.
|
||||
* Grid overview only: backtick toggles dropdown, number keys 1-9 switch views.
|
||||
* Arrow keys + Enter navigate within open dropdown.
|
||||
* Escape closes open dropdown.
|
||||
* Fullscreen: Escape calls closeSession().
|
||||
* @param {KeyboardEvent} e
|
||||
*/
|
||||
function handleGlobalKeydown(e) {
|
||||
// Settings open: only Escape closes it, then bail
|
||||
if (_settingsOpen) {
|
||||
if (e.key === 'Escape') {
|
||||
closeSettings();
|
||||
}
|
||||
if (e.key === 'Escape') { closeSettings(); }
|
||||
return;
|
||||
}
|
||||
if (e.key === ',' && !e.ctrlKey && !e.metaKey) {
|
||||
// Determine if focus is inside a text input
|
||||
const tag = document.activeElement && document.activeElement.tagName;
|
||||
if (tag !== 'INPUT' && tag !== 'TEXTAREA' && tag !== 'SELECT') {
|
||||
const inInput = (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT');
|
||||
// Comma key (not in inputs, no ctrl/meta) opens settings
|
||||
if (e.key === ',' && !e.ctrlKey && !e.metaKey && !inInput) {
|
||||
openSettings();
|
||||
return;
|
||||
}
|
||||
// View dropdown shortcuts — grid overview only, not in input, no ctrl/meta
|
||||
if (_viewMode === 'grid' && !inInput && !e.ctrlKey && !e.metaKey) {
|
||||
// Backtick toggles the view dropdown
|
||||
if (e.code === 'Backquote') {
|
||||
e.preventDefault();
|
||||
toggleViewDropdown();
|
||||
return;
|
||||
}
|
||||
// Number keys 1-9: 1=all, 9=hidden, 2-8=user views by index (num-2)
|
||||
if (e.code && e.code.startsWith('Digit')) {
|
||||
const num = parseInt(e.key, 10);
|
||||
if (num >= 1 && num <= 9) {
|
||||
const views = (_serverSettings && _serverSettings.views) || [];
|
||||
if (num === 1) {
|
||||
switchView('all');
|
||||
} else if (num === 9) {
|
||||
switchView('hidden');
|
||||
} else {
|
||||
const vIdx = num - 2;
|
||||
if (vIdx < views.length) { switchView(views[vIdx].name); }
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Arrow key navigation within open dropdown
|
||||
const menu = $('view-dropdown-menu');
|
||||
if (menu && !menu.classList.contains('hidden')) {
|
||||
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
const items = Array.from(menu.querySelectorAll('[role="menuitem"]'));
|
||||
if (items.length > 0) {
|
||||
const focusedEl = document.activeElement;
|
||||
let itemIdx = items.indexOf(focusedEl);
|
||||
if (e.key === 'ArrowDown') {
|
||||
itemIdx = (itemIdx + 1) % items.length;
|
||||
} else {
|
||||
itemIdx = (itemIdx - 1 + items.length) % items.length;
|
||||
}
|
||||
items[itemIdx].focus();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (e.key === 'Enter') {
|
||||
const focusedEl = document.activeElement;
|
||||
if (menu.contains(focusedEl)) { focusedEl.click(); return; }
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
closeViewDropdown();
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Fullscreen: Escape calls closeSession
|
||||
if (_viewMode === 'fullscreen' && e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
closeSession();
|
||||
|
||||
@@ -3165,3 +3165,67 @@ def test_switch_view_patches_state() -> None:
|
||||
body = match.group(1)
|
||||
assert "PATCH" in body, "switchView must use PATCH method"
|
||||
assert "/api/state" in body, "switchView must PATCH /api/state"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Keyboard Shortcuts — Backtick, Number Keys, Arrow Navigation (task-7)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_handle_global_keydown_has_backtick_handler() -> None:
|
||||
"""handleGlobalKeydown must handle backtick/Backquote key to toggle view dropdown."""
|
||||
match = re.search(
|
||||
r"function handleGlobalKeydown\s*\(e\)\s*\{(.*?)\n\}",
|
||||
_JS,
|
||||
re.DOTALL,
|
||||
)
|
||||
assert match, "handleGlobalKeydown function not found"
|
||||
body = match.group(1)
|
||||
has_backtick = (
|
||||
"Backquote" in body
|
||||
or "e.key === '`'" in body
|
||||
or 'e.key === "`"' in body
|
||||
)
|
||||
assert has_backtick, (
|
||||
"handleGlobalKeydown must handle backtick/Backquote key to toggle view dropdown"
|
||||
)
|
||||
assert "toggleViewDropdown" in body, (
|
||||
"handleGlobalKeydown backtick handler must call toggleViewDropdown"
|
||||
)
|
||||
|
||||
|
||||
def test_handle_global_keydown_has_number_key_shortcuts() -> None:
|
||||
"""handleGlobalKeydown must call switchView for number keys 1–9."""
|
||||
match = re.search(
|
||||
r"function handleGlobalKeydown\s*\(e\)\s*\{(.*?)\n\}",
|
||||
_JS,
|
||||
re.DOTALL,
|
||||
)
|
||||
assert match, "handleGlobalKeydown function not found"
|
||||
body = match.group(1)
|
||||
assert "switchView" in body, (
|
||||
"handleGlobalKeydown must call switchView for number keys 1–9"
|
||||
)
|
||||
has_number_handling = (
|
||||
"Digit" in body or "parseInt" in body
|
||||
)
|
||||
assert has_number_handling, (
|
||||
"handleGlobalKeydown must handle number key codes (e.g. e.code.startsWith('Digit'))"
|
||||
)
|
||||
|
||||
|
||||
def test_backtick_only_on_grid_not_fullscreen() -> None:
|
||||
"""Backtick handler must check _viewMode === 'grid' (not active in fullscreen)."""
|
||||
match = re.search(
|
||||
r"function handleGlobalKeydown\s*\(e\)\s*\{(.*?)\n\}",
|
||||
_JS,
|
||||
re.DOTALL,
|
||||
)
|
||||
assert match, "handleGlobalKeydown function not found"
|
||||
body = match.group(1)
|
||||
assert "_viewMode" in body, (
|
||||
"handleGlobalKeydown backtick handler must check _viewMode"
|
||||
)
|
||||
assert "'grid'" in body, (
|
||||
"handleGlobalKeydown must check _viewMode === 'grid' before handling backtick/number shortcuts"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user