Skip to content

Commit a1da259

Browse files
authored
Fix focus for keyboard navigation (#33)
* fix focus for keyboard navigation * Focus clicked cell on mouse down * Focus webview on activation * Arrow scrolling and enter to enter cell
1 parent b0143e1 commit a1da259

File tree

3 files changed

+67
-10
lines changed

3 files changed

+67
-10
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,9 @@
9999
"papaparse": "^5.4.1"
100100
},
101101
"devDependencies": {
102-
"@types/mocha": "^10.0.0",
102+
"@types/mocha": "^10.0.10",
103103
"@types/node": "^14.0.0",
104+
"@types/papaparse": "^5.3.16",
104105
"@types/vscode": "^1.60.0",
105106
"vscode": "^1.1.37",
106107
"@vscode/test-cli": "^0.0.11",

src/extension.ts

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,12 @@ class CsvEditorProvider implements vscode.CustomTextEditorProvider {
8989
CsvEditorProvider.editors.push(this);
9090
webviewPanel.webview.options = { enableScripts: true };
9191
this.updateWebviewContent();
92+
webviewPanel.webview.postMessage({ type: 'focus' });
93+
webviewPanel.onDidChangeViewState(e => {
94+
if (e.webviewPanel.active) {
95+
e.webviewPanel.webview.postMessage({ type: 'focus' });
96+
}
97+
});
9298

9399
// Handle messages from the webview.
94100
webviewPanel.webview.onDidReceiveMessage(async e => {
@@ -187,7 +193,7 @@ class CsvEditorProvider implements vscode.CustomTextEditorProvider {
187193
while (data[row].length <= col) data[row].push('');
188194
data[row][col] = value;
189195
const newCsvText = Papa.unparse(data, { delimiter: separator });
190-
const fullRange = new vscode.Range(0, 0, this.document.lineCount, this.document.lineAt(this.document.lineCount - 1).text.length);
196+
const fullRange = new vscode.Range(0, 0, this.document.lineCount, this.document.lineCount ? this.document.lineAt(this.document.lineCount - 1).text.length : 0);
191197
const edit = new vscode.WorkspaceEdit();
192198
edit.replace(this.document.uri, fullRange, newCsvText);
193199
await vscode.workspace.applyEdit(edit);
@@ -478,6 +484,7 @@ class CsvEditorProvider implements vscode.CustomTextEditorProvider {
478484
<div id="contextMenu"></div>
479485
<script nonce="${nonce}">
480486
document.body.setAttribute('tabindex', '0'); document.body.focus();
487+
window.addEventListener('mousedown', () => document.body.focus());
481488
const vscode = acquireVsCodeApi();
482489
let isUpdating = false, isSelecting = false, anchorCell = null, currentSelection = [];
483490
let startCell = null, endCell = null, selectionMode = "cell";
@@ -545,6 +552,7 @@ class CsvEditorProvider implements vscode.CustomTextEditorProvider {
545552
const target = e.target;
546553
selectionMode = (target.tagName === 'TH') ? "column" : (target.getAttribute('data-col') === '-1' ? "row" : "cell");
547554
startCell = target; endCell = target; isSelecting = true; e.preventDefault();
555+
target.focus();
548556
});
549557
table.addEventListener('mousemove', e => {
550558
if(!isSelecting) return;
@@ -682,6 +690,56 @@ class CsvEditorProvider implements vscode.CustomTextEditorProvider {
682690
if ((e.ctrlKey || e.metaKey) && e.key === 'c' && currentSelection.length > 0) {
683691
e.preventDefault(); copySelectionToClipboard(); return;
684692
}
693+
694+
/* ──────── NEW: ENTER + DIRECT TYPING HANDLERS ──────── */
695+
if (!editingCell && anchorCell && currentSelection.length === 1) {
696+
/* Enter opens edit mode */
697+
if (e.key === 'Enter') {
698+
e.preventDefault();
699+
editCell(anchorCell);
700+
return;
701+
}
702+
/* Typing clears cell and opens edit mode */
703+
if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
704+
e.preventDefault();
705+
const cell = anchorCell;
706+
editCell(cell);
707+
cell.innerText = '';
708+
if (document.execCommand) {
709+
document.execCommand('insertText', false, e.key);
710+
} else {
711+
cell.innerText = e.key;
712+
}
713+
setCursorToEnd(cell);
714+
return;
715+
}
716+
}
717+
718+
/* ──────── ARROW KEY NAVIGATION ──────── */
719+
if (!editingCell && anchorCell && ['ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(e.key)) {
720+
const { row, col } = getCellCoords(anchorCell);
721+
let targetRow = row, targetCol = col;
722+
switch(e.key){
723+
case 'ArrowUp': targetRow = row - 1; break;
724+
case 'ArrowDown': targetRow = row + 1; break;
725+
case 'ArrowLeft': targetCol = col - 1; break;
726+
case 'ArrowRight':targetCol = col + 1; break;
727+
}
728+
if(targetRow < 0 || targetCol < 0) return;
729+
const tag = (hasHeader && targetRow === 0 ? 'th' : 'td');
730+
const nextCell = table.querySelector(\`\${tag}[data-row="\${targetRow}"][data-col="\${targetCol}"]\`);
731+
if(nextCell){
732+
e.preventDefault();
733+
clearSelection();
734+
nextCell.classList.add('selected');
735+
currentSelection.push(nextCell);
736+
anchorCell = nextCell;
737+
nextCell.focus({preventScroll:true});
738+
nextCell.scrollIntoView({ block:'nearest', inline:'nearest', behavior:'smooth' });
739+
}
740+
return;
741+
}
742+
685743
if (editingCell && ((e.ctrlKey || e.metaKey) && e.key === 's')) {
686744
e.preventDefault();
687745
editingCell.blur();
@@ -700,10 +758,8 @@ class CsvEditorProvider implements vscode.CustomTextEditorProvider {
700758
editingCell.blur();
701759
let nextCell;
702760
if (e.shiftKey) {
703-
// Shift+Tab: move to the previous cell (decrement column index)
704761
nextCell = table.querySelector('td[data-row="'+row+'"][data-col="'+(col-1)+'"]');
705762
} else {
706-
// Tab: move to the next cell (increment column index)
707763
nextCell = table.querySelector('td[data-row="'+row+'"][data-col="'+(col+1)+'"]');
708764
}
709765
if (nextCell) {
@@ -713,7 +769,6 @@ class CsvEditorProvider implements vscode.CustomTextEditorProvider {
713769
if (editingCell && e.key === 'Escape') {
714770
e.preventDefault(); editingCell.innerText = originalCellValue; editingCell.blur();
715771
}
716-
// If not editing, pressing Escape clears the selection.
717772
if (!editingCell && e.key === 'Escape') {
718773
clearSelection();
719774
}
@@ -738,7 +793,6 @@ class CsvEditorProvider implements vscode.CustomTextEditorProvider {
738793
cell.classList.add('editing');
739794
cell.setAttribute('contenteditable', 'true');
740795
cell.focus();
741-
// Attach a blur handler to commit cell changes.
742796
const onBlurHandler = () => {
743797
const value = cell.innerText;
744798
const coords = getCellCoords(cell);
@@ -771,15 +825,16 @@ class CsvEditorProvider implements vscode.CustomTextEditorProvider {
771825
};
772826
window.addEventListener('message', event => {
773827
const message = event.data;
774-
if(message.type === 'updateCell'){
828+
if(message.type === 'focus'){
829+
document.body.focus();
830+
} else if(message.type === 'updateCell'){
775831
isUpdating = true;
776832
const { row, col, value } = message;
777833
const cell = table.querySelector('td[data-row="'+row+'"][data-col="'+col+'"]');
778834
if(cell){ cell.innerText = value; }
779835
isUpdating = false;
780836
}
781837
});
782-
// Global Escape handler to clear selection when not editing.
783838
document.addEventListener('keydown', e => {
784839
if(!editingCell && e.key === 'Escape'){
785840
clearSelection();

tsconfig.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"types": ["node", "vscode", "mocha"],
1010
"skipLibCheck": true
1111
},
12-
"include": ["./**/*.ts"],
13-
"exclude": ["node_modules"]
12+
"include": ["./**/*.ts"],
13+
"exclude": ["node_modules"]
1414
}
15+

0 commit comments

Comments
 (0)