Vize

Collection Registry

Headless Collection Registry; covers React Aria Collection, Ariakit Composite Store, Zag collection.

Package @vizejs/ui/collection
Maturity stable
Own the source vize lib pull collection
Requires —
Aliases collection registry, item registry, logical focus registry
Covers React Aria Collection, Ariakit Composite Store, Zag collection

API

createCollectionRegistry

Create an SSR-safe, mutation-aware registry for one compound collection. The registry never moves DOM focus itself. Consumers bind activeKey to roving tabindex or aria-activedescendant; synchronous recovery guarantees that removing or disabling the active item selects the next navigable item, then the previous one, before the mutation can leave focus state orphaned.

function createCollectionRegistry<Key extends CollectionKey, Value>( options: CollectionRegistryOptions<Key> = {}, ): CollectionRegistry<Key, Value>

validateCollectionKey

Reject keys that cannot serve as stable, serializable item identities.

function validateCollectionKey(key: CollectionKey): void

hasCollectionKey

Whether a resolved item list still contains a key.

function hasCollectionKey<Key extends CollectionKey, Value>( items: readonly CollectionItem<Key, Value>[], key: Key, ): boolean

recoverCollectionKey

Pick the logical focus successor for a key that left the navigable set. The next navigable item wins, then the previous one, so removing or disabling the active item never orphans logical focus.

function recoverCollectionKey<Key extends CollectionKey, Value>( removedKey: Key, previousItems: readonly CollectionItem<Key, Value>[], nextItems: readonly CollectionItem<Key, Value>[], ): Key | null

observeCollectionMutations

Observe DOM ordering and accessible text mutations for registered items. One observer is created per owning document. Connected items use their closest common ancestor and disconnected items are observed directly, so attribute and text noise from the rest of the document is never observed. Disconnected items additionally use a structure-only document observer that reports only once one of them mounts, because their insertion point is unknowable beforehand.

function observeCollectionMutations<Key extends CollectionKey, Value>( items: readonly CollectionItem<Key, Value>[], onMutation: () => void, ): MutationObserver[]

resolveCollectionRecord

Resolve every reactive source of one record into an immutable snapshot.

function resolveCollectionRecord<Key extends CollectionKey, Value>( record: CollectionRecord<Key, Value>, ): ResolvedCollectionRecord<Key, Value>

sortCollectionRecords

Sort resolved records in place using explicit order, DOM order, or registration order, rejecting ambiguous mixes of explicit orders.

function sortCollectionRecords<Key extends CollectionKey, Value>( records: ResolvedCollectionRecord<Key, Value>[], ): void

normalizeCollectionTextValue

Normalize authored or extracted typeahead text without applying a locale. Unicode is normalized to NFC and every whitespace run becomes one ASCII space. Locale-sensitive equality remains the responsibility of a collator.

function normalizeCollectionTextValue(value: string): string

extractCollectionTextValue

Extract a practical accessible text value from an item element. aria-labelledby and aria-label take precedence, hidden descendants are excluded unless explicitly referenced, and image/input fallbacks are included. Complex widgets should still provide textValue explicitly when their typeahead label differs from their accessible name.

function extractCollectionTextValue(element: Element): string

collectionTextStartsWith

Whether a candidate text starts with a query under locale-aware comparison.

function collectionTextStartsWith( candidate: string, query: string, collator: Intl.Collator, ): boolean

Behavior

collection.ts is the shared ordered-item and logical-focus contract for listbox, menu, tree, tabs, grid, command palette, combobox, and other compound widgets. It is headless: consumers render elements and bind activeKey to roving tabindex or aria-activedescendant without the registry writing DOM or owning selection.

State × input → outcome

State Input Required outcome
Live registry Register a unique non-empty string or safe-integer key One immutable item snapshot is exposed; the consumer value keeps its exact generic type
Live registry Register an empty/control-bearing string, non-safe number, negative zero, or duplicate key Registration throws a stable VIZE_UI_COLLECTION_* diagnostic and leaves the registry unchanged
No explicit order Every item element is connected in one document Items follow live DOM order, independent of setup or registration order
No explicit order SSR, pre-mount, an incomplete/disconnected item set, or different documents The entire collection retains deterministic registration order, avoiding a non-transitive mix of DOM and fallback ordering
Connected or partial item roots Relevant child, text, or accessible-label DOM mutation The connected common root and disconnected items are observed without watching the whole document; refresh() provides a deterministic fallback
Explicit order Every item supplies a unique safe integer Items follow ascending virtual order in SSR and the browser
Explicit order An order is missing, duplicated, or not a safe integer Resolution throws and a rejected registration is rolled back atomically
Missing explicit textValue A rendered element is available Text is extracted from same-root aria-labelledby, aria-label, inline content, image alt text, or input value; hidden decorative descendants are excluded
Any text source Whitespace or decomposed Unicode is present Text is NFC-normalized, trimmed, and whitespace-collapsed without locale-specific case folding
textValue="" Typeahead runs The item deliberately does not match typeahead
disabledBehavior="skip" An item is disabled The item remains inspectable but is excluded from navigation and typeahead; direct activation-key assignment is rejected
disabledBehavior="focusable" An item is disabled The item remains in focus navigation and typeahead so menu-style disabled-item semantics are possible; activation remains a higher-level policy
No active key Move next/first or previous/last The first or last navigable key becomes active respectively
Active key in the middle Move next/previous The adjacent navigable key becomes active; skipped disabled items are never selected
Active key at a boundary Move without/with loop The key remains unchanged without looping and wraps with looping
Active key Typeahead prefix or exact search The next locale-collated match after the active item is returned; wrapping defaults to enabled for repeated-character cycling
Active item Item unmounts or unregisters Recovery synchronously chooses the next surviving navigable item, then the previous item, then null
Active item Reactive disabled state becomes non-navigable The same synchronous next-then-previous recovery runs with reason item-disabled
Vue owner scope Item or registry scope stops Registrations are removed or the registry is disposed automatically; active state cannot point at destroyed content
Disposed registry A later mutation is requested Mutation throws VIZE_UI_COLLECTION_DISPOSED; repeated disposal is idempotent

Accessibility boundary

The registry supplies stable identity, deterministic order, disabled navigation policy, normalized text, and mutation-safe logical focus. A concrete composite must still apply its APG role model, accessible name and relationships, orientation, keyboard map, focus strategy, selection semantics, live announcements, and activation policy. Keeping those responsibilities explicit allows the same collection to serve roving DOM focus, aria-activedescendant, virtualized content, portalled content, DOM rendering, Vapor, and SSR.