diff --git a/src/common/layout/optima/portals/OptimaPortalIn.tsx b/src/common/layout/optima/portals/OptimaPortalIn.tsx new file mode 100644 index 000000000..c18b47995 --- /dev/null +++ b/src/common/layout/optima/portals/OptimaPortalIn.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import { createPortal } from 'react-dom'; + +import { OptimaPortalId, usePortalElement } from './store-optima-portals'; + + +export function OptimaPortalIn(props: { + targetPortalId: OptimaPortalId; + children: React.ReactNode; +}) { + + // react to the portal being made available + const portalElement = usePortalElement(props.targetPortalId); + if (!portalElement) + return null; + + return createPortal(props.children, portalElement); +} diff --git a/src/common/layout/optima/portals/store-optima-portals.ts b/src/common/layout/optima/portals/store-optima-portals.ts new file mode 100644 index 000000000..6d2a90e63 --- /dev/null +++ b/src/common/layout/optima/portals/store-optima-portals.ts @@ -0,0 +1,57 @@ +import { create } from 'zustand'; + + +// module configuration +const DEBUG_OPTIMA_PORTALS = false; + + +export type OptimaPortalId = + | 'optima-portal-drawer' + | 'optima-portal-properties' + | 'optima-portal-toolbar' + ; + +interface PortalState { + portalElements: Map; +} + +interface PortalActions { + addPortal: (id: OptimaPortalId, element: HTMLElement) => void; + removePortal: (id: OptimaPortalId) => void; +} + +type PortalStore = PortalState & PortalActions; + + +const useOptimaPortalsStore = create((set, get) => ({ + + // init state + portalElements: new Map(), + + // actions + addPortal: (id, element) => set((state) => { + const newPortals = new Map(state.portalElements); + newPortals.set(id, element); + if (DEBUG_OPTIMA_PORTALS) + console.log(' > store.addPortal', id, !!element); + return { portalElements: newPortals }; + }), + + removePortal: (id) => set((state) => { + const newPortals = new Map(state.portalElements); + newPortals.delete(id); + if (DEBUG_OPTIMA_PORTALS) + console.log(' < store.removePortal', id); + return { portalElements: newPortals }; + }), + +})); + + +export function optimaPortalsActions(): PortalActions { + return useOptimaPortalsStore.getState(); +} + +export function usePortalElement(id: OptimaPortalId) { + return useOptimaPortalsStore((state) => state.portalElements.get(id) || null); +} diff --git a/src/common/layout/optima/portals/useOptimaPortalOut.tsx b/src/common/layout/optima/portals/useOptimaPortalOut.tsx new file mode 100644 index 000000000..765ac4e7f --- /dev/null +++ b/src/common/layout/optima/portals/useOptimaPortalOut.tsx @@ -0,0 +1,45 @@ +import * as React from 'react'; + +import { OptimaPortalId, optimaPortalsActions } from './store-optima-portals'; + + +/** + * Note: this hook assumes that the ref is created by when useLayoutEffect is called, + * and will warn otherwise. + */ +export function useOptimaPortalOut(portalTargetId: OptimaPortalId, debugCallerName: string) { + + // state + const ref = React.useRef(null); + + React.useLayoutEffect(() => { + const { addPortal, removePortal } = optimaPortalsActions(); + if (!ref.current) { + console.warn(`useOptimaPortalOut: ref.current is null for type ${portalTargetId} (called by ${debugCallerName})`); + } else { + addPortal(portalTargetId, ref.current); + } + return () => removePortal(portalTargetId); + }, [debugCallerName, portalTargetId]); + + return ref; +} + +/* +// This version will add the portal only when really getting the ref +export function useOptimaPortalOut(portalTargetId: OptimaPortalId, debugCallerName: string) { + + const setRef = React.useCallback((node: HTMLElement | null) => { + const { addPortal, removePortal } = optimaPortalsActions(); + console.log('useOptimaPortalOut.setRef', portalTargetId, node); + if (node) { + console.log(' - useOptimaPortalOut call AddPortal', portalTargetId); + addPortal(portalTargetId, node); + } else { + removePortal(portalTargetId); + } + }, [portalTargetId]); + + return setRef; +} +*/ \ No newline at end of file