feat(browse): keyboard navigation in the file tree
Document-level keydown handler covers the W3C tree-view pattern so
users can drive the browse pane without the mouse:
↓ / ↑ — move selection (auto-previews files as the cursor
lands so the right pane keeps up)
→ — expand collapsed folder; jump to first child if
already expanded; no-op on leaves
← — collapse expanded folder; otherwise jump to parent
Enter / Space — preview file / toggle folder
Home / End — first / last visible row
Bails out cleanly when focus is in an input/textarea/contenteditable
or when a modal / context menu is open, so it doesn't fight existing
filter typing, YAML editor, or the right-click menu's own keys. Any
modifier (Ctrl/Cmd/Alt) lets the browser shortcut through unchanged.
Selection updates scroll the now-current row into view via
scrollIntoView({block:'nearest'}). Tree module gains a visibleIds
export so events.js can walk the same filtered+expanded order the
renderer uses.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4497ebdf99
commit
03d008ff0a
2 changed files with 105 additions and 1 deletions
|
|
@ -307,6 +307,109 @@
|
|||
navigateIntoFolder(node);
|
||||
});
|
||||
|
||||
// Keyboard navigation in the tree. Document-level listener so
|
||||
// the user doesn't have to click into the tree first; bails
|
||||
// out cleanly when focus is in an editable field or when a
|
||||
// modal / context-menu owns the keys. Roving-tabindex-style
|
||||
// semantics, matching the W3C tree-view pattern:
|
||||
//
|
||||
// ↓ / ↑ — move selection (auto-previews files)
|
||||
// → — expand if collapsed; jump to first child
|
||||
// if already expanded; no-op otherwise
|
||||
// ← — collapse if expanded; jump to parent
|
||||
// if collapsed/leaf
|
||||
// Enter / Space — preview file / toggle folder
|
||||
// Home / End — first / last visible row
|
||||
document.addEventListener('keydown', function (e) {
|
||||
// Skip editable contexts.
|
||||
var tag = (e.target && e.target.tagName) || '';
|
||||
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return;
|
||||
if (e.target && e.target.isContentEditable) return;
|
||||
// Skip when a modal or context menu is open.
|
||||
if (document.querySelector('.modal-overlay, .zddc-menu')) return;
|
||||
// Skip if any modifier is pressed — lets Ctrl-F, Cmd-T,
|
||||
// Alt-arrow back/forward etc. fall through unchanged.
|
||||
if (e.ctrlKey || e.metaKey || e.altKey) return;
|
||||
|
||||
var key = e.key;
|
||||
var navKey = key === 'ArrowDown' || key === 'ArrowUp'
|
||||
|| key === 'ArrowLeft' || key === 'ArrowRight'
|
||||
|| key === 'Home' || key === 'End'
|
||||
|| key === 'Enter' || key === ' ';
|
||||
if (!navKey) return;
|
||||
|
||||
var visible = tree.visibleIds();
|
||||
if (!visible.length) return;
|
||||
|
||||
// Commit to handling this key — preventDefault so the
|
||||
// browser doesn't also scroll on arrows / page-down on
|
||||
// Space. Selection / expand actions happen below.
|
||||
e.preventDefault();
|
||||
|
||||
var curIdx = visible.indexOf(state.selectedId);
|
||||
var node = state.selectedId != null
|
||||
? state.nodes.get(state.selectedId) : null;
|
||||
var expandable = !!(node && (node.isDir || node.isZip));
|
||||
var nextId = null;
|
||||
var previewModule = previewMod();
|
||||
|
||||
if (key === 'ArrowDown') {
|
||||
nextId = curIdx < 0
|
||||
? visible[0]
|
||||
: visible[Math.min(curIdx + 1, visible.length - 1)];
|
||||
} else if (key === 'ArrowUp') {
|
||||
nextId = curIdx < 0
|
||||
? visible[visible.length - 1]
|
||||
: visible[Math.max(curIdx - 1, 0)];
|
||||
} else if (key === 'Home') {
|
||||
nextId = visible[0];
|
||||
} else if (key === 'End') {
|
||||
nextId = visible[visible.length - 1];
|
||||
} else if (key === 'ArrowRight' && node) {
|
||||
if (expandable && !node.expanded) {
|
||||
tree.toggleFolder(node.id);
|
||||
return;
|
||||
}
|
||||
if (expandable && node.expanded
|
||||
&& node.childIds && node.childIds.length) {
|
||||
nextId = node.childIds[0];
|
||||
}
|
||||
} else if (key === 'ArrowLeft' && node) {
|
||||
if (expandable && node.expanded) {
|
||||
tree.toggleFolder(node.id);
|
||||
return;
|
||||
}
|
||||
if (node.parentId != null) {
|
||||
nextId = node.parentId;
|
||||
}
|
||||
} else if ((key === 'Enter' || key === ' ') && node) {
|
||||
if (expandable) {
|
||||
tree.toggleFolder(node.id);
|
||||
} else if (previewModule) {
|
||||
previewModule.showFilePreview(node);
|
||||
state.lastPreviewedNodeId = node.id;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (nextId == null) return;
|
||||
state.selectedId = nextId;
|
||||
var nextNode = state.nodes.get(nextId);
|
||||
tree.render();
|
||||
// Auto-preview files as the keyboard cursor lands on them
|
||||
// so the right pane keeps up with selection. Folders are
|
||||
// selection-only; their preview is "expand to see inside".
|
||||
if (nextNode && !nextNode.isDir && !nextNode.isZip
|
||||
&& previewModule) {
|
||||
previewModule.showFilePreview(nextNode);
|
||||
state.lastPreviewedNodeId = nextId;
|
||||
}
|
||||
// Scroll the now-selected row into view.
|
||||
var newRow = treeBody.querySelector(
|
||||
'.tree-row[data-id="' + nextId + '"]');
|
||||
if (newRow) newRow.scrollIntoView({ block: 'nearest' });
|
||||
});
|
||||
|
||||
// Right-click → context menu. Two surfaces:
|
||||
// - on a tree row: per-row menu (Open, Rename, Delete, …)
|
||||
// - on empty space in the pane: directory-scope menu
|
||||
|
|
|
|||
|
|
@ -691,6 +691,7 @@
|
|||
state.sort.dir = (dir === -1 ? -1 : 1);
|
||||
render();
|
||||
},
|
||||
pathFor: pathFor
|
||||
pathFor: pathFor,
|
||||
visibleIds: visibleIds
|
||||
};
|
||||
})();
|
||||
|
|
|
|||
Loading…
Reference in a new issue