Skip to content

bug(aria/tabs): the tab list loses its only Tab stop when the active tab is removed after an interaction #33961

Open

Description

@hebus

Is this a regression?

  • Yes, this behavior used to work in the previous version

The previous version in which this bug was not present was

No response

Description

When the tab that currently holds the roving tabindex="0" (the active tab, TabListPattern.activeTab) is removed from the tab list after the user has already interacted with the list, no remaining tab gets tabindex="0". Every tab ends up with tabindex="-1", so the tab list is no longer reachable with Tab / Shift+Tab (WCAG 2.1.1 / 2.4.3).

It is easy to hit with selectionMode="explicit", where the active tab (focus) and the selected tab can differ, but the cause does not look specific to it: any removal of the active item after the first interaction should behave the same.

Cause, from reading the source (the guard is still on main): TabListPattern.setDefaultState() is the only place that re-assigns activeItem, and it is guarded.

setDefaultStateEffect() {
  if (this.hasBeenInteracted()) return;   // never runs again after the first interaction
  this.setDefaultState();
}
// onKeydown / onClick / onFocusIn: this.hasBeenInteracted.set(true)

After the first focus / key / click, hasBeenInteracted stays true. If the active tab is then destroyed, activeItem still points to the removed tab, so no tab in items() matches and the roving tabindex is -1 everywhere. Nothing falls back to the selected tab or to the first focusable tab.

A possible fix is to re-run the default-state logic when the active item is no longer part of items(), regardless of hasBeenInteracted:

setDefaultStateEffect() {
  const active = this.activeTab();
  if (this.hasBeenInteracted() && active !== undefined && this.inputs.items().includes(active)) return;
  this.setDefaultState();
}

Related: #33934 (same area of code, but about a focus moved by a script, not about a removed active tab); #32969 / 0f5b0ee introduced the hasBeenInteracted guard.

Reproduction

StackBlitz link: https://stackblitz.com/edit/duypg9mo?file=src%2Fapp%2Fapp.ts

Steps to reproduce:

  1. Tab into the tab list (focus lands on A), press ArrowDown once: focus moves to B, which is now the active tab (tabindex="0"); A stays selected because the selection is explicit.
  2. Press Tab three times: the panel itself, the input, then the remove B button.
  3. Press Enter on remove B: the active tab B is destroyed.
  4. Inspect the tabs: A and C both have tabindex="-1".
  5. Press Shift+Tab repeatedly: focus goes back through the input and the panel, then to the previous focusable element of the page. The tab list is skipped entirely.

Reproduced on @angular/aria 22.2.1 and 22.2.3.

Expected Behavior

After the active tab is removed, a remaining tab (the selected one, else the first focusable) becomes the active tab and gets tabindex="0", so the tab list keeps exactly one Tab stop.

Actual Behavior

All tabs have tabindex="-1". The tab list cannot be reached with Tab / Shift+Tab, and the arrow keys cannot help since focus cannot enter the list.

Environment

  • Angular: 22.2.1
  • CDK/Material: @angular/aria 22.2.1 and 22.2.3, @angular/cdk 22.2.1 and 22.2.3
  • Browser(s): Microsoft Edge (Chromium), also driven with real key events over the Chrome DevTools Protocol
  • Operating System (e.g. Windows, macOS, Ubuntu): Windows 11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions