Skip to main content
cngx-src documentation

CngxRovingTabindex

DirectivePrimaryv0.1.0WCAG AA

projects/common/a11y/roving/roving-tabindex.directive.ts

Import#

import { CngxRovingTabindex } from '@cngx/common/a11y'

Description#

Implements the WAI-ARIA roving tabindex pattern for composite widgets.

Only the active item in the group has tabindex="0" - all others get tabindex="-1". Arrow keys move focus within the group; Tab leaves it. Home/End jump to first/last item.

Works for toolbars, tab lists, card grids, menu bars, and any composite widget where only one item should be in the tab order at a time.

Virtual mode

When virtualCount is set, activeIndex ranges from 0 to virtualCount - 1 instead of being bounded by contentChildren.length. Items are matched by data-cngx-recycle-index attribute. When the target item is not in the DOM (out of rendered range), pendingFocus is set so a wiring function like connectRecyclerToRoving() can scroll it into view.

Horizontal toolbar

<div cngxRovingTabindex orientation="horizontal" #rv="cngxRovingTabindex">
  <button cngxRovingItem>Cut</button>
  <button cngxRovingItem>Copy</button>
  <button cngxRovingItem>Paste</button>
</div>

Vertical menu with controlled index

<ul cngxRovingTabindex orientation="vertical" [(activeIndex)]="selectedIdx">
  @for (item of items(); track item.id) {
    <li cngxRovingItem>{{ item.label }}</li>
  }
</ul>

Virtual scroll integration

<div cngxRovingTabindex orientation="vertical" [virtualCount]="recycler.ariaSetSize()">
  @for (item of visibleItems(); track item.id; let i = $index) {
    <div cngxRovingItem [cngxVirtualItem]="recycler" [cngxVirtualItemIndex]="recycler.start() + i">
      {{ item.name }}
    </div>
  }
</div>

https://cngxjs.github.io/cngx/examples/#/common/a11y/roving-tabindex/horizontal-toolbar https://cngxjs.github.io/cngx/examples/#/common/a11y/roving-tabindex/vertical-menu

Metadata#

Host#

Relationships

Index#

Inputs#

activeIndex#number
model()

Index of the currently active (focusable) item. Supports two-way [(activeIndex)] binding.

default 0
input()

Whether navigation wraps from last to first and vice versa.

default true
orientation#"horizontal" | "vertical" | "both"
model()

Arrow key navigation axis. 'both' enables both horizontal and vertical arrows. Exposed as a model so composite hosts (e.g. CngxRadioGroup, where the keyboard axis must always be 'both' per WAI-ARIA APG regardless of the cosmetic group orientation) can override it from their own constructor.

default 'horizontal'
virtualCount#number | undefined
input()

Total item count for virtual mode. When set, activeIndex ranges from 0 to virtualCount - 1 and items are matched by data-cngx-recycle-index attribute. When not set, standard contentChildren-based navigation is used.

Outputs#

activeIndex#number
model()

Index of the currently active (focusable) item. Supports two-way [(activeIndex)] binding.

orientation#"horizontal" | "vertical" | "both"
model()

Arrow key navigation axis. 'both' enables both horizontal and vertical arrows. Exposed as a model so composite hosts (e.g. CngxRadioGroup, where the keyboard axis must always be 'both' per WAI-ARIA APG regardless of the cosmetic group orientation) can override it from their own constructor.

Instance Properties#

pendingFocus#unknown
Readonly

Index of the item that should receive focus but is not currently in the DOM. Non-null when virtual navigation targets an out-of-range item. Used by connectRecyclerToRoving() to scroll the item into view and focus it.

this.pendingFocusState.asReadonly()

Methods#

clearPendingFocus#void

Clears the pending focus target. Called by connectRecyclerToRoving() after the item has been scrolled into view and focused.

consumeNavigationKey#boolean

Whether the last focus move was driven by a navigation key, consuming the flag so it reads true exactly once. Consumer groups (radio, button-toggle) call this from the newly-focused leaf's (focus) handler to decide whether to auto-select. The flag is set before .focus(), so it is already true when focus lands - this closes the one-press-behind race that a group's own (keydown) binding suffered (#135).

handleKeyDown#void
Protected
handleKeyDown(event: KeyboardEvent)

Handles arrow-key, Home, and End navigation within the group. Prevents default scrolling on arrow keys.

Unified handler - delegates to virtual or contentChildren navigation based on whether virtualCount is set.

@parameventKeyboardEvent

HostListeners#

EventHandler
(keydown)keydown()