LearnNewsExamplesServices
Frontmatter
id19548
titleA rail reveal paints the browser's focus ring around the whole overlay
stateClosed
labels
bugdesignaiaccessibilitydashboard
assigneesneo-opus-grace
createdAtOct 10, 2026, 3:33 PM
updatedAtOct 10, 2026, 4:57 PM
githubUrlhttps://github.com/neomjs/neo/issues/19548
authorneo-opus-grace
commentsCount1
parentIssue14800
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtOct 10, 2026, 4:57 PM
milestonev13.2

A rail reveal paints the browser's focus ring around the whole overlay

Closed v13.2.0/archive-v13-2-0-chunk-24 bugdesignaiaccessibilitydashboard
neo-opus-grace
neo-opus-grace commented on Oct 10, 2026, 3:33 PM

Context

The 13.2 notes frame D2 (dock-d2-rail.png in neomjs/pages resources_pub/website/release-notes/v13.2.0/) shows a light-blue ring around the System Metrics overlay, which is revealed from the right edge rail. The operator (2026-10-10) saw no ring in the film takes or when using the rail by hand. Sampled pixels of the frame show the ring is #99C8FF with a #101010 edge. That is Chrome's default dark-scheme focus ring, not the overlay's designed border (--dock-reveal-border: --workstation-signal at 46% over --workstation-line, a teal-grey).

The Problem

Whether the ring paints depends on what held focus before the reveal, not on the reveal itself. A real mouse click never paints it: Chrome's last-input heuristic says pointer. The film driver's rail step (apps/workstation/tour/GestureDriver.mjs:673-704, #19127) clicks with synthetic events (detail 0) and then calls button.focus() with no modality. That programmatic focus is focus-visible, and the overlay's programmatic focus inherits it. The frame is embedded in the 13.2 notes, and every scripted capture of a reveal can repeat it.

The Architectural Reality

  • Rail#onRevealStateChange (src/dashboard/dock/interaction/Rail.mjs:740-741) calls RevealOverlay#focusReveal() when a tab click opens a reveal (revealed-focused). Rail#onTabClick (:777) receives the click payload, which carries detail (src/main/DomEvents.mjs:329). detail is ≥ 1 for a pointer click and 0 for a keyboard- or AT-activated one.
  • focusReveal() (src/dashboard/dock/interaction/RevealOverlay.mjs:390-396) calls this.focus(this.id, true) with no modality. The root is tabIndex: -1. DomUtils.isFocusable (src/main/DomUtils.mjs:128-131) accepts a negative tab index, so the root itself takes focus.
  • DomAccess#focus (src/main/DomAccess.mjs:358-400) stamps neo-focus-pointer for 'pointer' and says it "suppresses the accidental ring", but no stylesheet keys on that class. Only the Grid view styles its own focus (#18968, resources/scss/src/grid/View.scss:9-19). The contract is unbacked for all three callers that state 'pointer': RevealOverlay.mjs:538, src/draggable/container/SortZone.mjs:681 and src/grid/Body.mjs:1277.
  • Deliberate outlines share the property: the locked pane (resources/scss/src/dashboard/Container.scss:916) and the arrival pulse (:372). A suppression rule must lose to every author rule and beat only the user agent's.

Design authority: DomAccess#focus (src/main/DomAccess.mjs:358-360): "explicit input-modality contract. :focus-visible cannot tie an async worker→main programmatic focus back to the originating pointer gesture, so the caller states it." The same method's comment says "'pointer' suppresses the accidental ring".

The Fix

  1. resources/scss/src/Global.scss: :where(.neo-focus-pointer:focus-visible) {outline: none}. With zero specificity it beats only the user-agent ring, and any component outline still wins. The DomAccess#focus comment names this rule.
  2. Rail#onTabClick records the opening gesture's modality ('pointer' when detail >= 1), and focusReveal(modality) passes it to focus(). A keyboard-opened reveal passes none, so the user agent's keyboard ring stays.
  3. The film driver's synthetic click carries detail: 1, as a real click does.
  4. D2 is reshot at the fixed head, with the same path and caption, so the notes embed is unchanged.

Acceptance Criteria

  • AC-1 Red first in a real browser: an e2e arm focuses the rail tab programmatically (the film driver's path), opens the reveal with a click of detail: 1, and asserts that the focused overlay root's computed outline-style is none. The arm is red on dev.
  • AC-2 Green at the head. A keyboard-opened reveal (Enter on the tab) keeps a visible ring on the focused overlay root (control arm, green on both).
  • AC-3 Deliberate outlines are unchanged: the DockLock component spec (outline-style: solid on a locked pane) and the dashboard e2e folder stay green.
  • AC-4 (Post-Merge) D2 is reshot without the ring, and the pages main URL serves the new bytes.

Out of Scope

Component keyboard rings keyed on :focus-visible instead of neo-focus-keyboard, and the look of the keyboard ring around the overlay root (it stays the user agent's).

Related

#18968, #17317, #19339, #19127, #19506, #14800.

Sweeps: live latest-open sweep checked the latest 20 open issues at 13:33Z, no equivalent · A2A in-flight sweep: 30 messages, no overlapping claim · MC sweep: "rail reveal overlay focus ring, focusReveal modality" (5 results; #18968 and #19339 are the precedents, no decision that the overlay should carry the user-agent ring) · Own-assignment sweep: 2 open (#14800, #15000), none overlapping.

Origin Session ID: dbd862d7-9868-430b-8150-7a1fe23827c0 Retrieval Hint: "rail reveal overlay focus ring focusReveal modality tabindex -1 user agent outline"

tobiu referenced in commit aab57f9 - "fix(dock): a rail reveal a pointer opens paints no focus ring (#19548) (#19551) on Oct 10, 2026, 4:57 PM
tobiu closed this issue on Oct 10, 2026, 4:57 PM