diff --git a/src/components/Dialog/ButtonWithSubmenu.tsx b/src/components/Dialog/ButtonWithSubmenu.tsx index 070b2eed9..74e18ba23 100644 --- a/src/components/Dialog/ButtonWithSubmenu.tsx +++ b/src/components/Dialog/ButtonWithSubmenu.tsx @@ -1,6 +1,6 @@ import clsx from 'clsx'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { useDialog, useDialogIsOpen } from './hooks'; +import { useDialogIsOpen, useDialogOnNearestManager } from './hooks'; import { useDialogAnchor } from './DialogAnchor'; import type { ComponentProps, ComponentType } from 'react'; import type { PopperLikePlacement } from './hooks'; @@ -24,8 +24,8 @@ export const ButtonWithSubmenu = ({ const keepSubmenuOpen = useRef(false); const dialogCloseTimeout = useRef(null); const dialogId = useMemo(() => `submenu-${Math.random().toString(36).slice(2)}`, []); - const dialog = useDialog({ id: dialogId }); - const dialogIsOpen = useDialogIsOpen(dialogId); + const { dialog, dialogManager } = useDialogOnNearestManager({ id: dialogId }); + const dialogIsOpen = useDialogIsOpen(dialogId, dialogManager?.id); const { setPopperElement, styles } = useDialogAnchor({ open: dialogIsOpen, placement, diff --git a/src/components/Dialog/DialogAnchor.tsx b/src/components/Dialog/DialogAnchor.tsx index 9ee362183..a94cb29df 100644 --- a/src/components/Dialog/DialogAnchor.tsx +++ b/src/components/Dialog/DialogAnchor.tsx @@ -60,6 +60,7 @@ export function useDialogAnchor({ export type DialogAnchorProps = PropsWithChildren> & { id: string; + dialogManagerId?: string; focus?: boolean; trapFocus?: boolean; } & ComponentProps<'div'>; @@ -68,6 +69,7 @@ export const DialogAnchor = ({ allowFlip = true, children, className, + dialogManagerId, focus = true, id, placement = 'auto', @@ -76,8 +78,8 @@ export const DialogAnchor = ({ trapFocus, ...restDivProps }: DialogAnchorProps) => { - const dialog = useDialog({ id }); - const open = useDialogIsOpen(id); + const dialog = useDialog({ dialogManagerId, id }); + const open = useDialogIsOpen(id, dialogManagerId); const { setPopperElement, styles } = useDialogAnchor({ allowFlip, open, @@ -105,7 +107,7 @@ export const DialogAnchor = ({ } return ( - +
{ - const { dialogManager } = useDialogManager(); - const openedDialogCount = useOpenedDialogCount(); + const { dialogManager } = useNearestDialogManagerContext() ?? {}; + const openedDialogCount = useOpenedDialogCount({ dialogManagerId: dialogManager?.id }); + // const [destinationRoot, setDestinationRoot] = useState(null); + + // todo: allow to configure and then enable + // useEffect(() => { + // if (!destinationRoot) return; + // const handleClickOutside = (event: MouseEvent) => { + // if (!destinationRoot?.contains(event.target as Node)) { + // dialogManager?.closeAll(); + // } + // }; + // document.addEventListener('click', handleClickOutside, { capture: true }); + // return () => { + // document.removeEventListener('click', handleClickOutside, { capture: true }); + // }; + // }, [destinationRoot, dialogManager]); if (!openedDialogCount) return null; return (
dialogManager.closeAll()} + onClick={() => dialogManager?.closeAll()} + // ref={setDestinationRoot} style={ { '--str-chat__dialog-overlay-height': openedDialogCount > 0 ? '100%' : '0', @@ -27,14 +43,16 @@ export const DialogPortalDestination = () => { type DialogPortalEntryProps = { dialogId: string; + dialogManagerId?: string; }; export const DialogPortalEntry = ({ children, dialogId, + dialogManagerId, }: PropsWithChildren) => { - const { dialogManager } = useDialogManager({ dialogId }); - const dialogIsOpen = useDialogIsOpen(dialogId, dialogManager.id); + const { dialogManager } = useDialogManager({ dialogId, dialogManagerId }); + const dialogIsOpen = useDialogIsOpen(dialogId, dialogManagerId); const getPortalDestination = useCallback( () => document.querySelector(`div[data-str-chat__portal-id="${dialogManager.id}"]`), diff --git a/src/components/Dialog/hooks/useDialog.ts b/src/components/Dialog/hooks/useDialog.ts index 73881effb..3c12ab758 100644 --- a/src/components/Dialog/hooks/useDialog.ts +++ b/src/components/Dialog/hooks/useDialog.ts @@ -1,5 +1,9 @@ import { useCallback, useEffect } from 'react'; -import { modalDialogManagerId, useDialogManager } from '../../../context'; +import { + modalDialogManagerId, + useDialogManager, + useNearestDialogManagerContext, +} from '../../../context'; import { useStateStore } from '../../../store'; import type { DialogManagerState, GetOrCreateDialogParams } from '../DialogManager'; @@ -25,6 +29,16 @@ export const useDialog = ({ dialogManagerId, id }: UseDialogParams) => { return dialogManager.getOrCreate({ id }); }; +export const useDialogOnNearestManager = ({ id }: Pick) => { + const { dialogManager } = useNearestDialogManagerContext() ?? {}; + const dialog = useDialog({ dialogManagerId: dialogManager?.id, id }); + + return { + dialog, + dialogManager, + }; +}; + export const modalDialogId = 'modal-dialog' as const; export const useModalDialog = () => diff --git a/src/components/MessageActions/MessageActions.tsx b/src/components/MessageActions/MessageActions.tsx index 5edf36f84..c3db70ca8 100644 --- a/src/components/MessageActions/MessageActions.tsx +++ b/src/components/MessageActions/MessageActions.tsx @@ -4,7 +4,7 @@ import React, { useCallback, useRef } from 'react'; import { MessageActionsBox } from './MessageActionsBox'; -import { DialogAnchor, useDialog, useDialogIsOpen } from '../Dialog'; +import { DialogAnchor, useDialogIsOpen, useDialogOnNearestManager } from '../Dialog'; import { ActionsIcon as DefaultActionsIcon } from '../Message/icons'; import { isUserMuted, shouldRenderMessageActions } from '../Message/utils'; @@ -85,8 +85,8 @@ export const MessageActions = (props: MessageActionsProps) => { const dialogIdNamespace = threadList ? '-thread-' : ''; const dialogId = `message-actions${dialogIdNamespace}--${message.id}`; - const dialog = useDialog({ id: dialogId }); - const dialogIsOpen = useDialogIsOpen(dialogId); + const { dialog, dialogManager } = useDialogOnNearestManager({ id: dialogId }); + const dialogIsOpen = useDialogIsOpen(dialogId, dialogManager?.id); const messageActions = getMessageActions(); @@ -108,6 +108,7 @@ export const MessageActions = (props: MessageActionsProps) => { toggleOpen={dialog?.toggle} > component', () => { it('should close message actions box on icon click if already opened', async () => { renderMessageActions(); expect(MessageActionsBoxMock).not.toHaveBeenCalled(); + const dialogOverlay = screen.queryByTestId(dialogOverlayTestId); + expect(dialogOverlay).not.toBeInTheDocument(); await toggleOpenMessageActions(); expect(MessageActionsBoxMock).toHaveBeenLastCalledWith( expect.objectContaining({ open: true }), undefined, ); await toggleOpenMessageActions(); - const dialogOverlay = screen.queryByTestId(dialogOverlayTestId); - expect(dialogOverlay).not.toBeInTheDocument(); + await waitFor(() => { + expect(dialogOverlay).not.toBeInTheDocument(); + }); }); it('should close message actions box when user clicks overlay if it is already opened', async () => { diff --git a/src/components/MessageInput/AttachmentSelector.tsx b/src/components/MessageInput/AttachmentSelector.tsx index 41dd31f86..b12585680 100644 --- a/src/components/MessageInput/AttachmentSelector.tsx +++ b/src/components/MessageInput/AttachmentSelector.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import { UploadIcon as DefaultUploadIcon } from './icons'; import { useAttachmentManagerState } from './hooks/useAttachmentManagerState'; import { CHANNEL_CONTAINER_ID } from '../Channel/constants'; -import { DialogAnchor, useDialog, useDialogIsOpen } from '../Dialog'; +import { DialogAnchor, useDialogIsOpen, useDialogOnNearestManager } from '../Dialog'; import { DialogMenuButton } from '../Dialog/DialogMenu'; import { Modal as DefaultModal } from '../Modal'; import { ShareLocationDialog as DefaultLocationDialog } from '../Location'; @@ -208,8 +208,10 @@ export const AttachmentSelector = ({ const actions = useAttachmentSelectorActionsFiltered(attachmentSelectorActionSet); const menuDialogId = `attachment-actions-menu${messageComposer.threadId ? '-thread' : ''}`; - const menuDialog = useDialog({ id: menuDialogId }); - const menuDialogIsOpen = useDialogIsOpen(menuDialogId); + const { dialog: menuDialog, dialogManager } = useDialogOnNearestManager({ + id: menuDialogId, + }); + const menuDialogIsOpen = useDialogIsOpen(menuDialogId, dialogManager?.id); const [modalContentAction, setModalContentActionAction] = useState(); @@ -255,6 +257,7 @@ export const AttachmentSelector = ({ +
>(null); const dialogIdNamespace = threadList ? '-thread-' : ''; const dialogId = `reaction-selector${dialogIdNamespace}--${message.id}`; - const dialog = useDialog({ id: dialogId }); - const dialogIsOpen = useDialogIsOpen(dialogId); + const { dialog, dialogManager } = useDialogOnNearestManager({ id: dialogId }); + const dialogIsOpen = useDialogIsOpen(dialogId, dialogManager?.id); + return ( <> useMemo(() => getDialogManager(modalDialogManagerId), []); + +export const useNearestDialogManagerContext = () => + useContext(DialogManagerProviderContext); diff --git a/src/experimental/MessageActions/MessageActions.tsx b/src/experimental/MessageActions/MessageActions.tsx index 794d3004a..2ce3517cc 100644 --- a/src/experimental/MessageActions/MessageActions.tsx +++ b/src/experimental/MessageActions/MessageActions.tsx @@ -4,7 +4,11 @@ import type { PropsWithChildren } from 'react'; import { useChatContext, useMessageContext, useTranslationContext } from '../../context'; import { ActionsIcon } from '../../components/Message/icons'; -import { DialogAnchor, useDialog, useDialogIsOpen } from '../../components/Dialog'; +import { + DialogAnchor, + useDialogIsOpen, + useDialogOnNearestManager, +} from '../../components/Dialog'; import { MessageActionsWrapper } from '../../components/MessageActions/MessageActions'; import { useBaseMessageActionSetFilter, useSplitMessageActionSet } from './hooks'; import { defaultMessageActionSet } from './defaults'; @@ -48,9 +52,12 @@ export const MessageActions = ({ const dropdownDialogId = `message-actions--${message.id}`; const reactionSelectorDialogId = `reaction-selector--${message.id}`; - const dialog = useDialog({ id: dropdownDialogId }); - const dropdownDialogIsOpen = useDialogIsOpen(dropdownDialogId); - const reactionSelectorDialogIsOpen = useDialogIsOpen(reactionSelectorDialogId); + const { dialog, dialogManager } = useDialogOnNearestManager({ id: dropdownDialogId }); + const dropdownDialogIsOpen = useDialogIsOpen(dropdownDialogId, dialogManager?.id); + const reactionSelectorDialogIsOpen = useDialogIsOpen( + reactionSelectorDialogId, + dialogManager?.id, + ); // do not render anything if total action count is zero if (dropdownActionSet.length + quickActionSet.length === 0) { @@ -78,6 +85,7 @@ export const MessageActions = ({