All files / platform/core/src/hooks useViewportElement.ts

98.11% Statements 52/53
87.09% Branches 27/31
100% Functions 15/15
96.96% Lines 32/33

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109                  220x                         220x 220x   220x 847x     220x   2014x 1339x   675x 675x     172x 172x       32178x   778x   778x 348x           2420x     220x   880x     42192x 17686x   17686x   17686x                               36006x     15280x   32178x                       16863x 2406x   927x 4260x 4260x 6666x    
import React, { createContext, useContext, useState, useSyncExternalStore } from 'react';
 
type ViewportElementRegistry = {
  registerViewport: (viewportId: string, element: HTMLElement) => void;
  unregisterViewport: (viewportId: string) => void;
  getViewportElement: (viewportId: string) => HTMLElement | null;
  subscribe: (listener: () => void) => () => void;
};
 
const ViewportElementsContext = createContext<ViewportElementRegistry | undefined>(undefined);
 
/**
 * Per-viewport DOM elements, so that a component which is not a viewport's child
 * can still reach its element - the colorbar and the size and mouse-position
 * hooks all need one. The viewport that owns the element registers it as React
 * attaches it; everyone else looks it up by viewport ID.
 *
 * The registry lives outside React, so registering is not a render. Readers are
 * notified explicitly rather than relying on a re-render happening to occur -
 * see useViewportElement.
 */
function createViewportElementRegistry(): ViewportElementRegistry {
  const viewportElements = new Map<string, HTMLElement>();
  const listeners = new Set<() => void>();
 
  const notify = () => {
    listeners.forEach(listener => listener());
  };
 
  return {
    registerViewport: (viewportId: string, element: HTMLElement) => {
      if (viewportElements.get(viewportId) === element) {
        return;
      }
      viewportElements.set(viewportId, element);
      notify();
    },
    unregisterViewport: (viewportId: string) => {
      if (viewportElements.delete(viewportId)) {
        notify();
      }
    },
    getViewportElement: (viewportId: string): HTMLElement | null =>
      viewportElements.get(viewportId) || null,
    subscribe: (listener: () => void) => {
      listeners.add(listener);
 
      return () => {
        listeners.delete(listener);
      };
    },
  };
}
 
export const ViewportElementsProvider = ({ children }: { children: React.ReactNode }) => {
  // useState rather than useMemo: useMemo is a hint React may discard, and
  // discarding this would throw away every registration in the viewer.
  const [registry] = useState(createViewportElementRegistry);
 
  return React.createElement(ViewportElementsContext.Provider, { value: registry }E, children)E;
};
 
function useViewportElementRegistry(): ViewportElementRegistry {
  const registry = useContext(ViewportElementsContext);
 
  Iif (registry === undefined) {
    throw new Error('Viewport element hooks must be used within a ViewportElementsProvider');
  }
 
  return registry;
}
 
/**
 * The DOM element for a viewport, or null while none is attached. Re-renders the
 * caller when that viewport's element is registered or removed.
 *
 * Notification is registry-wide, but useSyncExternalStore compares the value it
 * reads back, so a caller watching one viewport does not re-render when another
 * viewport registers.
 *
 * The element type is the caller's assertion, as with useRef<T> - the registry
 * itself only knows it holds an HTMLElement.
 */
export function useViewportElement<T extends HTMLElement = HTMLElement>(
  viewportId: string
): T | null {
  const { getViewportElement, subscribe } = useViewportElementRegistry();
 
  return useSyncExternalStore(subscribe, () => getViewportElement(viewportId)) as T | null;
}
 
/**
 * For the component that owns a viewport's element. Subscribes to nothing, so
 * registering does not re-render the owner - which matters, because the owner is
 * the viewport itself.
 *
 * `unregister` is returned as a plain function rather than handled by a ref
 * cleanup so that callers keep control of when it runs relative to the rest of
 * their teardown.
 */
export const useViewportElementRegistration = (viewportId: string) => {
  const { registerViewport, unregisterViewport } = useViewportElementRegistry();
 
  return {
    register: (element: HTMLElement) => registerViewport(viewportId, element),
    unregister: () => unregisterViewport(viewportId),
  };
};