Skip to content

Conversation

@xerial
Copy link
Member

@xerial xerial commented Feb 5, 2026

Summary

  • Add Keyboard object for global and scoped keyboard shortcut handling with modifier key tracking
  • Add Focus object for accessibility features: focus trapping, auto-focus, focus restoration, and active element tracking
  • Add RxElement.empty for convenience when creating empty reactive elements
  • Export new types (Keyboard, KeyCombination, Modifiers, Focus) from wvlet.uni.dom.all

Features

Keyboard Shortcuts

import wvlet.uni.dom.all.*

// Global shortcuts
val cancel = Keyboard.bind("ctrl+s", () => save())

// Multiple shortcuts
val cancel = Keyboard.bindAll(
  "ctrl+s" -> (() => save()),
  "escape" -> (() => closeModal()),
  "ctrl+shift+p" -> (() => openCommandPalette())
)

// Scoped shortcuts (only when element has focus)
input(Keyboard.scoped("enter" -> (() => submit())))

// Track modifier state
Keyboard.modifiers.map { mods =>
  if mods.shift then "Multi-select mode" else "Single select"
}

Focus Management

// Focus trap for modals
Focus.trap(
  div(cls -> "modal",
    input(Focus.onMount, placeholder -> "Auto-focused"),
    button("Submit"),
    button(onclick -> (() => close()), "Close")
  )
)

// Focus restoration
val restore = Focus.saveAndRestore()
// ... do something that changes focus
restore() // returns focus to original element

// Track active element
Focus.active.map(_.map(_.tagName).getOrElse("none"))

Test plan

  • All 240 tests pass including 22 keyboard tests and 17 focus tests
  • Compilation successful
  • Code formatted with scalafmtAll

🤖 Generated with Claude Code

Add two complementary features for building accessible, keyboard-navigable UIs:

Keyboard Shortcuts:
- Global shortcuts with Keyboard.bind() and Keyboard.bindAll()
- Scoped shortcuts with Keyboard.scoped() for element-specific bindings
- Modifier key tracking with Keyboard.modifiers and Keyboard.isPressed()
- KeyCombination parsing for flexible shortcut strings (ctrl+s, cmd+k, etc.)

Focus Management:
- Focus trapping with Focus.trap() for modals and dialogs
- Auto-focus on mount with Focus.onMount and Focus.onMountDelayed()
- Focus restoration with Focus.saveAndRestore() and Focus.withRestoration()
- Active element tracking with Focus.active reactive stream
- Programmatic focus via Focus.focusById() and Focus.blur()
- Utility for finding focusable elements with Focus.getFocusableElements()

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@github-actions github-actions bot added doc Improvements or additions to documentation feature New feature labels Feb 5, 2026
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @xerial, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the uni-dom library by integrating robust keyboard shortcut handling and advanced focus management capabilities. These additions aim to improve the accessibility and user experience of applications built with uni-dom, allowing developers to easily implement complex keyboard interactions and manage focus flow for better navigation and usability.

Highlights

  • Keyboard Shortcut Management: Implemented a new Keyboard object providing functionalities for global and scoped keyboard shortcut handling, including tracking of modifier key states.
  • Focus Management: Introduced a Focus object to enhance accessibility with features such as focus trapping within elements, auto-focusing on mount, and restoring focus to previous elements.
  • Utility for Empty Reactive Elements: Added RxElement.empty for convenience when creating reactive elements that render nothing.
  • API Exports: The new Keyboard, KeyCombination, Modifiers, and Focus types are now exported through wvlet.uni.dom.all for easy access.
Changelog
  • plans/2026-02-04-keyboard-focus.md
    • Added a new design document outlining the API, usage, implementation details, and verification for the new keyboard and focus features.
  • uni-dom-test/src/test/scala/wvlet/uni/dom/FocusTest.scala
    • Added comprehensive unit tests for the Focus object, covering its various functionalities like trap, onMount, saveAndRestore, and element identification.
  • uni-dom-test/src/test/scala/wvlet/uni/dom/KeyboardTest.scala
    • Introduced new unit tests for the Keyboard object, validating KeyCombination parsing, modifier state tracking, and shortcut binding mechanisms.
  • uni/.js/src/main/scala/wvlet/uni/dom/Focus.scala
    • Implemented the core Focus object, FocusTrap, and FocusOnMount classes, providing the logic for focus management features.
  • uni/.js/src/main/scala/wvlet/uni/dom/Keyboard.scala
    • Implemented the core Keyboard object, Modifiers, and KeyCombination classes, enabling global and scoped keyboard shortcut functionality.
  • uni/.js/src/main/scala/wvlet/uni/dom/RxElement.scala
    • Added a new empty value to the RxElement companion object for convenience in rendering empty reactive elements.
  • uni/.js/src/main/scala/wvlet/uni/dom/all.scala
    • Exported the newly introduced Keyboard, KeyCombination, Modifiers, and Focus types to the all object, making them readily available for use.
Activity
  • All 240 existing tests, including 22 new keyboard tests and 17 new focus tests, are passing.
  • The project successfully compiles.
  • The code has been formatted using scalafmtAll.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces valuable keyboard shortcut and focus management features to uni-dom. The code is well-structured, follows existing patterns, and includes a good set of tests. I've provided a couple of suggestions to enhance the robustness of the focus management logic by considering element visibility and to strengthen a related test case. Overall, this is an excellent contribution.

Comment on lines 88 to 90
(
dom.document.activeElement == dom.document.body || dom.document.activeElement == testInput
) shouldBe true
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This test assertion is a bit weak. If Focus.blur() has no effect, dom.document.activeElement would remain testInput, and the overall condition ... || dom.document.activeElement == testInput would be true, causing the test to pass incorrectly.

A stronger test would explicitly assert that the focus is no longer on the input element and has moved to the body element.

      (dom.document.activeElement == testInput) shouldBe false
      // After blur, activeElement should be body (or null in some browsers)
      dom.document.activeElement shouldBe dom.document.body

"button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), " +
"textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"]):not([disabled])"
val nodeList = container.querySelectorAll(selector)
(0 until nodeList.length).map(nodeList(_))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

getFocusableElements doesn't account for element visibility. Hidden elements (e.g., with display: none or visibility: hidden) will be included in the focusable list, but users cannot interact with them. This can cause Focus.trap to attempt to focus an invisible element, leading to a poor user experience.

To make this more robust, you should filter out elements that are not visible. A common technique is to check if the element has a positive offsetWidth or offsetHeight.

    (0 until nodeList.length).map(nodeList(_)).filter {
      case elem: dom.HTMLElement =>
        elem.offsetWidth > 0 || elem.offsetHeight > 0 || elem.getClientRects().length > 0
      case _ =>
        // For non-HTMLElements (like SVG), assume they are visible if selected.
        true
    }

xerial and others added 2 commits February 4, 2026 16:46
- Fix RxElement.empty infinite recursion by returning Embedded(DomNode.empty)
- Focus trap now focuses container when no focusable children exist

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Strengthen Focus.blur test to verify focus actually moves away from input
- Filter invisible elements (display:none, visibility:hidden) from focusable list

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@xerial xerial merged commit ceeb28e into main Feb 5, 2026
13 checks passed
@xerial xerial deleted the feature/keyboard-shortcuts branch February 5, 2026 01:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

doc Improvements or additions to documentation feature New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant