Skip to content

Scroll Panel: scroll region adds an unnecessary keyboard tab stop #4175

Description

@twjeffery

goa-scroll-panel gives its scroll region its own keyboard tab stop, even when the content inside is already keyboard-reachable and even when nothing is overflowing. This surfaced while reviewing the WorkSideMenu focus-ring fix: on a short menu, the whole menu container takes a focus ring and a tab stop separate from the individual menu items.

Current behavior

The scrollable content region in ScrollPanel.svelte renders with role="region", aria-label="Scrollable content", and tabindex="0". That tabindex is applied whenever vertical scrolling is enabled by the direction prop ("vertical" is the default). It does not depend on whether the content overflows, or on what is inside the panel.

In WorkSideMenu, the slotted content is a list of focusable items (each menu item is a button or link with its own tabindex="0"). So a keyboard user already tabs through the items, and each off-screen item scrolls into view as it receives focus. The container's own tab stop adds nothing: you Tab once to focus the whole panel (the full-height ring), then Tab again to reach the first item. Screen reader users also hear an extra "Scrollable content" region announced on the way through.

Image

Expected behavior

A scroll region only needs its own keyboard focus when its content cannot be reached any other way. That happens when the content is not itself focusable (a block of static text, an image) and it overflows, so the only way a keyboard user can scroll it is to focus the region and use the arrow keys.

When the content is already focusable (a menu, a list of links, form fields), tabbing through it already moves the scroll, so the region should not take focus at all. And when nothing overflows, there is nothing to scroll to regardless. WorkSideMenu is the focusable-content case, so its scroll region should never be a tab stop.

Suggested fix / root cause

The tabindex keys off the direction prop alone. It should instead match what browsers now do natively: make a scroll region keyboard-focusable only when it actually scrolls AND contains no focusable children. Chrome (since v130) and Firefox already apply this rule automatically, so the manual tabindex="0" overrides them and reintroduces the exact tab stop those browsers avoid.

Two directions:

  • Gate the manual tabindex on both conditions (actually overflowing AND no focusable children inside). The component already tracks live overflow state for its scroll-shadow indicators, so half of that signal exists already.
  • Or drop the manual tabindex entirely and rely on the browsers' native behavior, which is cleaner. Confirm Safari/WebKit support first, since if WebKit has not shipped this yet, removing it would regress keyboard scroll access there for panels holding non-focusable content.

Open questions

  • For the case that genuinely needs it (non-focusable content that overflows), gating focus on live overflow means the tab stop appears and disappears as content grows or shrinks. Is that acceptable, or does the tab order need to stay stable?
  • This lives in the shared goa-scroll-panel primitive, so it affects every component that uses it, not just WorkSideMenu.

Issue created with help from Claude

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    Status
    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions