diff --git a/.changeset/big-dryers-perform.md b/.changeset/big-dryers-perform.md new file mode 100644 index 000000000..fc8b49f09 --- /dev/null +++ b/.changeset/big-dryers-perform.md @@ -0,0 +1,5 @@ +--- +'@cube-dev/ui-kit': minor +--- + +Render dialogs managed by useDialogContainer via provider. diff --git a/src/components/Root.tsx b/src/components/Root.tsx index e5c56178d..1f0790bbf 100644 --- a/src/components/Root.tsx +++ b/src/components/Root.tsx @@ -15,6 +15,7 @@ import { TOKENS } from '../tokens'; import { useViewportSize } from '../utils/react'; import { TrackingProps, TrackingProvider } from '../providers/TrackingProvider'; +import { DialogProvider } from './overlays/Dialog/index'; import { PortalProvider } from './portal'; import { GlobalStyles } from './GlobalStyles'; import { AlertDialogApiProvider } from './overlays/AlertDialog'; @@ -150,9 +151,11 @@ export function Root(allProps: CubeRootProps) { /> - - {children} - + + + {children} + + diff --git a/src/components/overlays/Dialog/dialog-container.tsx b/src/components/overlays/Dialog/dialog-container.tsx deleted file mode 100644 index 809b5c006..000000000 --- a/src/components/overlays/Dialog/dialog-container.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import React, { useState, useMemo } from 'react'; - -import { useEvent } from '../../../_internal/index'; - -import { DialogContainer } from './DialogContainer'; - -/** - * Generic hook to manage a dialog component. - * - * @param Component - A React component that represents the dialog content. It must accept props of type P. - * @returns An object with `open` function to open the dialog with provided props and `rendered` JSX element to include in your component tree. - */ -export function useDialogContainer

(Component: React.ComponentType

) { - const [isOpen, setIsOpen] = useState(false); - const [componentProps, setComponentProps] = useState

(null); - - // 'open' accepts props required by the Component and opens the dialog - const open = useEvent((props: P) => { - setComponentProps(props); - setIsOpen(true); - }); - - const close = useEvent(() => { - setIsOpen(false); - }); - - // Render the dialog only when componentProps is set - const rendered = useMemo(() => { - if (!componentProps) return null; - - return ( - - - - ); - }, [componentProps, isOpen]); - - return { open, close, rendered }; -} diff --git a/src/components/overlays/Dialog/index.ts b/src/components/overlays/Dialog/index.ts index 3eecaa079..7865f04c8 100644 --- a/src/components/overlays/Dialog/index.ts +++ b/src/components/overlays/Dialog/index.ts @@ -2,4 +2,4 @@ export * from './DialogContainer'; export * from './DialogForm'; export * from './DialogTrigger'; export * from './Dialog'; -export * from './dialog-container'; +export * from './use-dialog-container'; diff --git a/src/components/overlays/Dialog/use-dialog-container.tsx b/src/components/overlays/Dialog/use-dialog-container.tsx new file mode 100644 index 000000000..97bf752cd --- /dev/null +++ b/src/components/overlays/Dialog/use-dialog-container.tsx @@ -0,0 +1,118 @@ +import React, { + createContext, + useState, + useContext, + useCallback, + useEffect, + useRef, + useMemo, + ReactNode, +} from 'react'; + +import { useEvent } from '../../../_internal/index'; + +import { DialogContainer } from './DialogContainer'; + +// Define a context type to handle dialog operations +interface DialogContextType { + addDialog: (element: React.ReactNode) => number; + removeDialog: (id: number) => void; + updateDialog: (id: number, element: React.ReactNode) => void; +} + +// Create a context for dialogs +const DialogContext = createContext(null); + +// Provider component that renders dialogs outside the normal tree +export const DialogProvider = ({ children }: { children: ReactNode }) => { + const [dialogs, setDialogs] = useState< + Array<{ id: number; element: React.ReactNode }> + >([]); + + const addDialog = useCallback((element: React.ReactNode) => { + // Create a unique id for the dialog + const id = Date.now() + Math.random(); + + setDialogs((prev) => [...prev, { id, element }]); + + return id; + }, []); + + const removeDialog = useCallback((id: number) => { + setDialogs((prev) => prev.filter((dialog) => dialog.id !== id)); + }, []); + + const updateDialog = useCallback((id: number, element: React.ReactNode) => { + setDialogs((prev) => + prev.map((dialog) => (dialog.id === id ? { id, element } : dialog)), + ); + }, []); + + return ( + + {children} + {dialogs.map(({ id, element }) => ( + {element} + ))} + + ); +}; + +/** + * Custom hook to open a dialog using a global context. + * + * @param Component - A React component representing the dialog content. It receives props of type P. + * @returns An object with an `open` function to display the dialog and a generic `close` function. + */ +export function useDialogContainer

(Component: React.ComponentType

) { + const context = useContext(DialogContext); + if (!context) { + throw new Error('useDialogContainer must be used within a DialogProvider'); + } + const { addDialog, removeDialog, updateDialog } = context; + + const [isOpen, setIsOpen] = useState(false); + const [componentProps, setComponentProps] = useState

(null); + + const open = useEvent((props: P) => { + setComponentProps(props); + setIsOpen(true); + }); + + const close = useEvent(() => { + setIsOpen(false); + }); + + const renderedElement = useMemo( + () => + componentProps ? ( + + {componentProps && } + + ) : null, + [isOpen, componentProps], + ); + + const dialogIdRef = useRef(null); + + useEffect(() => { + // Register the dialog on mount + dialogIdRef.current = addDialog(renderedElement); + + return () => { + // Remove the dialog on unmount + if (dialogIdRef.current !== null) { + removeDialog(dialogIdRef.current); + } + }; + }, []); + + useEffect(() => { + // Update the dialog when the rendered element changes + if (dialogIdRef.current !== null) { + updateDialog(dialogIdRef.current, renderedElement); + } + }, [renderedElement]); + + return { open, close }; +}