Public: anyone can read this; the crew can change it.
The live region keeps your place: focus, an announcement, and what someone is typing #56
Check · Review · Land · Live done — Landed and live in production.
`static/live.js` no longer throws away the person using the page when a region is swapped. Before `el.replaceWith(next)` it records what has focus inside the region — by `id`, else by `name` — along with the caret, and afterwards hands both back to the same control in the fresh markup; when that control is gone, focus lands on the region's heading (given `tabindex="-1"` for the moment) instead of…
shipped production is serving it deploy 2adbe7d
Sign in to join this conversation.
static/live.jsno longer throws away the person using the page when a region is swapped. Beforeel.replaceWith(next)it records what has focus inside the region — byid, else byname— along with the caret, and afterwards hands both back to the same control in the fresh markup; when that control is gone, focus lands on the region's heading (giventabindex="-1"for the moment) instead of falling to<body>, and every focus call passespreventScroll: true, so a region that grows moves nothing. An<input>,<textarea>or<select>that holds focus and holds a value the server's copy does not carry keeps its whole region as it is — the swap returns "kept" — and a one-shotblurlistener refreshes it the moment they leave. Each of the three live regions (#proposals,#proposal,#thread) is nowaria-live="polite"and points withdata-statusat a one-linerole="status"paragraph outside it: outside because a region is replaced wholesale and an announcement replaced along with it is a new node some readers never speak. The text issummarize(type)insrc/live.tsand rides the event stream as a third field, so the sentence is always the app's own ("A new remark", "A vote was cast" — the vote and roster lines are there already so #44 and #38 inherit them).static/app.cssgains one.offscreenclass (clipped, notdisplay: none), used for the status lines and for the two headings the regions previously lacked; the thread's buttons gained ids so focus can find them again.To look at:
swap,spotOf,typingandrestoreinstatic/live.js— exported, becausebun testhosts no DOM and this app takes no dependency to get one, sosrc/focus.test.tsdrives the real functions over a stub of the handful of properties they touch (children, id, name, value, caret, focus), and the file's stream only starts whendocumentandEventSourceexist. The live region markup is pinned server-side insrc/server.test.ts; I updated the existing assertion there that spelled out the old<section id="thread" …>tag, since the attributes on it are exactly what this issue changes.bun testis green (119) andbun run src/db.ts --checkreports version 7, integrity ok; the home page still answers in about 4 ms and renders once, server-side, withlive.jsstill pure enhancement.What I did not do: no diffing, no framework, no second file —
live.jswent from 68 to 211 lines, about half of that comments, and each function is one concept, so it still reads in one sitting; if it grows again the focus half should move out. I also did not verify with a real screen reader, and today no live region actually contains a form control (the composers sit outside them), so the typing guard is proved by unit test and by the markup rather than by hand on the preview — it earns its keep when #38 puts reply forms and #44 puts vote buttons inside these regions.🤖 Generated with Claude Code
Closes #49