99 lines
2.9 KiB
TypeScript
99 lines
2.9 KiB
TypeScript
|
|
/**
|
||
|
|
* React hooks for the rect registry.
|
||
|
|
*
|
||
|
|
* Components mount, get a ref to a DOM node, and ask the registry to
|
||
|
|
* track it via `useRegisterRect(kind, id, ref)`. Unmount/ref-change
|
||
|
|
* unregisters automatically.
|
||
|
|
*
|
||
|
|
* The registry itself lives behind a React context so multiple subtrees
|
||
|
|
* can share one registry (the overlay sees what every renderer publishes).
|
||
|
|
*/
|
||
|
|
|
||
|
|
import {
|
||
|
|
createContext,
|
||
|
|
useCallback,
|
||
|
|
useContext,
|
||
|
|
useEffect,
|
||
|
|
useSyncExternalStore,
|
||
|
|
type RefObject,
|
||
|
|
} from "react";
|
||
|
|
|
||
|
|
import {
|
||
|
|
createRectRegistry,
|
||
|
|
type RectKind,
|
||
|
|
type RectRegistry,
|
||
|
|
} from "./rect-registry";
|
||
|
|
import { attachRectChangePumps, type RectChangeObserverHandle } from "./events";
|
||
|
|
|
||
|
|
export interface RectRegistryContextValue {
|
||
|
|
readonly registry: RectRegistry;
|
||
|
|
readonly observer: RectChangeObserverHandle;
|
||
|
|
}
|
||
|
|
|
||
|
|
const RectRegistryContext = createContext<RectRegistryContextValue | null>(null);
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Create an isolated registry + change pump pair for tests or app
|
||
|
|
* composition roots that wire their own provider.
|
||
|
|
*/
|
||
|
|
export function createRectRegistryContextValue(): RectRegistryContextValue {
|
||
|
|
const registry = createRectRegistry();
|
||
|
|
const observer = attachRectChangePumps(registry);
|
||
|
|
return { registry, observer };
|
||
|
|
}
|
||
|
|
|
||
|
|
export function useRectRegistryContext(): RectRegistryContextValue {
|
||
|
|
const ctx = useContext(RectRegistryContext);
|
||
|
|
if (!ctx) {
|
||
|
|
throw new Error(
|
||
|
|
"useRectRegistryContext must be used inside <RectRegistryProvider />",
|
||
|
|
);
|
||
|
|
}
|
||
|
|
return ctx;
|
||
|
|
}
|
||
|
|
|
||
|
|
export const RectRegistryProvider = RectRegistryContext.Provider;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Register a DOM ref's bounding rect with the registry.
|
||
|
|
*
|
||
|
|
* Re-runs when `kind`/`id`/`ref.current` change. The observer also starts
|
||
|
|
* watching the element for scroll/resize so the overlay can re-query
|
||
|
|
* without polling.
|
||
|
|
*/
|
||
|
|
export function useRegisterRect(
|
||
|
|
kind: RectKind,
|
||
|
|
id: string,
|
||
|
|
ref: RefObject<Element | null>,
|
||
|
|
): void {
|
||
|
|
const { registry, observer } = useRectRegistryContext();
|
||
|
|
useEffect(() => {
|
||
|
|
const el = ref.current;
|
||
|
|
if (!el) return;
|
||
|
|
const unregister = registry.register(kind, id, () => el.getBoundingClientRect());
|
||
|
|
const unobserve = observer.observe(el);
|
||
|
|
return () => {
|
||
|
|
unobserve();
|
||
|
|
unregister();
|
||
|
|
};
|
||
|
|
}, [kind, id, ref, registry, observer]);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Subscribe to registry change events from inside React. Returns a
|
||
|
|
* monotonically-increasing version number that bumps on every event, so
|
||
|
|
* `useMemo`/`useEffect` deps can include it to re-derive cached values.
|
||
|
|
*
|
||
|
|
* Implementation: leans on `registry.getVersion()` for the snapshot so
|
||
|
|
* `useSyncExternalStore` doesn't accumulate per-render subscribers.
|
||
|
|
*/
|
||
|
|
export function useRectRegistryVersion(): number {
|
||
|
|
const { registry } = useRectRegistryContext();
|
||
|
|
const subscribe = useCallback(
|
||
|
|
(callback: () => void) => registry.subscribe(callback),
|
||
|
|
[registry],
|
||
|
|
);
|
||
|
|
const getSnapshot = useCallback(() => registry.getVersion(), [registry]);
|
||
|
|
return useSyncExternalStore(subscribe, getSnapshot, () => 0);
|
||
|
|
}
|