Repository navigation
bug(aria/tabs): the tab list loses its only Tab stop when the active tab is removed after an interaction #33961
Description
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:
Tabinto the tab list (focus lands onA), pressArrowDownonce: focus moves toB, which is now the active tab (tabindex="0");Astays selected because the selection is explicit.- Press
Tabthree times: the panel itself, the input, then theremove Bbutton. - Press
Enteronremove B: the active tabBis destroyed. - Inspect the tabs:
AandCboth havetabindex="-1". - Press
Shift+Tabrepeatedly: 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/aria22.2.1 and 22.2.3,@angular/cdk22.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
Activity