UNPKG

@gravity-ui/graph

Version:

Modern graph editor component

104 lines (103 loc) 3.46 kB
import { forwardRef, useImperativeHandle, useState } from "react"; import { createPortal } from "react-dom"; import { GraphState } from "../graph"; import { Layer } from "../services/Layer"; import { useGraphContext } from "./GraphContext"; import { useGraphEvent } from "./hooks/useGraphEvents"; import { useLayer } from "./hooks/useLayer"; /** * Internal Layer class for GraphPortal * Creates HTML element and provides it through portal */ class GraphPortalLayer extends Layer { constructor(props) { super({ html: { zIndex: props.zIndex ?? 100, classNames: ["graph-portal-layer", "no-pointer-events", "no-user-select"].concat(props.className ? [props.className] : []), transformByCameraPosition: props.transformByCameraPosition ?? false, }, ...props, }); } /** * Get HTML element for creating portal */ getPortalTarget() { return this.getHTML(); } } /** * Declarative component for creating HTML layers using render prop pattern. * * Creates internal Layer with HTML element and renders passed * React components through React Portal without need to create * separate Layer class. * * @example * ```tsx * function MyGraph() { * const { graph } = useGraph(); * const portalRef = useRef<GraphPortalLayer>(null); * * return ( * <GraphCanvas graph={graph} renderBlock={renderBlock}> * <GraphPortal * ref={portalRef} * className="my-custom-layer" * zIndex={200} * transformByCameraPosition={true} * > * <div style={{ position: 'absolute', top: 10, left: 10 }}> * <h3>Custom UI Layer</h3> * <button onClick={() => portalRef.current?.hide()}> * Hide layer * </button> * </div> * </GraphPortal> * </GraphCanvas> * ); * } * ``` * * @example * ```tsx * // With render prop for accessing layer and graph * <GraphPortal> * {(layer, graph) => ( * <div onClick={() => layer.hide()}> * Graph blocks count: {graph.api.getBlocks().length} * </div> * )} * </GraphPortal> * ``` */ export const GraphPortal = forwardRef(function GraphPortal({ className, zIndex, transformByCameraPosition = false, children }, ref) { // Get graph from context const { graph } = useGraphContext(); // Track graph state to determine readiness for portal creation const [graphState, setGraphState] = useState(graph?.state ?? GraphState.INIT); // Subscribe to graph state changes useGraphEvent(graph, "state-change", ({ state }) => { setGraphState(state); }); // Always create internal layer using useLayer (hooks must be called unconditionally) const layer = useLayer(graph, GraphPortalLayer, { className, zIndex, transformByCameraPosition, }); // Expose layer through ref useImperativeHandle(ref, () => layer, [layer]); // If graph is not ready or layer not yet created, don't render portal if (!graph || graphState < GraphState.ATTACHED || !layer) { return null; } // If no HTML element, don't render portal const portalTarget = layer.getPortalTarget(); if (!portalTarget) { return null; } // Render portal content return createPortal(typeof children === "function" ? children(layer, graph) : children, portalTarget, "graph-portal"); });