feat(navigation-menu): enhance context handling and lifecycle management

This commit is contained in:
2026-06-10 16:16:12 +07:00
parent a82f5f2dfd
commit 9375304e1a
55 changed files with 1997 additions and 179 deletions
+32 -6
View File
@@ -157,29 +157,55 @@ function createCollectionState<Value = unknown>(): CollectionContext<Value> {
};
}
const CollectionCtx = useContextFactory<CollectionContext>('CollectionContext');
const DEFAULT_COLLECTION_KEY = 'CollectionContext';
// One context factory per namespace key (`useContextFactory` mints a unique
// Symbol per call). Without namespacing, a collection provider nested inside
// another (e.g. `RovingFocusGroup` between `NavigationMenuRoot` and
// `NavigationMenuTrigger`) shadows the outer collection for every descendant.
const collectionContextFactories = new Map<
string,
ReturnType<typeof useContextFactory<CollectionContext>>
>();
function getCollectionContextFactory(key: string) {
let factory = collectionContextFactories.get(key);
if (!factory) {
factory = useContextFactory<CollectionContext>(key);
collectionContextFactories.set(key, factory);
}
return factory;
}
/**
* Creates a new collection state and provides it to descendants.
* Call this in the parent (e.g. `RovingFocusGroup`, `ListboxRoot`).
*
* Pass a dedicated `key` when the component tree may nest another collection
* provider between this one and its injectors, so they don't shadow each other.
*
* @example
* ```ts
* const { getItems, CollectionSlot } = useCollectionProvider();
* ```
*/
export function useCollectionProvider<Value = unknown>(): CollectionContext<Value> {
export function useCollectionProvider<Value = unknown>(
key: string = DEFAULT_COLLECTION_KEY,
): CollectionContext<Value> {
const ctx = createCollectionState<Value>();
CollectionCtx.provide(ctx as CollectionContext);
getCollectionContextFactory(key).provide(ctx as CollectionContext);
return ctx;
}
/**
* Injects the collection context from the nearest `useCollectionProvider()`.
* Injects the collection context from the nearest `useCollectionProvider()`
* called with the same `key`.
* Call this in children (e.g. `RovingFocusItem`, `ListboxItem`).
*
* @throws when used outside a provider.
*/
export function useCollectionInjector<Value = unknown>(): CollectionContext<Value> {
return CollectionCtx.inject() as CollectionContext<Value>;
export function useCollectionInjector<Value = unknown>(
key: string = DEFAULT_COLLECTION_KEY,
): CollectionContext<Value> {
return getCollectionContextFactory(key).inject() as CollectionContext<Value>;
}