fix: keep arrow keys and tab inside editor mode

This commit is contained in:
Erik Kotsiuba
2026-04-19 16:32:27 +02:00
parent bc92712053
commit cdea90bbfe
5 changed files with 46 additions and 26 deletions
+2 -1
View File
@@ -102,7 +102,8 @@ Editing flow:
- `Enter`: save and submit the current input - `Enter`: save and submit the current input
- `Esc`: save draft and close the editor - `Esc`: save draft and close the editor
- `Up`, `Down`, `Tab`, `Shift+Tab`: save draft and continue navigation - arrow keys and `Tab` stay in the editor so the cursor can move while typing
- navigation resumes only after closing the editor with `Esc`
## Non-interactive mode ## Non-interactive mode
+2 -2
View File
@@ -16,8 +16,8 @@ export const UI_TEXT = {
reviewTitle: "Review your answers", reviewTitle: "Review your answers",
readyToSubmit: "Ready to submit your answers?", readyToSubmit: "Ready to submit your answers?",
unanswered: "→ (unanswered)", unanswered: "→ (unanswered)",
footerInput: " Enter submit · Esc save and close · Tab/←→ save and navigate", footerInput: " Enter submit · Esc save and close",
footerNote: " Enter save · Esc save and close · Tab/←→ save and navigate", footerNote: " Enter save · Esc save and close",
footerSubmit: " ↑↓ select · Enter confirm · Esc cancel", footerSubmit: " ↑↓ select · Enter confirm · Esc cancel",
footerMulti: footerMulti:
" ⇆ tab · ↑↓ select · Space toggle · Enter continue · N option note · Ctrl+N question note · Esc dismiss · 1-9 quick toggle", " ⇆ tab · ↑↓ select · Space toggle · Enter continue · N option note · Ctrl+N question note · Esc dismiss · 1-9 quick toggle",
-12
View File
@@ -42,18 +42,6 @@ export function formatKeybindingLabel(key: string): string {
} }
function getEditingInputCommand(data: string): AskInputCommand { function getEditingInputCommand(data: string): AskInputCommand {
if (matchesKey(data, Key.tab) || matchesKey(data, Key.right)) {
return { kind: "editMoveTab", delta: 1 };
}
if (matchesKey(data, Key.shift("tab")) || matchesKey(data, Key.left)) {
return { kind: "editMoveTab", delta: -1 };
}
if (matchesKey(data, Key.up)) {
return { kind: "editMoveOption", delta: -1 };
}
if (matchesKey(data, Key.down)) {
return { kind: "editMoveOption", delta: 1 };
}
if (matchesKey(data, Key.escape)) { if (matchesKey(data, Key.escape)) {
return { kind: "editClose" }; return { kind: "editClose" };
} }
+33 -10
View File
@@ -21,19 +21,35 @@ function inputState() {
return state; return state;
} }
test("editing views let up and down move navigation", () => { test("typing mode delegates arrow keys to the editor", () => {
const input = inputState(); const input = inputState();
assert.deepEqual(getInputCommand(input, "\x1b[A"), { assert.deepEqual(getInputCommand(input, "\x1b[A"), {
kind: "editMoveOption", kind: "delegateToEditor",
delta: -1,
}); });
assert.deepEqual(getInputCommand(input, "\x1b[B"), { assert.deepEqual(getInputCommand(input, "\x1b[B"), {
kind: "editMoveOption", kind: "delegateToEditor",
delta: 1, });
assert.deepEqual(getInputCommand(input, "\x1b[C"), {
kind: "delegateToEditor",
});
assert.deepEqual(getInputCommand(input, "\x1b[D"), {
kind: "delegateToEditor",
}); });
}); });
test("note editing views let up and down move navigation", () => { test("typing mode delegates tab to the editor", () => {
const input = inputState();
assert.deepEqual(getInputCommand(input, "\t"), {
kind: "delegateToEditor",
});
assert.deepEqual(getInputCommand(input, "\x1b[Z"), {
kind: "delegateToEditor",
});
});
test("note editing mode delegates arrows and tab to the editor", () => {
const state = enterQuestionNoteMode( const state = enterQuestionNoteMode(
createInitialState({ createInitialState({
questions: [ questions: [
@@ -48,11 +64,18 @@ test("note editing views let up and down move navigation", () => {
); );
assert.deepEqual(getInputCommand(state, "\x1b[A"), { assert.deepEqual(getInputCommand(state, "\x1b[A"), {
kind: "editMoveOption", kind: "delegateToEditor",
delta: -1,
}); });
assert.deepEqual(getInputCommand(state, "\x1b[B"), { assert.deepEqual(getInputCommand(state, "\x1b[B"), {
kind: "editMoveOption", kind: "delegateToEditor",
delta: 1, });
assert.deepEqual(getInputCommand(state, "\x1b[C"), {
kind: "delegateToEditor",
});
assert.deepEqual(getInputCommand(state, "\x1b[D"), {
kind: "delegateToEditor",
});
assert.deepEqual(getInputCommand(state, "\t"), {
kind: "delegateToEditor",
}); });
}); });
+9 -1
View File
@@ -1,6 +1,9 @@
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import test from "node:test"; import test from "node:test";
import { renderEditorBlock } from "../src/ui/render-helpers.ts"; import {
renderEditorBlock,
renderFooterText,
} from "../src/ui/render-helpers.ts";
const ABC_PATTERN = /abc/; const ABC_PATTERN = /abc/;
const BORDER_PATTERN = /┌|└/; const BORDER_PATTERN = /┌|└/;
@@ -69,3 +72,8 @@ test("renderEditorBlock reapplies background after editor reset sequences", () =
assert.match(lines[0], SELECTED_BG_PATTERN); assert.match(lines[0], SELECTED_BG_PATTERN);
assert(lines[0].includes("\x1b[0m{selectedBg}")); assert(lines[0].includes("\x1b[0m{selectedBg}"));
}); });
test("editing footers do not advertise tab navigation", () => {
assert.equal(renderFooterText("input"), " Enter submit · Esc save and close");
assert.equal(renderFooterText("note"), " Enter save · Esc save and close");
});