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);
|
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:
|
// Right-click → context menu. Two surfaces:
|
||||||
// - on a tree row: per-row menu (Open, Rename, Delete, …)
|
// - on a tree row: per-row menu (Open, Rename, Delete, …)
|
||||||
// - on empty space in the pane: directory-scope menu
|
// - on empty space in the pane: directory-scope menu
|
||||||
|
|
|
||||||
|
|
@ -691,6 +691,7 @@
|
||||||
state.sort.dir = (dir === -1 ? -1 : 1);
|
state.sort.dir = (dir === -1 ? -1 : 1);
|
||||||
render();
|
render();
|
||||||
},
|
},
|
||||||
pathFor: pathFor
|
pathFor: pathFor,
|
||||||
|
visibleIds: visibleIds
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue