diff --git a/apps/meteor/client/providers/ModalProvider.tsx b/apps/meteor/client/providers/ModalProvider.tsx index e3bab9d296b..868cccf5005 100644 --- a/apps/meteor/client/providers/ModalProvider.tsx +++ b/apps/meteor/client/providers/ModalProvider.tsx @@ -1,9 +1,7 @@ import { ModalContext } from '@rocket.chat/ui-contexts'; -import React, { useState, useMemo, memo, ReactNode, useCallback, ReactElement } from 'react'; +import React, { useState, useMemo, memo, ReactNode, ReactElement } from 'react'; import { modal } from '../../app/ui-utils/client/lib/modal'; -import ModalBackdrop from '../components/modal/ModalBackdrop'; -import ModalPortal from '../components/modal/ModalPortal'; import { useImperativeModal } from '../views/hooks/useImperativeModal'; type ModalProviderProps = { @@ -14,27 +12,18 @@ const ModalProvider = ({ children }: ModalProviderProps): ReactElement => { const [currentModal, setCurrentModal] = useState(null); const contextValue = useMemo( - () => - Object.assign(modal, { + () => ({ + modal: Object.assign(modal, { setModal: setCurrentModal, }), - [], + currentModal, + }), + [currentModal], ); useImperativeModal(setCurrentModal); - const handleDismiss = useCallback(() => setCurrentModal(null), [setCurrentModal]); - - return ( - - {children} - {currentModal && ( - - {currentModal} - - )} - - ); + return ; }; export default memo(ModalProvider); diff --git a/apps/meteor/client/stories/contexts/ModalContextMock.tsx b/apps/meteor/client/stories/contexts/ModalContextMock.tsx index 7abc158dcc1..cca7c117560 100644 --- a/apps/meteor/client/stories/contexts/ModalContextMock.tsx +++ b/apps/meteor/client/stories/contexts/ModalContextMock.tsx @@ -9,16 +9,22 @@ type ModalContextMockProps = { }; const ModalContextMock = ({ children }: ModalContextMockProps): ReactElement => { - const parent = useContext(ModalContext); + const context = useContext(ModalContext); const value = useMemo( - (): ContextType => ({ - ...parent, - setModal: (modal): void => { - logAction('setModal', modal); - }, - }), - [parent], + (): ContextType => + context?.modal + ? { + modal: { + ...context.modal, + setModal: (modal): void => { + logAction('setModal', modal); + }, + }, + currentModal: context.currentModal, + } + : undefined, + [context], ); return ; diff --git a/apps/meteor/client/views/modal/ModalRegion.tsx b/apps/meteor/client/views/modal/ModalRegion.tsx new file mode 100644 index 00000000000..928683dbd87 --- /dev/null +++ b/apps/meteor/client/views/modal/ModalRegion.tsx @@ -0,0 +1,23 @@ +import { useModal, useCurrentModal } from '@rocket.chat/ui-contexts'; +import React, { useCallback, ReactElement } from 'react'; + +import ModalBackdrop from '../../components/modal/ModalBackdrop'; +import ModalPortal from '../../components/modal/ModalPortal'; + +const ModalRegion = (): ReactElement | null => { + const currentModal = useCurrentModal(); + const { setModal } = useModal(); + const handleDismiss = useCallback(() => setModal(null), [setModal]); + + if (!currentModal) { + return null; + } + + return ( + + {currentModal} + + ); +}; + +export default ModalRegion; diff --git a/apps/meteor/client/views/root/AppRoot.tsx b/apps/meteor/client/views/root/AppRoot.tsx index 31e35d05cde..38f49b63823 100644 --- a/apps/meteor/client/views/root/AppRoot.tsx +++ b/apps/meteor/client/views/root/AppRoot.tsx @@ -10,6 +10,7 @@ const MeteorProvider = lazy(() => import('../../providers/MeteorProvider')); const BannerRegion = lazy(() => import('../banners/BannerRegion')); const AppLayout = lazy(() => import('./AppLayout')); const PortalsWrapper = lazy(() => import('./PortalsWrapper')); +const ModalRegion = lazy(() => import('../modal/ModalRegion')); const AppRoot: FC = () => ( }> @@ -20,6 +21,7 @@ const AppRoot: FC = () => ( + diff --git a/packages/ui-contexts/src/ModalContext.ts b/packages/ui-contexts/src/ModalContext.ts index 3d65fbb53d4..983c9c6d0dc 100644 --- a/packages/ui-contexts/src/ModalContext.ts +++ b/packages/ui-contexts/src/ModalContext.ts @@ -35,39 +35,25 @@ type ModalInstance = ModalConfiguration & { }; export type ModalContextValue = { - open( - config?: ModalConfiguration, - fn?: (instance: ModalInstance, value: unknown) => void, - onCancel?: (instance: ModalInstance) => void, - ): void; - push( - config?: ModalConfiguration, - fn?: (instance: ModalInstance, value: unknown) => void, - onCancel?: (instance: ModalInstance) => void, - ): ModalInstance; - cancel(): void; - close(): void; - confirm(value: unknown): void; - showInputError(text: string): void; - onKeyDown(event: KeyboardEvent): void; - setModal(modal: ReactNode): void; + modal: { + open( + config?: ModalConfiguration, + fn?: (instance: ModalInstance, value: unknown) => void, + onCancel?: (instance: ModalInstance) => void, + ): void; + push( + config?: ModalConfiguration, + fn?: (instance: ModalInstance, value: unknown) => void, + onCancel?: (instance: ModalInstance) => void, + ): ModalInstance; + cancel(): void; + close(): void; + confirm(value: unknown): void; + showInputError(text: string): void; + onKeyDown(event: KeyboardEvent): void; + setModal(modal: ReactNode): void; + }; + currentModal: ReactNode; }; -export const ModalContext = createContext({ - open: () => undefined, - push: () => ({ - render: (): void => undefined, - hide: (): void => undefined, - destroy: (): void => undefined, - close: (): void => undefined, - confirm: (): void => undefined, - cancel: (): void => undefined, - showInputError: (): void => undefined, - }), - cancel: () => undefined, - close: () => undefined, - confirm: () => undefined, - showInputError: () => undefined, - onKeyDown: () => undefined, - setModal: () => undefined, -}); +export const ModalContext = createContext(undefined); diff --git a/packages/ui-contexts/src/hooks/useCurrentModal.ts b/packages/ui-contexts/src/hooks/useCurrentModal.ts new file mode 100644 index 00000000000..692c6d7e024 --- /dev/null +++ b/packages/ui-contexts/src/hooks/useCurrentModal.ts @@ -0,0 +1,16 @@ +import { useContext } from 'react'; + +import { ModalContext, ModalContextValue } from '../ModalContext'; + +/** + * Similar to useModal this hook return the current modal from the context value + */ +export const useCurrentModal = (): ModalContextValue['currentModal'] => { + const context = useContext(ModalContext); + + if (!context) { + throw new Error('useCurrentModal must be used inside Modal Context'); + } + + return context.currentModal; +}; diff --git a/packages/ui-contexts/src/hooks/useModal.ts b/packages/ui-contexts/src/hooks/useModal.ts index cda4bcfe0bd..b2b0ea26ca5 100644 --- a/packages/ui-contexts/src/hooks/useModal.ts +++ b/packages/ui-contexts/src/hooks/useModal.ts @@ -2,4 +2,15 @@ import { useContext } from 'react'; import { ModalContext, ModalContextValue } from '../ModalContext'; -export const useModal = (): ModalContextValue => useContext(ModalContext); +/** + * Consider using useCurrentModal to get the current modal + */ +export const useModal = (): ModalContextValue['modal'] => { + const context = useContext(ModalContext); + + if (!context) { + throw new Error('useModal must be used inside Modal Context'); + } + + return context.modal; +}; diff --git a/packages/ui-contexts/src/hooks/useSetModal.ts b/packages/ui-contexts/src/hooks/useSetModal.ts index 248d1e04096..5f69e087d9e 100644 --- a/packages/ui-contexts/src/hooks/useSetModal.ts +++ b/packages/ui-contexts/src/hooks/useSetModal.ts @@ -1,5 +1,5 @@ -import { ReactNode, useContext } from 'react'; +import type { ReactNode } from 'react'; -import { ModalContext } from '../ModalContext'; +import { useModal } from './useModal'; -export const useSetModal = (): ((modal?: ReactNode) => void) => useContext(ModalContext).setModal; +export const useSetModal = (): ((modal?: ReactNode) => void) => useModal().setModal; diff --git a/packages/ui-contexts/src/index.ts b/packages/ui-contexts/src/index.ts index 2a30306e8e8..9e82a199422 100644 --- a/packages/ui-contexts/src/index.ts +++ b/packages/ui-contexts/src/index.ts @@ -21,6 +21,7 @@ export { useAttachmentAutoLoadEmbedMedia } from './hooks/useAttachmentAutoLoadEm export { useAttachmentDimensions } from './hooks/useAttachmentDimensions'; export { useAttachmentIsCollapsedByDefault } from './hooks/useAttachmentIsCollapsedByDefault'; export { useConnectionStatus } from './hooks/useConnectionStatus'; +export { useCurrentModal } from './hooks/useCurrentModal'; export { useCurrentRoute } from './hooks/useCurrentRoute'; export { useCustomSound } from './hooks/useCustomSound'; export { useEndpoint } from './hooks/useEndpoint';