Primitives / foldkit-primitives/interaction

RovingTabindex

namespace · packages/primitives/src/interaction/roving-tabindex.ts:8

export * as RovingTabindex from './roving-tabindex.js'

One tab stop for a set of items (a toolbar, a tab list, a listbox): arrows move focus between them, the rest stay out of the tab order. The current item's id is the Model slice, so a reorder keeps the same item current and a resumed page knows where focus was. Which item comes next is a pure function of the items and the key; the Behavior wires it to the slots.

In the guides

Members

RovingTabindex.Args

const · packages/primitives/src/interaction/roving-tabindex.ts:31

const Args: Schema.Struct<{
  readonly orientation: Schema.Literals<
    readonly ['vertical', 'horizontal', 'both']
  >
  readonly loop: Schema.Boolean
  readonly virtual: Schema.Boolean
}>

RovingTabindex.behavior

function · packages/primitives/src/interaction/roving-tabindex.ts:161

function behavior<Field extends string>(
  declared: Declared<typeof bundle, Field>,
  args: Args,
): <Slots>(
  slots: Slots,
) => <Input extends { readonly [K in Field]: Model }, ParentMessage>(
  options: BehaviorOptions<Input, Slots>,
) => Behavior.NamedBehavior<Slots, Input, ParentMessage>

Wires a placed RovingTabindex to the slots: the container's arrow, Home and End keys focus the next item (synchronously, then dispatch), each item gets tabindex 0 or -1 and reports Focused when focused. Under virtual DOM focus never leaves the container: the key only dispatches, the container carries aria-activedescendant, and items keep no tabindex.

Every handled key is default-prevented. Nothing is written on dispose: the attributes are data, so a view that no longer attaches this leaves no tabindex behind.

RovingTabindex.BehaviorOptions

interface · packages/primitives/src/interaction/roving-tabindex.ts:118

interface BehaviorOptions<Input, Slots> {
  readonly container: keyof Slots & string
  readonly item: keyof Slots & string
  readonly items: (input: Input) => Behaviors.Collection.Items<unknown>
  readonly direction?: (input: Input) => Direction
}

RovingTabindex.bundle

const · packages/primitives/src/interaction/roving-tabindex.ts:41

const bundle: Bundle.Bundle<
  'RovingTabindex',
  {
    readonly orientation: 'horizontal' | 'vertical' | 'both'
    readonly loop: boolean
    readonly virtual: boolean
  },
  { readonly current: string | null },
  { readonly _tag: 'Focused'; readonly id: string },
  never,
  never,
  never,
  void,
  {},
  {}
>

RovingTabindex.Direction

type · packages/primitives/src/interaction/roving-tabindex.ts:51

type Direction = 'ltr' | 'rtl'

RovingTabindex.idSelector

function · packages/primitives/src/interaction/roving-tabindex.ts:129

function idSelector(id: string): string

A selector for an element by id, safe for any id.

RovingTabindex.itemAttributes

function · packages/primitives/src/interaction/roving-tabindex.ts:136

function itemAttributes<ParentMessage>(
  h: HtmlBuilder<ParentMessage>,
  items: Behaviors.Collection.Items<unknown>,
  current: string | null,
  item: SlotItem,
  virtual: boolean,
  focused: (id: string) => ParentMessage,
): ReadonlyArray<Attribute<ParentMessage>>

The attributes one item gets: tabindex 0 when it holds the tab stop and -1 otherwise (none under virtual), and OnFocus reporting it current. Shared with ListNavigation.

RovingTabindex.Message

const · packages/primitives/src/interaction/roving-tabindex.ts:25

const Message: MessageUnion<{
  readonly Focused: { readonly id: Schema.String }
}>

RovingTabindex.Model

const · packages/primitives/src/interaction/roving-tabindex.ts:19

const Model: Schema.Struct<{ readonly current: Schema.NullOr<Schema.String> }>

RovingTabindex.move

function · packages/primitives/src/interaction/roving-tabindex.ts:71

function move(
  enabled: ReadonlyArray<number>,
  current: number,
  key: string,
  modifiers: KeyboardModifiers,
  options: MoveOptions,
): number | undefined

The index the key moves to from current, over the enabled indices only, or undefined when the key is not a navigation key here. current may be -1 (nothing current yet): arrows then land on the first or last enabled item. Under rtl, left and right swap. PageUp and PageDown move by page and clamp at the ends, never wrapping.

RovingTabindex.MoveOptions

interface · packages/primitives/src/interaction/roving-tabindex.ts:53

interface MoveOptions {
  readonly orientation: Orientation
  readonly loop: boolean
  readonly direction: Direction
  readonly page?: number
}

RovingTabindex.Orientation

const · packages/primitives/src/interaction/roving-tabindex.ts:16

const Orientation: Schema.Literals<readonly ['vertical', 'horizontal', 'both']>

RovingTabindex.tabStop

function · packages/primitives/src/interaction/roving-tabindex.ts:109

function tabStop(
  items: Behaviors.Collection.Items<unknown>,
  current: string | null,
): number

The item that holds the tab stop: the current one, else the first enabled.