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.
|
* Global keydown handler.
|
||||||
* If settings are open: Escape closes settings.
|
* Settings open: Escape closes settings, return.
|
||||||
* Comma key (not in inputs) opens settings.
|
* Ignore shortcuts when typing in INPUT/TEXTAREA/SELECT.
|
||||||
* In fullscreen: Escape returns to grid.
|
* 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
|
* @param {KeyboardEvent} e
|
||||||
*/
|
*/
|
||||||
function handleGlobalKeydown(e) {
|
function handleGlobalKeydown(e) {
|
||||||
|
// Settings open: only Escape closes it, then bail
|
||||||
if (_settingsOpen) {
|
if (_settingsOpen) {
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') { closeSettings(); }
|
||||||
closeSettings();
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.key === ',' && !e.ctrlKey && !e.metaKey) {
|
// Determine if focus is inside a text input
|
||||||
const tag = document.activeElement && document.activeElement.tagName;
|
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();
|
openSettings();
|
||||||
return;
|
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') {
|
if (_viewMode === 'fullscreen' && e.key === 'Escape') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
closeSession();
|
closeSession();
|
||||||
|
|||||||
@@ -3165,3 +3165,67 @@ def test_switch_view_patches_state() -> None:
|
|||||||
body = match.group(1)
|
body = match.group(1)
|
||||||
assert "PATCH" in body, "switchView must use PATCH method"
|
assert "PATCH" in body, "switchView must use PATCH method"
|
||||||
assert "/api/state" in body, "switchView must PATCH /api/state"
|
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