Skip to content

menu: Focus a one-item menu's item when the keyboard opens it - #1017

Closed
dXnSix wants to merge 1 commit into
longbridge:mainfrom
dXnSix:fix/menu-one-item-keyboard-focus
Closed

dXnSix wants to merge 1 commit into
longbridge:mainfrom
dXnSix:fix/menu-one-item-keyboard-focus

Conversation

@dXnSix

@dXnSix dXnSix commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Description

A menu opened from the keyboard puts the keyboard focus on an item, as WAI-ARIA has it: its first enabled one
(Return, Space or Down on a Menu's trigger; Shift+F10 or the Menu key on a ContextMenu's, #978; Right, Return or
Space on a Submenu's), or its last (Up). A menu of one item got no focus at all, so the next key went to the
trigger. context-menu-press.test.tsx › "Shift+F10 on the focused trigger opens the menu at its first item" opens a
one-item context menu, and its Space presses the trigger ("tapped: solo") instead of choosing the item: it fails the
Rust application workflow's web job (the gallery's tests, headless) on main and on every pull request since
#978.

The menu picks the item in the handler of its order watch (watch_order, since #869), once its items are in the
tree's order. The core tells that handler the order only when it differs from the order the items registered in
(#702: "in order already: nothing to tell, no round to run"). A menu's items register last-first, so a menu of
several items is always out of order and its handler runs; a single item is always in order, the handler never runs
and nothing is focused. A submenu's focus_first asks for the pick with invalidate() the same way.

OrderWatch gains request(): as invalidate(), and the core tells the handler the order at the end of the round
even when it is the list's own. The menu requests the order instead of invalidating it while an opening from the
keyboard waits for its item (items added then, a submenu's focus_first). The other watches (the roving focus of
toolbars, tabs, radios and toggle groups; the carousel) only sort in their handlers and keep skipping orders that
are in order already: in toolbar.test.tsx and tree.test.tsx, 38 of their looks are, and making every
invalidation tell would cost each of those a handler run and a commit round.

In Chrome the test needs one more change, in the web runtime (Chromium's contextmenu for the keys names the mouse
as its pointer): #1010's 1137dd5.

Public API

  • nsh_core::element::OrderWatch::request(&self) (#[doc(hidden)], as watch_order and OrderWatch are):
    as invalidate, and the handler hears the order even when it is the list's own.
  • Behavior: a Menu, Submenu or ContextMenu of one item opened from the keyboard focuses it, as one of several
    focuses its first enabled item.

How to Test

  • ./target/debug/nsh test examples/gallery/tests/context-menu-press.test.tsx: "Shift+F10 on the focused trigger
    opens the menu at its first item" fails on main (5.4 s, the timeout above) and passes with this change (442 ms).
  • cargo test -p nsh-base --test base menu::: an_opening_from_the_keyboard_focuses_a_menus_only_item,
    right_on_a_submenus_trigger_focuses_its_only_item and shift_f10_focuses_a_context_menus_only_item (the web and
    Linux) fail without the menu's change and pass with it; the other 16 pass either way.
  • cargo test -p nsh-core --lib order_watch: a_requested_order_watch_hears_its_order_even_unchanged (an
    invalidation of an in-order watch tells nothing, a request tells once).
  • cargo test -p nsh-base --test base (772 passed) and cargo test -p nsh-core --lib (1136 passed); clippy with
    -D warnings on both crates' targets; cargo xtask compat.

Checklist

  • I have read the CONTRIBUTING document and followed the guidelines.
  • Reviewed the changes in this PR and confirmed AI generated code (if any) is accurate.
  • Passed cargo test for nsh-core and nsh-base, cargo +nightly fmt on the files it touches and clippy for
    both (no platform code changes).
  • Changes to what apps use keep what released versions shipped (cargo xtask compat passes; what apps use keeps
    its shape).
  • Ran the related examples/gallery demos on iOS and Android (if the change is visible or platform-specific).

A menu opened from the keyboard focuses its first enabled item (its
last for Up on a Menu's trigger) in the handler of its order watch,
once its items are in the tree's order. The core tells that handler
the order only when it differs from the order the items registered
in, and a menu's items register last-first: a menu of several items
is always out of order, so its item got the focus, while the only
item of a one-item menu was in order already, the handler never ran
and the next key went to the trigger. That is why
context-menu-press.test.tsx's Shift+F10 test, whose menu has one
item, failed on every run since longbridge#978, headless and in the browser.

OrderWatch::request asks the core to tell the handler the order even
when it is the list's own. The menu requests it while an opening from
the keyboard waits for its item. The other watches (the roving focus,
the carousel) only sort, and keep skipping orders that are in order
already, as most of a toolbar's are: their mounts cost no more rounds.

@huacnlee huacnlee left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

✅ The one-item keyboard-focus fix passes the targeted review and independent Linux behavior checks. Requesting the order callback explicitly is a reasonable way to apply pending focus while preserving the identity-permutation fast path for the other order watchers. The hidden OrderWatch::request() API is documented and limited to this operation; no new dependency or public struct is introduced.

Integration needs coordination: the shared menu fix already pushed to #1006/#1009/#1010/#1011 addresses the same bug with MenuItem layout callbacks. A local merge-tree check of this head and that implementation (254f65075) conflicts in crates/nsh-base/src/menu.rs. Please select one pending-focus approach when integrating these branches. Chromium keyboard context menus also require the separate event-classification fix in stacked #1018.

Verified on dbc55fc284a87366cb8cb2288b5a07e28f2cee57:

  • Built nsh-cli and nsh-linux-host; all 19 Rust menu tests and both order-watch tests pass. Replacing only menu.rs with main's version makes exactly the three added one-item menu/submenu/context-menu regressions fail (16 existing tests still pass), then restoring it returns the worktree to the reviewed source.
  • All-targets Clippy for nsh-base and nsh-core with -D warnings passes. All six changed Rust files pass nightly rustfmt. Workspace fmt reports eight unrelated files, each byte-identical to this PR's main parent.
  • Real GTK host under isolated Xvfb, driven with XTest keyboard events: baseline Tab→Return→Space leaves chose: none; this PR selects Open. The same behavior passes in light and dark appearances and at 2× scaling. Right→Space selects the sole submenu item, Up opens the sole item, and Escape restores trigger focus so reopening and selecting succeeds. Screenshots were inspected and baseline/PR comparisons retained.

The Linux screenshots cover the shared menu-focus change, not native macOS/Windows execution. Independent real Chromium testing also confirms the dependency: this exact PR runtime passes two of the three context-menu tests, with Shift+F10 still failing; applying only #1018's JavaScript change to the same runtime passes all three and an additional screenshot check. The GitHub Web CI check was still running at review time. This is a comment review, not an approval or a merge recommendation for the overlapping branches.

🤖 Noop (reviewer) · AI agent acting for @huacnlee

@dXnSix

dXnSix commented Nov 5, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review. On the overlap with 254f650 (#1006, #1009–#1011), I'd keep this PR's approach:

  • Both put the focus in the same batch as the items: with nsh test --trace, the Focus command goes out with the 18 items that answer Shift+F10 either way.
  • 254f650 picks from a MenuItem's layout, and a Submenu's trigger is not a MenuItem, so a menu level whose only entry is a submenu is never focused. right_on_a_submenus_trigger_focuses_its_only_item fails with 254f650's menu.rs: Enter on a menu that holds only a Share submenu focuses nothing, so Right opens nothing. This PR requests the order for every entry Items::add takes, menu items and submenu triggers alike, and passes all 19 menu tests.
  • No menu item carries a layout listener for this.

So 254f650 could leave those four branches, and this PR's three tests cover the case. For Chrome, I agree #1010's 1137dd5 is the better classifier (see #1018).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants