Skip to content

useSelectableCollection

Shared collection and value resolution utilities for compound selection primitives.

selectable-collection is the shared item registry used by compound selection components such as RadioGroup, CheckboxGroup, ToggleGroup, and CommandPalette.

Use it when building a custom compound primitive that needs DOM-order item registration, enabled-item filtering, or consistent fallback selection from highlighted, selected, and first enabled item.

Installation

$pnpm exec dgadd add ui/selectable-collection
[Installs to]src/lib/selectable-collection.ts[Item]ui/selectable-collection

dgadd is not public on npm yet. Until the first release, pack @diffgazer/add from the repository and install that tarball into this app, which is what puts dgadd on pnpm exec.

UI components require Tailwind CSS v4. Local copy mode imports src/styles/styles.css; package mode uses @diffgazer/ui CSS once packages are available.

In copy mode the item writes three files — lib/selectable-collection.ts, lib/selectable-collection-observer.ts, and lib/selectable-group.ts — and pulls in the @diffgazer-keys/focusable registry item.


API

ts
interface SelectableCollectionItem {
  id: string
  value: string
  disabled: boolean
  element: HTMLElement | null
}

function useSelectableCollection(
  containerRef: React.RefObject<HTMLElement | null>
): {
  items: SelectableCollectionItem[]
  registerItem: (
    itemId: string,
    value: string,
    disabled: boolean,
    element: HTMLElement | null
  ) => void
  unregisterItem: (itemId: string) => void
}

function getEnabledSelectableCollectionItems(
  items: SelectableCollectionItem[],
  disabled: boolean
): SelectableCollectionItem[]

function getSelectableCollectionItemByValue(
  items: SelectableCollectionItem[],
  value: string | null | undefined
): SelectableCollectionItem | null

function resolveSelectableCollectionItem(
  items: SelectableCollectionItem[],
  ...values: Array<string | null | undefined>
): SelectableCollectionItem | null

Usage

tsx
import { useRef } from "react"
import {
  getEnabledSelectableCollectionItems,
  resolveSelectableCollectionItem,
  useSelectableCollection,
} from "@/lib/selectable-collection"

function CustomPicker({
  value,
  highlighted,
  disabled = false,
}: {
  value: string | null
  highlighted: string | null
  disabled?: boolean
}) {
  const containerRef = useRef<HTMLDivElement>(null)
  const { items, registerItem, unregisterItem } = useSelectableCollection(containerRef)
  const enabledItems = getEnabledSelectableCollectionItems(items, disabled)
  const tabTargetValue =
    resolveSelectableCollectionItem(enabledItems, highlighted, value)?.value ?? null

  return (
    <div ref={containerRef} data-tab-target={tabTargetValue ?? undefined}>
      {/* Register child items with registerItem/unregisterItem. */}
    </div>
  )
}

Notes

  • The collection is sorted by DOM order, so keyboard traversal matches the rendered layout.
  • Disabled items and items without mounted elements are excluded by getEnabledSelectableCollectionItems.
  • resolveSelectableCollectionItem tries candidates in order and falls back to the first enabled item.