Skip to content

[Tabs]: overflow dropdown item of an anchor trigger does not follow the link #754

Description

@gerrit-nortal

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

  1. 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);
    }
  2. Log navigations in the console without following them:

    navigation.addEventListener("navigate", (e) => {
      console.log(e.destination.url, e.userInitiated);
      e.preventDefault();
    });
  3. Wide viewport, click a visible tab: two navigations to the same URL in the same task, one userInitiated: true (the link), one false (go()).

  4. 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

  • The component is developed using best practices, conventions, and modern frontend trends
  • The component follows BEM naming conventions (if applicable)
  • The component implementation matches the Figma design, including all defined states and interactions
  • The component supports responsiveness and scales across breakpoints (if applicable)
  • The component does not introduce runtime errors or warnings
  • Linter passes
  • The component is exported from its tier barrel
  • The component has unit tests with at least 80% code coverage (enforced by the global Jest coverageThreshold over tedi/components, not per component)
  • CodeCov passes its threshold (80% on the changed code, with 10% tolerance — a separate gate from the Jest one above)
  • The component has passed code review
  • The component has passed Chromatic visual review
  • Storybook stories are created:
    • Cover all Figma variations
    • Include edge cases, empty/error states
  • Component’s API (props, slots, events) is documented
  • ZeroHeight is updated with design/development guidelines
  • External dependencies used in the component are documented in Storybook and Zeroheight, with rationale
  • Semantic commit format type(scope): Subject text #issue-ref is used (for semantic-release), with the issue reference required - see Commit convention
  • External libraries comply with validated open source licenses
  • Component is versioned correctly and ready for release if required
  • Update the Angular development status on the statuses page in Zeroheight

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

tedi-readyTEDI-Ready component issue

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions