diff --git a/.changes/planning/fix-surface-journey-radio-focus-7d2e4b91.md b/.changes/planning/fix-surface-journey-radio-focus-7d2e4b91.md new file mode 100644 index 00000000000..cf891cc5c2d --- /dev/null +++ b/.changes/planning/fix-surface-journey-radio-focus-7d2e4b91.md @@ -0,0 +1,7 @@ +--- +bump: patch +--- + +### Fixed + +- The interview page keeps keyboard focus on a choice when a state push redraws the choices. Before, focus fell back to the page whenever Claude's next update arrived while a choice had focus, for example right after Answer again, so the arrow keys stopped moving between the choices. diff --git a/plugins/planning/surface/index.html b/plugins/planning/surface/index.html index c893a20eb0f..be324f94881 100644 --- a/plugins/planning/surface/index.html +++ b/plugins/planning/surface/index.html @@ -1700,10 +1700,10 @@

Interview

setHtml($("sumRest"), h); restoreFocus(f); } - function focusKey(){ // the focused control, by id or by its data attributes within the nearest region with an id + function focusKey(){ // the focused control, by id or by its data attributes (a radio by name and value) within the nearest region with an id const a = document.activeElement; if (!a || a === document.body || !a.closest) return null; if (a.id) return {s: "#" + CSS.escape(a.id), start: a.selectionStart, end: a.selectionEnd}; - const attrs = [...a.attributes].filter(x => x.name.startsWith("data-")).map(x => "[" + x.name + '="' + CSS.escape(x.value) + '"]').join(""), box = a.closest("[id]"); + const attrs = [...a.attributes].filter(x => x.name.startsWith("data-") || (a.type === "radio" && (x.name === "name" || x.name === "value"))).map(x => "[" + x.name + '="' + CSS.escape(x.value) + '"]').join(""), box = a.closest("[id]"); return attrs && box ? {s: "#" + CSS.escape(box.id) + " " + a.tagName.toLowerCase() + attrs, pin: !!a.closest(".sec.pin")} : null; } function restoreFocus(f){ diff --git a/plugins/planning/surface/tests/ui_journey.js b/plugins/planning/surface/tests/ui_journey.js index e9b347f54fb..568d209813f 100644 --- a/plugins/planning/surface/tests/ui_journey.js +++ b/plugins/planning/surface/tests/ui_journey.js @@ -240,7 +240,9 @@ async page => { // the user journey in order on one page, no reload after phase ok("Answer again moves focus to the first choice", await page.evaluate(() => document.activeElement.matches('#choices input[type="radio"]'))); // set up the notice checks: a note for Claude to answer, then the summary with a hiding filter and a hidden rail - await post({kind: "note", text: "Is the plan on track?"}); + const r0 = await page.evaluate(() => document.activeElement.value); + await post({kind: "note", text: "Is the plan on track?"}); await settle(2000); + ok("a state push that rebuilds the choices keeps focus on the same radio", await page.evaluate(v => document.activeElement.matches('#choices input[type="radio"]') && document.activeElement.value === v, r0), await focused()); await page.selectOption("#filter", "answered"); await page.click("#title"); await page.keyboard.press("w"); await page.keyboard.press("["); await settle(300); ok("setup: the summary shows with the Answered filter and the rail hidden", await sel() === "summary" && await page.$eval("#layout", el => el.classList.contains("norail")), await sel()); }