Skip to content

Commit 81c5187

Browse files
committed
feat(Dialog): use provider for useDialogContainer
1 parent e5b912b commit 81c5187

File tree

5 files changed

+134
-47
lines changed

5 files changed

+134
-47
lines changed

.changeset/big-dryers-perform.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@cube-dev/ui-kit': minor
3+
---
4+
5+
Render dialogs managed by useDialogContainer via provider.

src/components/Root.tsx

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { TOKENS } from '../tokens';
1515
import { useViewportSize } from '../utils/react';
1616
import { TrackingProps, TrackingProvider } from '../providers/TrackingProvider';
1717

18+
import { DialogProvider } from './overlays/Dialog/index';
1819
import { PortalProvider } from './portal';
1920
import { GlobalStyles } from './GlobalStyles';
2021
import { AlertDialogApiProvider } from './overlays/AlertDialog';
@@ -148,13 +149,15 @@ export function Root(allProps: CubeRootProps) {
148149
monospaceFont={monospaceFont}
149150
fontDisplay={fontDisplay}
150151
/>
151-
<ModalProvider>
152-
<PortalProvider value={ref}>
153-
<NotificationsProvider rootRef={ref}>
154-
<AlertDialogApiProvider>{children}</AlertDialogApiProvider>
155-
</NotificationsProvider>
156-
</PortalProvider>
157-
</ModalProvider>
152+
<DialogProvider>
153+
<ModalProvider>
154+
<PortalProvider value={ref}>
155+
<NotificationsProvider rootRef={ref}>
156+
<AlertDialogApiProvider>{children}</AlertDialogApiProvider>
157+
</NotificationsProvider>
158+
</PortalProvider>
159+
</ModalProvider>
160+
</DialogProvider>
158161
</RootElement>
159162
</StyleSheetManager>
160163
</TrackingProvider>

src/components/overlays/Dialog/dialog-container.tsx

Lines changed: 0 additions & 39 deletions
This file was deleted.

src/components/overlays/Dialog/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ export * from './DialogContainer';
22
export * from './DialogForm';
33
export * from './DialogTrigger';
44
export * from './Dialog';
5-
export * from './dialog-container';
5+
export * from './use-dialog-container';
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import React, {
2+
createContext,
3+
useState,
4+
useContext,
5+
useCallback,
6+
useEffect,
7+
useRef,
8+
useMemo,
9+
ReactNode,
10+
} from 'react';
11+
12+
import { useEvent } from '../../../_internal/index';
13+
14+
import { DialogContainer } from './DialogContainer';
15+
16+
// Define a context type to handle dialog operations
17+
interface DialogContextType {
18+
addDialog: (element: React.ReactNode) => number;
19+
removeDialog: (id: number) => void;
20+
updateDialog: (id: number, element: React.ReactNode) => void;
21+
}
22+
23+
// Create a context for dialogs
24+
const DialogContext = createContext<DialogContextType | null>(null);
25+
26+
// Provider component that renders dialogs outside the normal tree
27+
export const DialogProvider = ({ children }: { children: ReactNode }) => {
28+
const [dialogs, setDialogs] = useState<
29+
Array<{ id: number; element: React.ReactNode }>
30+
>([]);
31+
32+
const addDialog = useCallback((element: React.ReactNode) => {
33+
// Create a unique id for the dialog
34+
const id = Date.now() + Math.random();
35+
36+
setDialogs((prev) => [...prev, { id, element }]);
37+
38+
return id;
39+
}, []);
40+
41+
const removeDialog = useCallback((id: number) => {
42+
setDialogs((prev) => prev.filter((dialog) => dialog.id !== id));
43+
}, []);
44+
45+
const updateDialog = useCallback((id: number, element: React.ReactNode) => {
46+
setDialogs((prev) =>
47+
prev.map((dialog) => (dialog.id === id ? { id, element } : dialog)),
48+
);
49+
}, []);
50+
51+
return (
52+
<DialogContext.Provider value={{ addDialog, removeDialog, updateDialog }}>
53+
{children}
54+
{dialogs.map(({ id, element }) => (
55+
<React.Fragment key={id}>{element}</React.Fragment>
56+
))}
57+
</DialogContext.Provider>
58+
);
59+
};
60+
61+
/**
62+
* Custom hook to open a dialog using a global context.
63+
*
64+
* @param Component - A React component representing the dialog content. It receives props of type P.
65+
* @returns An object with an `open` function to display the dialog and a generic `close` function.
66+
*/
67+
export function useDialogContainer<P>(Component: React.ComponentType<P>) {
68+
const context = useContext(DialogContext);
69+
if (!context) {
70+
throw new Error('useDialogContainer must be used within a DialogProvider');
71+
}
72+
const { addDialog, removeDialog, updateDialog } = context;
73+
74+
const [isOpen, setIsOpen] = useState(false);
75+
const [componentProps, setComponentProps] = useState<P | null>(null);
76+
77+
const open = useEvent((props: P) => {
78+
setComponentProps(props);
79+
setIsOpen(true);
80+
});
81+
82+
const close = useEvent(() => {
83+
setIsOpen(false);
84+
});
85+
86+
const renderedElement = useMemo(
87+
() =>
88+
componentProps ? (
89+
<DialogContainer isOpen={isOpen} onDismiss={close}>
90+
{componentProps && <Component {...componentProps} />}
91+
</DialogContainer>
92+
) : null,
93+
[isOpen, componentProps],
94+
);
95+
96+
const dialogIdRef = useRef<number | null>(null);
97+
98+
useEffect(() => {
99+
// Register the dialog on mount
100+
dialogIdRef.current = addDialog(renderedElement);
101+
102+
return () => {
103+
// Remove the dialog on unmount
104+
if (dialogIdRef.current !== null) {
105+
removeDialog(dialogIdRef.current);
106+
}
107+
};
108+
}, []);
109+
110+
useEffect(() => {
111+
// Update the dialog when the rendered element changes
112+
if (dialogIdRef.current !== null) {
113+
updateDialog(dialogIdRef.current, renderedElement);
114+
}
115+
}, [renderedElement]);
116+
117+
return { open, close };
118+
}

0 commit comments

Comments
 (0)