Add opt-in lvt-mod:skip-when-typing guard for window keyboard bindings#137
Conversation
Global lvt-on:window:keydown shortcuts previously fired even while the user was typing in a text field, so a single-letter shortcut (e.g. "j") could not coexist with text entry. Add an opt-in lvt-mod:skip-when-typing attribute: when present, the binding is suppressed if document.activeElement is (or is within) an editable element — text-like input, textarea, select, or contenteditable. Button/checkbox/radio inputs are not treated as editable, and bindings without the attribute (e.g. Escape-to-cancel) still fire while typing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014bnEwkKH6E9q2QQnqj3cWe
|
Review: opt-in Overall clean — logic is correct, tests cover the key cases, and the opt-in design is the right call. A few things worth knowing: Shadow DOM gap (minor)
No functional bugs found. Tests are thorough. Good to merge once the shadow DOM question is answered for your use case. |
Addresses the Claude review on #137: document.activeElement returns the shadow HOST when focus is inside a web component's shadow root, so isEditableTarget would miss a focused input there and let a shortcut fire while the user types. Add deepActiveElement(), which descends through shadowRoot.activeElement (recursively, for nested shadow DOM), and use it in the window-keydown guard. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014bnEwkKH6E9q2QQnqj3cWe
ReviewGood feature overall — the opt-in design is the right call, shadow-DOM traversal is handled correctly, and the test coverage is solid. A few things worth addressing: Test DOM cleanup (no cleanup → test pollution)
afterEach(() => {
document.querySelectorAll("[data-lvt-id]").forEach((el) => el.remove());
});
|
- Exclude input[type="image"] from EDITABLE_SELECTOR — it's a submit-style button, not a text field, so shortcuts should fire while it's focused. - Add a test confirming the skip-when-typing guard also applies to keyup bindings (the guard already covers keydown and keyup; this pins it). The reviewer's third point (test DOM cleanup) is already handled: the suite's beforeEach/afterEach both reset document.body.innerHTML, which removes appended nodes including the shadow host. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014bnEwkKH6E9q2QQnqj3cWe
|
Clean implementation with thorough test coverage. A few observations: Logic is correct. The opt-in design (guard only fires when Minor type imprecision —
No bugs or security issues. LGTM. |
…input - isEditableTarget now takes Element | null (its only caller passes deepActiveElement()'s Element | null result); the instanceof guard is kept. - Document that input[type="range"] is intentionally treated as editable so arrow-bound shortcuts yield to a focused slider. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014bnEwkKH6E9q2QQnqj3cWe
|
Code review — PR #137 (skip-when-typing guard) Overall this is well-implemented and the test coverage is solid. Two actionable findings: 1. range input behavior is asserted in comments but not tested The comment explicitly states range inputs are intentionally left in scope (so arrow-key shortcuts are suppressed while a slider is focused). That is a non-obvious policy decision worth pinning with a test. Without one, a future reader who finds the omission surprising may "fix" it and break the intended UX. 2. Test DOM leaks between cases
No bugs or security issues found. The |
Adds a test asserting an arrow-bound skip-when-typing shortcut is suppressed while a range input is focused, so the slider consumes the arrow — pinning the documented intentional inclusion of range in EDITABLE_SELECTOR. (The reviewer's second point — test DOM cleanup — is already covered by the suite's top-level beforeEach/afterEach, both of which reset document.body.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014bnEwkKH6E9q2QQnqj3cWe
ReviewLogic: guard is silently skipped when The <!-- No lvt-key → guard never runs, shortcut fires while typing -->
<div lvt-on:window:keydown="nextFile" lvt-mod:skip-when-typing></div>That combination is admittedly unusual (a binding that fires on every keydown), but the dependency is invisible to consumers. Two options:
Minor gap: Chrome supports Everything else looks good — the shadow-DOM traversal is correct, the opt-in design is the right call, tests cover the key cases, and the selector exclusions for button/checkbox/radio inputs are well-reasoned. The range-input intentional inclusion is clearly documented. |
…only CE Addresses #137 review: - The guard lived inside the `hasAttribute("lvt-key")` block, so a binding with `lvt-mod:skip-when-typing` but no `lvt-key` was silently never guarded. Move the guard out so it applies to any keydown/keyup binding that opts in, regardless of a key filter (test added). - Match `contenteditable="plaintext-only"` (a distinct editable mode in Chrome) in EDITABLE_SELECTOR. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014bnEwkKH6E9q2QQnqj3cWe
|
Overall: looks good — the opt-in design is correct and tests are thorough. Two things worth addressing before merge: 1.
'input:not([type="button"]):not([type="submit"]):not([type="reset"])'
+ ':not([type="checkbox"]):not([type="radio"]):not([type="image"])'
+ ':not([type="file"])',2. The comment explicitly calls out that No other issues. The |
- file inputs are button-like (open a picker, take no typed text) — exclude them so shortcuts fire while one is focused, consistent with button/image. - Clarify the rationale comment that input[type=number] is intentionally editable (you type digits and arrows step the value). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014bnEwkKH6E9q2QQnqj3cWe
ReviewClean implementation with solid test coverage. A few things worth noting: Minor bug: color input unintentionally treated as editable The exclusion list covers button/submit/reset/checkbox/radio/image/file, but not Redundant instanceof check in isEditableTarget The parameter is already typed as Everything else looks good:
The color input exclusion is the only real issue worth fixing before merge. |
…get (#137) - color inputs open a picker and accept no typed text — exclude them from the editable set (the last non-text input type, alongside button/file/image/etc). - isEditableTarget's param is Element | null, so the instanceof Element check only guarded null; use node !== null. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014bnEwkKH6E9q2QQnqj3cWe
Picks up the merged livetemplate/client#137 (skip-when-typing guard) including the review hardening: shadow-DOM deep active element, non-text input-type exclusions (image/file/color), contenteditable=plaintext-only, and the guard applying independent of lvt-key. Keyboard e2e suite re-verified green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014bnEwkKH6E9q2QQnqj3cWe
…ity (#64) * Add keyboard shortcuts, help overlay, and composer autofocus Resolves #28 (review-side keyboard operability). Every common review action now has a keyboard shortcut, single-sourced from internal/review/keymap.go: the keyBindings slice renders BOTH the hidden lvt-on:window:keydown elements and the rows of a "?" help overlay, so the live bindings and their documentation can't drift. Shortcuts (all skip-when-typing so they don't fire in a text field): j / ↓ next file k / ↑ prev file n / p next / prev comment c comment on file f toggle file tree a all comments r show/hide resolved . focus mode ? keyboard help Esc (cancel / close) and Enter (save) round out the help reference. Details: - Esc is a guard-free global window binding rendered as a child of <body>, so it cancels the composer even while the textarea is focused and works in both repo and external mode. (An attribute on <body> itself would be missed: the window-binding scan is wrapper.querySelectorAll, which excludes the wrapper.) - The composer textarea gets lvt-autofocus so the cursor lands there when it opens — keyboard users can type immediately. - New controller handlers: ToggleKeyboardHelp, NextComment/PrevComment (step state.VisibleComments in all-comments order, wrap, switch file as needed). ClearSelection also closes the help overlay. - Toolbar "?" button + mobile more-menu item, both dispatch toggleKeyboardHelp. - Embeds the patched livetemplate client (lvt-mod:skip-when-typing guard). Relies on the upstream lvt-mod:skip-when-typing primitive. The "/" filter-focus accelerator is deferred (focusing an always-visible input on keypress needs a focus-on-command primitive; the filter stays Tab-reachable). Tests: keymap/controller unit tests (internal/review/keyboard_test.go), TestTemplateOutputSignature golden regenerated, and a chromedp e2e (e2e/e2e_keyboard_test.go) covering nav, the typing guard, Esc-to-cancel, and the help overlay with all four debug signals. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014bnEwkKH6E9q2QQnqj3cWe * Make rendered markdown blocks keyboard-operable Phase 3 focusability audit: the review flow is already almost entirely native focusables (line/file-tree/toolbar buttons, base <select>, filter <input>, TOC <a>, external pins). The one gap was selectBlock on the rendered-markdown block, a <div class="md-rendered"> that holds arbitrary HTML and so can't be a native <button>. Give it role="button" tabindex="0" and an Enter keydown bound to selectBlock, so a keyboard user can focus a rendered block and comment on it — the keyboard equivalent of clicking it. (Enter only, not Space, which would fight page-scroll without a custom preventDefault.) Drag-only sub-region selection (HTML region-select, image area-select) stays mouse-only by design; the keyboard fallback for those file types is the "c" file-level comment, already noted in the help overlay. Adds TestE2E_KeyboardMarkdownBlock (focusable + Enter opens the composer) and regenerates the template signature golden. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014bnEwkKH6E9q2QQnqj3cWe * Add keyboard comment-lifecycle e2e (save + resolve) Proves the core of issue #28 end-to-end: a reviewer authors AND saves a comment with only the keyboard ("c" → type → Tab to Save → Enter, CSV-verified), then resolves it from the keyboard. Records that focus lands on <body> after the composer closes — a noted ergonomics follow-up (the user isn't stranded: all global shortcuts work from body), not a blocker for keyboard operability. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014bnEwkKH6E9q2QQnqj3cWe * Fix keyboard-review issues found in manual testing Four fixes surfaced while testing the keyboard support on a live binary: 1. Can't switch files after a line comment (the serious one). Saving a LINE comment left the composer stuck open with focus trapped in the textarea, so every skip-when-typing shortcut was suppressed until reload. Root cause: the conditional composer slot inside the non-keyed line-row in the VisibleLines range wasn't cleared by the patch when the selection cleared (the file composer, in a stable section, closed fine). Fix: key the line-row div with a "-composing" marker so it's cleanly replaced when the composer toggles. lvt-form:preserve is kept (no draft loss, no per-keystroke round-trips). 2. Pressing "r" with no resolved comments did nothing. Add a Flash state field + auto-dismissing toast ("No resolved comments"); ToggleShowResolved sets it instead of silently no-op'ing. New ClearFlash handler. 3. "f" did nothing on desktop (the file tree is a persistent sidebar there). Add desktop CSS so toggleFiles collapses the sidebar and reveals the hamburger as the reopen affordance (FileDrawerOpen means "open" on mobile, "collapsed" on desktop — read oppositely per media query). 4. Focus landed on <body> after saving a comment. AddComment (all add-paths) now sets ScrollToCommentID to the saved comment, and the comment-card templates gained tabindex=-1 + lvt-autofocus on that match, so focus (and scroll) land on the saved comment. Heading nav and comment scroll are made mutually exclusive (each clears the other) so they don't fight — covered by the existing TOC-navigation e2e. Tests: new unit tests (flash, clearFlash) and e2e (TestE2E_LineCommentClosesComposerAndNavigates, TestE2E_ShowResolvedFlash, TestE2E_DesktopFilesToggle) + the comment-lifecycle test now asserts focus lands on the saved comment. Full browser e2e suite green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014bnEwkKH6E9q2QQnqj3cWe * Fix blue background on rendered markdown blocks Phase 3 added role="button" to the .md-rendered blocks for keyboard accessibility, but Pico CSS paints every [role=button] as a filled primary (blue) button — so each rendered markdown block got a blue background and white text. Drop role="button"; keep tabindex="0" + the Enter keydown, so the block stays keyboard-focusable and Enter-activatable (no Pico button styling). Pico does not style bare [tabindex], so the prose renders normally again. TestE2E_KeyboardMarkdownBlock still passes (asserts tabindex 0 + Enter opens the composer; no role assertion). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014bnEwkKH6E9q2QQnqj3cWe * Add keyboard line cursor (arrow keys) for commenting on a line Commenting on a specific line previously required Tab-ing focus through the whole UI to a line button. Reassign ArrowUp/ArrowDown from file navigation to a diff line cursor: CursorUp/CursorDown walk the visible diff lines (skipping fold markers), and the cursor line is highlighted (.is-cursor), scrolled into view, and focused (lvt-autofocus) so Enter activates it → the line composer. j/k keep switching files; the cursor resets when the file changes. Keymap/help updated (↑/↓ = move line cursor, Enter = comment on cursor line); new TestE2E_KeyboardLineCursor; the obsolete arrow-switches-files assertion was removed from TestE2E_KeyboardShortcuts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014bnEwkKH6E9q2QQnqj3cWe * Re-sync embedded client JS from merged client main Picks up the merged livetemplate/client#137 (skip-when-typing guard) including the review hardening: shadow-DOM deep active element, non-text input-type exclusions (image/file/color), contenteditable=plaintext-only, and the guard applying independent of lvt-key. Keyboard e2e suite re-verified green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014bnEwkKH6E9q2QQnqj3cWe --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
What
Adds an opt-in
lvt-mod:skip-when-typingmodifier forlvt-on:window:keydown/keyupbindings. When present, the binding does not fire ifdocument.activeElementis (or is within) an editable element — a text-like<input>,<textarea>,<select>, or[contenteditable]. Button/checkbox/radio inputs are not treated as editable, so shortcuts still fire when one is focused.Why
Global single-key shortcuts (e.g.
j/kto navigate) previously fired even while the user was typing in a text field, so a shortcut letter couldn't coexist with text entry. The guard is opt-in so that bindings which must fire while typing (e.g.Escapeto cancel) simply omit the attribute and keep working.Consumed downstream by prereview's keyboard-support feature (livetemplate/prereview#28).
Tests
tests/event-delegation.test.tscovers: opted-in binding suppressed in textarea/input/contenteditable; fires on non-editable focus; and a binding without the opt-in still fires while a textarea is focused. Full suite green.🤖 Generated with Claude Code