Problem
Anchor tab triggers (<a tedi-tabs-trigger [routerLink]> / [href], added in #594) navigate through the link itself. TabsTriggerComponent is built around that: Space is forwarded to a native click (tabs-trigger.component.ts#L120-L129), and arrow keys only move focus on anchor tabs, without activating them (#L131-L140).
The overflow dropdown (overflowMode="dropdown") does not follow that rule. Its item calls TabsListComponent.selectTab(id), which only calls tabs.select(id) (tabs-list.component.ts#L164-L166, tabs-list.component.html#L34). For an anchor tab nothing navigates: valueChange fires, the URL stays the same.
A consumer therefore has to navigate on valueChange. But valueChange also fires for an ordinary click on a visible anchor tab, because handleClick calls tabs.select(id) after the link has already started its navigation (#L88-L107). The consumer cannot tell the two apart from the event, so navigating on valueChange navigates twice on every visible tab click: once through the link, once through the consumer's handler. With [href] that is two document navigations to the same URL; with [routerLink] it is two router navigations, so guards and resolvers run twice.
TabsListComponent.dropdownItems() does not help the consumer either: it holds every trigger except the active one, whether it overflows or not.
Steps to reproduce
-
Render anchor tabs with the overflow dropdown and navigate on valueChange, as a consumer must for the dropdown to work:
<tedi-tabs [value]="active()" (valueChange)="go($event)">
<tedi-tabs-list overflowMode="dropdown">
@for (tab of tabs; track tab.id) {
<a tedi-tabs-trigger [id]="tab.id" [href]="tab.href">{{ tab.label }}</a>
}
</tedi-tabs-list>
</tedi-tabs>
go(id: string) {
location.assign(this.tabs.find((t) => t.id === id)!.href);
}
-
Log navigations in the console without following them:
navigation.addEventListener("navigate", (e) => {
console.log(e.destination.url, e.userInitiated);
e.preventDefault();
});
-
Wide viewport, click a visible tab: two navigations to the same URL in the same task, one userInitiated: true (the link), one false (go()).
-
Without (valueChange)="go($event)": narrow the viewport until "More" appears and pick a tab from it. Nothing navigates.
Expected: every way of choosing an anchor tab (click, Space, dropdown item) navigates exactly once, through the link.
Actual (8.0.0, same code on rc): the dropdown item never navigates; navigating on valueChange to fix that makes visible tab clicks navigate twice.
Solution
Make the dropdown item of an anchor trigger click the trigger's link, the same way handleKeydown does for Space:
// TabsListComponent
selectTab(id: string): void {
const trigger = this.triggers().find((t) => t.id() === id);
if (trigger?.isAnchor) {
// Follows the link, as Space does on the tab itself. The click runs routerLink/href
// and the trigger's own handleClick, which calls tabs.select(id).
trigger.host.nativeElement.click();
return;
}
this.tabs.select(id);
}
click() on the hidden (display: none) anchor still runs its click handlers and follows href.
- Disabled items stay covered: the dropdown item is already
[disabled], and handleClick checks disabled().
- Button triggers keep the current
tabs.select(id) path.
valueChange is still emitted once, from handleClick, so uncontrolled usage keeps updating activeTab.
isAnchor and host are already public on TabsTriggerComponent.
Document in Storybook / ZeroHeight: with anchor triggers, the link navigates and valueChange only reports the new active tab; do not navigate from valueChange.
Examples and information
- Measured in the consuming app with the Navigation API (step 2): before the consumer-side workaround, one click on a visible tab logged two navigations to the same URL in the same millisecond; after it, one.
- Consumer-side workaround until this is fixed: forward
valueChange only while TabsListComponent.showMore() is true. While the dropdown is shown, the non-selected triggers are display: none, so the only way to choose another tab is the dropdown.
Project
SFOS
DoD
Problem
Anchor tab triggers (
<a tedi-tabs-trigger [routerLink]>/[href], added in #594) navigate through the link itself.TabsTriggerComponentis built around that: Space is forwarded to a native click (tabs-trigger.component.ts#L120-L129), and arrow keys only move focus on anchor tabs, without activating them (#L131-L140).The overflow dropdown (
overflowMode="dropdown") does not follow that rule. Its item callsTabsListComponent.selectTab(id), which only callstabs.select(id)(tabs-list.component.ts#L164-L166,tabs-list.component.html#L34). For an anchor tab nothing navigates:valueChangefires, the URL stays the same.A consumer therefore has to navigate on
valueChange. ButvalueChangealso fires for an ordinary click on a visible anchor tab, becausehandleClickcallstabs.select(id)after the link has already started its navigation (#L88-L107). The consumer cannot tell the two apart from the event, so navigating onvalueChangenavigates twice on every visible tab click: once through the link, once through the consumer's handler. With[href]that is two document navigations to the same URL; with[routerLink]it is two router navigations, so guards and resolvers run twice.TabsListComponent.dropdownItems()does not help the consumer either: it holds every trigger except the active one, whether it overflows or not.Steps to reproduce
Render anchor tabs with the overflow dropdown and navigate on
valueChange, as a consumer must for the dropdown to work:Log navigations in the console without following them:
Wide viewport, click a visible tab: two navigations to the same URL in the same task, one
userInitiated: true(the link), onefalse(go()).Without
(valueChange)="go($event)": narrow the viewport until "More" appears and pick a tab from it. Nothing navigates.Expected: every way of choosing an anchor tab (click, Space, dropdown item) navigates exactly once, through the link.
Actual (8.0.0, same code on
rc): the dropdown item never navigates; navigating onvalueChangeto fix that makes visible tab clicks navigate twice.Solution
Make the dropdown item of an anchor trigger click the trigger's link, the same way
handleKeydowndoes for Space:click()on the hidden (display: none) anchor still runs its click handlers and followshref.[disabled], andhandleClickchecksdisabled().tabs.select(id)path.valueChangeis still emitted once, fromhandleClick, so uncontrolled usage keeps updatingactiveTab.isAnchorandhostare already public onTabsTriggerComponent.Document in Storybook / ZeroHeight: with anchor triggers, the link navigates and
valueChangeonly reports the new active tab; do not navigate fromvalueChange.Examples and information
valueChangeonly whileTabsListComponent.showMore()is true. While the dropdown is shown, the non-selected triggers aredisplay: none, so the only way to choose another tab is the dropdown.Project
SFOS
DoD
coverageThresholdovertedi/components, not per component)type(scope): Subject text #issue-refis used (forsemantic-release), with the issue reference required - see Commit convention