Utilities

Collection

Headless utilities for registering elements by value and navigating them in document order.

When to Use

Use Collection when building another headless component whose items mount, unmount, reorder, or become disabled and you need one reliable registry for keyboard navigation. Do not use it just to render an ordinary list; React and native list elements are simpler when no composite behavior is required.

Features

  • Registers, updates, looks up, and removes value-keyed elements.
  • Returns items in current document order rather than registration order.
  • Tracks disabled state and arbitrary typed item data.
  • Provides first, last, next, and previous navigation helpers.
  • Handles looping and optional inclusion of disabled items.
  • Renders no DOM and assigns no accessibility semantics.

Import

import {
  getNextCollectionItem,
  sortCollectionItemsByDocumentOrder,
  useCollection,
} from "@flowstack-ui/atom";

Anatomy

useCollection()
getNextCollectionItem()
sortCollectionItemsByDocumentOrder()

API Reference

useCollection

Creates a stable registry whose returned items are sorted by connected DOM position. The hook is generic over string values, element type, and item data.

useCollection<
  Value extends string,
  Element extends HTMLElement,
  Data extends Record<string, unknown>
>(): UseCollectionReturn<Value, Element, Data>
MethodSignatureDescription
registerItem(value, element, options?) => voidAdds or replaces an item. Options accept disabled and data.
unregisterItem(value) => voidRemoves an item by value.
updateItem(value, options) => voidUpdates disabled state or data for an existing item.
getItem(value) => CollectionItem | undefinedReturns one registered item.
getItems() => CollectionItem[]Returns all items in document order.
getValues() => Value[]Returns ordered values.
getEnabledItems() => CollectionItem[]Returns ordered items that are not disabled.
getFirstItem(options?) => CollectionItem | nullReturns the first eligible item.
getLastItem(options?) => CollectionItem | nullReturns the last eligible item.
getNextItem(value, direction?, options?) => CollectionItem | nullMoves forward or backward from a value.
clearItems() => voidRemoves every item.

registerItem defaults disabled to false and data to an empty object. Connected elements sort before disconnected elements. In development, a duplicate connected value produces a warning because values must be unique.

getNextCollectionItem

Finds the next or previous eligible item in an already ordered item array.

getNextCollectionItem(items, value, direction?, options?)
ParameterTypeDefault
itemsCollectionItem[]required
valuestringrequired
direction"next" | "previous""next"
options.loopbooleantrue
options.includeDisabledbooleanfalse

When value is missing, looping starts at the first or last eligible item; without looping it returns null.

sortCollectionItemsByDocumentOrder

Returns a new array sorted by each element's current document position. The input array is not mutated. Connected elements appear before disconnected elements, whose relative order remains stable.

sortCollectionItemsByDocumentOrder(items): CollectionItem[]

Examples

Register a Navigable List

import { useEffect, useRef } from "react";
import { useCollection } from "@flowstack-ui/atom";

const actions = ["copy", "paste", "delete"] as const;

function Action({
  value,
  registerItem,
  unregisterItem,
}: {
  value: string;
  registerItem: (value: string, element: HTMLButtonElement) => void;
  unregisterItem: (value: string) => void;
}) {
  const ref = useRef<HTMLButtonElement>(null);

  useEffect(() => {
    const element = ref.current;
    if (!element) return;
    registerItem(value, element);
    return () => unregisterItem(value);
  }, [registerItem, unregisterItem, value]);

  return <button ref={ref}>{value}</button>;
}

export function ActionList() {
  const collection = useCollection<string, HTMLButtonElement>();

  function focusNext(value: string) {
    collection.getNextItem(value)?.element.focus();
  }

  return (
    <div>
      {actions.map((action) => (
        <Action
          key={action}
          value={action}
          registerItem={collection.registerItem}
          unregisterItem={collection.unregisterItem}
        />
      ))}
      <button onClick={() => focusNext("copy")}>Focus after Copy</button>
    </div>
  );
}

Accessibility

Collection renders nothing, supplies no roles or labels, and owns no keyboard events. The component built with it must implement the correct native or WAI-ARIA pattern, focus model, and keys. Disabled filtering in Collection is a navigation tool; it does not expose aria-disabled on the registered element. Collection emits no Data attributes because it never creates an element.

Changelog

See CHANGELOG.md.