roving-tab-index

Overview

Description

Section titled Description

Roving tabindex is a pattern whereby multiple focusable elements are represented by a single tabindex=0 element, while the individual elements maintain tabindex=-1 and are made accessible via arrow keys after the entry element if focused. This allows keyboard users to quickly tab through a page without having to stop on every element in a large collection. Attaching a RovingTabindexController to your custom element will manage the supplied elements via this pattern.

Usage

Section titled Usage

See it on NPM! How big is this package in your project?

yarn add @spectrum-web-components/reactive-controllers

Import the RovingTabindexController via:

import { RovingTabindexController } from '@spectrum-web-components/reactive-controllers/RovingTabindex.js';

Example

Section titled Example

A Container element that manages a collection of <sp-button> elements that are slotted into it from outside might look like the following:

import { html, LitElement } from 'lit';
import { RovingTabindexController } from '@spectrum-web-components/reactive-controllers/RovingTabindex.js';
import type { Button } from '@spectrum-web-components/button';

class Container extends LitElement {
    rovingTabindexController =
        new RovingTabindexController() <
        Button >
        (this,
        {
            elements: () => [...this.querySelectorAll('sp-button')],
        });

    render() {
        return html`
            <slot></slot>
        `;
    }
}

The above will default to entering the Container element via the first <sp-button> element every time while making all slotted <sp-button> elements accessible via the the arrow key (ArrowLeft, ArrowRight, ArrowUp, and ArrowDown) managed tab order.

Options

Section titled Options

A Container can further customize the implementation of the RovingTabindexController with the following options:

  • direction to customize how and which arrow keys manage what element is to be focused and accepts a either a string of both, vertical, horizontal, or grid or a method returning one of those strings
  • elementEnterAction enacts actions other than focus on the entered element which accepts a method with a signature of (el: T) => void
  • elements provides the elements that will have their tabindex managed via a method with a signature of () => T[]
  • focusInIndex to control what element will recieve tabindex=0 while focus is outside of the Container and accepts a method with a signature of (_elements: T[]) => number
  • isFocusableElement describes the state an element much be in to receive focus via a method with a signature of (el: T) => boolean
  • listenerScope outlines which parts on a container's DOM when listening for arrow key presses via an element reference or a method returning an element reference with the signature () => HTMLElement

Advanced usage

Section titled Advanced usage

These options can be combined to form various interfaces from the more default that we saw above to the very complex. Below is another Container that manages slotted <sp-button> elements via the RovingTabindexController. The options provided ensure:

  • the first focused <sp-button> is the one selected by the container
  • the elements are only focused via the ArrowLeft and ArrowRight keys
  • when an element is focused it becomes the selected element
  • only enabled elements are focusable
import { html, LitElement } from 'lit';
import { RovingTabindexController } from '@spectrum-web-components/reactive-controllers/RovingTabindex.js';
import type { Button } from '@spectrum-web-components/button';

class Container extends LitElement {
    rovingTabindexController = new RovingTabindexController<Button>(this, {
        focusInIndex: (buttons) => return this.selected
            ? buttons.indexOf(this.selected)
            : 0,
        direction: 'horizontal',
        elementEnterAction: (button) => this.selected = button,
        elements: () => [...this.querySelectorAll('sp-button')],
        isFocusableElement: (button) => !button.disabled,
    });

    selected!: Button;

    render() {
        return html`<slot></slot>`;
    }
}

The above usage is very close to what can be seen in the <sp-radio-group> element.