ItemsCursorMixin

Tracks and navigates the current item in a set of items

Overview

Purpose: Tracks and navigates the current item in a set of items.

This mixin generally works in the middle of the Elix render pipeline:

events → methodssetState → render DOM → post-render

Expects the component to provide:

  • items state member representing the items that can be selected. This is usually provided by ContentItemsMixin.

Provides the component with:

  • currentIndex state member to track the index of the currently selected item.
  • internal.goFirst(), internal.goLast(), internal.goNext(), and internal.goPrevious() cursor methods to move the current item.

Usage

import ItemsCursorMixin from "elix/src/base/ItemsCursorMixin.js";
class MyElement extends ItemsCursorMixin(HTMLElement) {}

ItemsCursorMixin is designed to support components that track which item in a given set is the current item. This is generally done to let the user select a value (e.g., as the target of an action, or in configuring something), or as a navigation construct (where only one page/mode is visible at a time).

Examples:

  • Single-selection list boxes such as ListBox.
  • Multi-selection list boxes such as MultiSelectListBox.
  • Dropdown lists and combo boxes
  • Carousels such as Carousel.
  • Slideshows
  • Tab UIs (including top-level navigation toolbars that behave like tabs) such as Tabs.

Significantly, the concept of the current item (alternatively, the item cursor) is conceptually distinct from selection. In a single-selection list, the selection tracks the cursor, but in a multi-selection list, the cursor can be moved independently from the set of selected items.

The items collection

ItemsCursorMixin manages a selection within an identified collection of HTML elements. A component identifies that collection by defining a state member called items. A simplistic implementation of items could populate it with the component's light DOM children:

class SimpleList extends ItemsCursorMixin(ReactiveMixin(HTMLElement)) {
  connectedCallback() {
    super.connectedCallback();
    this[internal.setState]({
      items: [...this.children],
    });
  }
}

The above definition for items is simplistic, as it does not support the Gold Standard checklist item Content Assignment. Nevertheless, it can suffice here for demonstration purposes. A more complete component could use SlotContentMixin and ContentItemsMixin to meet the Gold Standard criteria.

The key point is that the component provides the collection of items, and they can come from anywhere. The component could, for example, indicate that the items being managed reside in the component's own Shadow DOM subtree:

class ShadowList extends ItemsCursorMixin(ReactiveMixin(HTMLElement)) {
  constructor() {
    super();
    this.attachShadow({ mode: "open" });
    // Populate shadow tree with elements.
    this[internal.setState]({
      items: [...this.shadowRoot.children],
    });
  }
}

For flexibility, ItemsCursorMixin can work with an items collection of type NodeList or Array.

The current item

The mixin tracks the current item by its index in the currentIndex state member. This is the zero-based index of the current item within the items collection (above). If no item is current, currentIndex is -1.

Applications working with selection sometimes want to reference select by index, and sometimes by object reference. The mixin supports both approaches with complementary properties that can both be get and set:

  • currentIndex. The index of the current item.
  • currentItem. The current item itself. If no item is current, currentItem is null.

ItemsCursorMixin clamps the currentIndex value so that it temporarily falls within the bounds of the items array. Example: suppose there are 5 items, and currentIndex is 4 (the last item). If the last item is removed, currentIndex will be updated to 3 (the new last item) so that the index remains valid. If a new item is added, currentIndex will be restored to its original value of 4. This supports flexible timing in cases where currentIndex will end up being applied before the full set of items is available.

Requiring a current item

The ItemsCursorMixin defines a currentItemRequired state member which is false by default. This is appropriate, for example, in components like list boxes or combo boxes which initially may have no item current.

Some components do require a current item if items are defined. An example is a carousel: as long as the carousel contains at least one item, the carousel should always show some item as current. Such components can set currentItemRequired to true.

Cursor operations

The selection can be programmatically manipulated via public cursor methods:

  • internal.goFirst. Moves to the first item.
  • internal.goLast. Moves to the last item.
  • internal.goNext. Moves to the next item in the list. Special case: if no item is current, but items exist, this moves to the first item. This case covers list-style components that receive the keyboard focus but do not yet have a selection. In such a case, advancing the selection (with, say, the Down arrow) can be implicitly interpreted as moving to the first item.
  • internal.goPrevious. Moves to the previous item in the list. Special case: if no item is current, but items exist, this moves to the last item. As with internal.goNext, this behavior covers list-style components.

If items has no items, these cursor operations have no effect.

All cursor methods return a boolean value: true if they moved the item cursor, false if not.

Wrapping cursor operations

In some cases, such as carousels, cursor operations should wrap around from the last item to the first and vice versa. This optional behavior, useful in carousel-style components and slideshows, can be enabled by setting the mixin's cursorOperationsWrap state member to true. The default value is false.

Cursor properties

Two properties track whether the internal.goNext and internal.goPrevious methods are available:

  • canGoNext. This is true if the internal.goNext method can be called.
  • canGoPrevious. This is true if the internal.goPrevious method can be called.

These properties are useful for components that want to offer the user, e.g., Next/Previous buttons to move the selection. The properties above can be monitored for changes to know whether such Next/Previous buttons should be enabled or disabled.

Both the goNext and goPrevious methods support a special case: if there is no current item but items exist, those methods select the first or last item respectively. Accordingly, if there is no current item but items exist, the canGoNext and canGoPrevious properties will always be true.

Finally, if cursorOperationsWrap is true, canGoNext and canGoPrevious will always be true if items exist.

Rendering the current item

When the current item changes, the component's internal.render method can updates the items to reflect which item is now current. For example, in single-selection lists, AriaListMixin updates an item's aria-selected attribute to reflect which item is current to assistive technologies. It does this with code similar to:

[internal.render](changed) {
  if (super[internal.render]) { super[internal.render](changed); }
  if (changed.items || changed.currentIndex) {
    // Reflect the selection state to the current item.
    const { items, currentIndex } = this[internal.state];
    if (items) {
      items.forEach((item, index) => {
        const selected = index === currentIndex;
        item.setAttribute('aria-selected', selected);
      });
    }
  }
}

API

Used by classes Carousel, CarouselSlideshow, CarouselWithThumbnails, CenteredStrip, CrossfadeStage, CrossfadeStage, DropdownList, Explorer, FilterListBox, ListBox, ListExplorer, Menu, Modes, MultiSelectListBox, OptionList, PlainCarousel, PlainCarouselSlideshow, PlainCarouselWithThumbnails, PlainCenteredStrip, PlainCenteredStripHighlight, PlainCenteredStripOpacity, PlainCrossfadeStage, PlainCrossfadeStage, PlainDropdownList, PlainExplorer, PlainFilterListBox, PlainListBox, PlainListExplorer, PlainMenu, PlainModes, PlainMultiSelectListBox, PlainOptionList, PlainSlideshow, PlainSlideshow, PlainSlideshowWithPlayControls, PlainSlideshowWithPlayControls, PlainSlidingPages, PlainSlidingStage, PlainTabs, PlainTabStrip, Slideshow, Slideshow, SlideshowWithPlayControls, SlideshowWithPlayControls, SlidingPages, SlidingStage, Tabs, and TabStrip.

closestAvailableItemIndex(state, options) method

Look for an item which is available in the given state..

The options parameter can accept options for:

  • direction: 1 to move forward, -1 to move backward
  • index: the index to start at, defaults to state.currentIndex
  • wrap: whether to wrap around the ends of the items array, defaults to state.cursorOperationsWrap.

If an available item was found, this returns its index. If no item was found, this returns -1.

Parameters:

  • state: PlainObject
  • options: PlainObject

Returns: number

[internal.goFirst]() method

Move to the first item in the set.

Returns: Boolean True if the current item changed, false if not.

[internal.goLast]() method

Move to the last item in the set.

Returns: Boolean True if the current item changed, false if not.

[internal.goNext]() method

Move to the next item in the set.

If no item is current, move to the first item.

Returns: Boolean True if the current item changed, false if not.

[internal.goPrevious]() method

Move to the previous item in the set.

If no item is current, move to the last item.

Returns: Boolean True if the current item changed, false if not.