Skip to content

Commit e4081c8

Browse files
authored
fix: use the nearest dialog manager to display dialogs (#2865)
1 parent fe84cf9 commit e4081c8

File tree

11 files changed

+86
-32
lines changed

11 files changed

+86
-32
lines changed

src/components/Dialog/ButtonWithSubmenu.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import clsx from 'clsx';
22
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3-
import { useDialog, useDialogIsOpen } from './hooks';
3+
import { useDialogIsOpen, useDialogOnNearestManager } from './hooks';
44
import { useDialogAnchor } from './DialogAnchor';
55
import type { ComponentProps, ComponentType } from 'react';
66
import type { PopperLikePlacement } from './hooks';
@@ -24,8 +24,8 @@ export const ButtonWithSubmenu = ({
2424
const keepSubmenuOpen = useRef(false);
2525
const dialogCloseTimeout = useRef<NodeJS.Timeout | null>(null);
2626
const dialogId = useMemo(() => `submenu-${Math.random().toString(36).slice(2)}`, []);
27-
const dialog = useDialog({ id: dialogId });
28-
const dialogIsOpen = useDialogIsOpen(dialogId);
27+
const { dialog, dialogManager } = useDialogOnNearestManager({ id: dialogId });
28+
const dialogIsOpen = useDialogIsOpen(dialogId, dialogManager?.id);
2929
const { setPopperElement, styles } = useDialogAnchor<HTMLDivElement>({
3030
open: dialogIsOpen,
3131
placement,

src/components/Dialog/DialogAnchor.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export function useDialogAnchor<T extends HTMLElement>({
6060

6161
export type DialogAnchorProps = PropsWithChildren<Partial<DialogAnchorOptions>> & {
6262
id: string;
63+
dialogManagerId?: string;
6364
focus?: boolean;
6465
trapFocus?: boolean;
6566
} & ComponentProps<'div'>;
@@ -68,6 +69,7 @@ export const DialogAnchor = ({
6869
allowFlip = true,
6970
children,
7071
className,
72+
dialogManagerId,
7173
focus = true,
7274
id,
7375
placement = 'auto',
@@ -76,8 +78,8 @@ export const DialogAnchor = ({
7678
trapFocus,
7779
...restDivProps
7880
}: DialogAnchorProps) => {
79-
const dialog = useDialog({ id });
80-
const open = useDialogIsOpen(id);
81+
const dialog = useDialog({ dialogManagerId, id });
82+
const open = useDialogIsOpen(id, dialogManagerId);
8183
const { setPopperElement, styles } = useDialogAnchor<HTMLDivElement>({
8284
allowFlip,
8385
open,
@@ -105,7 +107,7 @@ export const DialogAnchor = ({
105107
}
106108

107109
return (
108-
<DialogPortalEntry dialogId={id}>
110+
<DialogPortalEntry dialogId={id} dialogManagerId={dialogManagerId}>
109111
<FocusScope autoFocus={focus} contain={trapFocus} restoreFocus>
110112
<div
111113
{...restDivProps}

src/components/Dialog/DialogPortal.tsx

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,36 @@ import type { PropsWithChildren } from 'react';
22
import React, { useCallback } from 'react';
33
import { useDialogIsOpen, useOpenedDialogCount } from './hooks';
44
import { Portal } from '../Portal/Portal';
5-
import { useDialogManager } from '../../context';
5+
import { useDialogManager, useNearestDialogManagerContext } from '../../context';
66

77
export const DialogPortalDestination = () => {
8-
const { dialogManager } = useDialogManager();
9-
const openedDialogCount = useOpenedDialogCount();
8+
const { dialogManager } = useNearestDialogManagerContext() ?? {};
9+
const openedDialogCount = useOpenedDialogCount({ dialogManagerId: dialogManager?.id });
10+
// const [destinationRoot, setDestinationRoot] = useState<HTMLDivElement | null>(null);
11+
12+
// todo: allow to configure and then enable
13+
// useEffect(() => {
14+
// if (!destinationRoot) return;
15+
// const handleClickOutside = (event: MouseEvent) => {
16+
// if (!destinationRoot?.contains(event.target as Node)) {
17+
// dialogManager?.closeAll();
18+
// }
19+
// };
20+
// document.addEventListener('click', handleClickOutside, { capture: true });
21+
// return () => {
22+
// document.removeEventListener('click', handleClickOutside, { capture: true });
23+
// };
24+
// }, [destinationRoot, dialogManager]);
1025

1126
if (!openedDialogCount) return null;
1227

1328
return (
1429
<div
1530
className='str-chat__dialog-overlay'
16-
data-str-chat__portal-id={dialogManager.id}
31+
data-str-chat__portal-id={dialogManager?.id}
1732
data-testid='str-chat__dialog-overlay'
18-
onClick={() => dialogManager.closeAll()}
33+
onClick={() => dialogManager?.closeAll()}
34+
// ref={setDestinationRoot}
1935
style={
2036
{
2137
'--str-chat__dialog-overlay-height': openedDialogCount > 0 ? '100%' : '0',
@@ -27,14 +43,16 @@ export const DialogPortalDestination = () => {
2743

2844
type DialogPortalEntryProps = {
2945
dialogId: string;
46+
dialogManagerId?: string;
3047
};
3148

3249
export const DialogPortalEntry = ({
3350
children,
3451
dialogId,
52+
dialogManagerId,
3553
}: PropsWithChildren<DialogPortalEntryProps>) => {
36-
const { dialogManager } = useDialogManager({ dialogId });
37-
const dialogIsOpen = useDialogIsOpen(dialogId, dialogManager.id);
54+
const { dialogManager } = useDialogManager({ dialogId, dialogManagerId });
55+
const dialogIsOpen = useDialogIsOpen(dialogId, dialogManagerId);
3856

3957
const getPortalDestination = useCallback(
4058
() => document.querySelector(`div[data-str-chat__portal-id="${dialogManager.id}"]`),

src/components/Dialog/hooks/useDialog.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { useCallback, useEffect } from 'react';
2-
import { modalDialogManagerId, useDialogManager } from '../../../context';
2+
import {
3+
modalDialogManagerId,
4+
useDialogManager,
5+
useNearestDialogManagerContext,
6+
} from '../../../context';
37
import { useStateStore } from '../../../store';
48

59
import type { DialogManagerState, GetOrCreateDialogParams } from '../DialogManager';
@@ -25,6 +29,16 @@ export const useDialog = ({ dialogManagerId, id }: UseDialogParams) => {
2529
return dialogManager.getOrCreate({ id });
2630
};
2731

32+
export const useDialogOnNearestManager = ({ id }: Pick<UseDialogParams, 'id'>) => {
33+
const { dialogManager } = useNearestDialogManagerContext() ?? {};
34+
const dialog = useDialog({ dialogManagerId: dialogManager?.id, id });
35+
36+
return {
37+
dialog,
38+
dialogManager,
39+
};
40+
};
41+
2842
export const modalDialogId = 'modal-dialog' as const;
2943

3044
export const useModalDialog = () =>

src/components/MessageActions/MessageActions.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import React, { useCallback, useRef } from 'react';
44

55
import { MessageActionsBox } from './MessageActionsBox';
66

7-
import { DialogAnchor, useDialog, useDialogIsOpen } from '../Dialog';
7+
import { DialogAnchor, useDialogIsOpen, useDialogOnNearestManager } from '../Dialog';
88
import { ActionsIcon as DefaultActionsIcon } from '../Message/icons';
99
import { isUserMuted, shouldRenderMessageActions } from '../Message/utils';
1010

@@ -85,8 +85,8 @@ export const MessageActions = (props: MessageActionsProps) => {
8585

8686
const dialogIdNamespace = threadList ? '-thread-' : '';
8787
const dialogId = `message-actions${dialogIdNamespace}--${message.id}`;
88-
const dialog = useDialog({ id: dialogId });
89-
const dialogIsOpen = useDialogIsOpen(dialogId);
88+
const { dialog, dialogManager } = useDialogOnNearestManager({ id: dialogId });
89+
const dialogIsOpen = useDialogIsOpen(dialogId, dialogManager?.id);
9090

9191
const messageActions = getMessageActions();
9292

@@ -108,6 +108,7 @@ export const MessageActions = (props: MessageActionsProps) => {
108108
toggleOpen={dialog?.toggle}
109109
>
110110
<DialogAnchor
111+
dialogManagerId={dialogManager?.id}
111112
id={dialogId}
112113
placement={isMine ? 'top-end' : 'top-start'}
113114
referenceElement={actionsBoxButtonRef.current}

src/components/MessageActions/__tests__/MessageActions.test.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from 'react';
22
import '@testing-library/jest-dom';
3-
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react';
3+
import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
44

55
import { MessageActions } from '../MessageActions';
66
import { MessageActionsBox as MessageActionsBoxMock } from '../MessageActionsBox';
@@ -137,14 +137,17 @@ describe('<MessageActions /> component', () => {
137137
it('should close message actions box on icon click if already opened', async () => {
138138
renderMessageActions();
139139
expect(MessageActionsBoxMock).not.toHaveBeenCalled();
140+
const dialogOverlay = screen.queryByTestId(dialogOverlayTestId);
141+
expect(dialogOverlay).not.toBeInTheDocument();
140142
await toggleOpenMessageActions();
141143
expect(MessageActionsBoxMock).toHaveBeenLastCalledWith(
142144
expect.objectContaining({ open: true }),
143145
undefined,
144146
);
145147
await toggleOpenMessageActions();
146-
const dialogOverlay = screen.queryByTestId(dialogOverlayTestId);
147-
expect(dialogOverlay).not.toBeInTheDocument();
148+
await waitFor(() => {
149+
expect(dialogOverlay).not.toBeInTheDocument();
150+
});
148151
});
149152

150153
it('should close message actions box when user clicks overlay if it is already opened', async () => {

src/components/MessageInput/AttachmentSelector.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react';
22
import { UploadIcon as DefaultUploadIcon } from './icons';
33
import { useAttachmentManagerState } from './hooks/useAttachmentManagerState';
44
import { CHANNEL_CONTAINER_ID } from '../Channel/constants';
5-
import { DialogAnchor, useDialog, useDialogIsOpen } from '../Dialog';
5+
import { DialogAnchor, useDialogIsOpen, useDialogOnNearestManager } from '../Dialog';
66
import { DialogMenuButton } from '../Dialog/DialogMenu';
77
import { Modal as DefaultModal } from '../Modal';
88
import { ShareLocationDialog as DefaultLocationDialog } from '../Location';
@@ -208,8 +208,10 @@ export const AttachmentSelector = ({
208208
const actions = useAttachmentSelectorActionsFiltered(attachmentSelectorActionSet);
209209

210210
const menuDialogId = `attachment-actions-menu${messageComposer.threadId ? '-thread' : ''}`;
211-
const menuDialog = useDialog({ id: menuDialogId });
212-
const menuDialogIsOpen = useDialogIsOpen(menuDialogId);
211+
const { dialog: menuDialog, dialogManager } = useDialogOnNearestManager({
212+
id: menuDialogId,
213+
});
214+
const menuDialogIsOpen = useDialogIsOpen(menuDialogId, dialogManager?.id);
213215

214216
const [modalContentAction, setModalContentActionAction] =
215217
useState<AttachmentSelectorAction>();
@@ -255,6 +257,7 @@ export const AttachmentSelector = ({
255257
<AttachmentSelectorMenuInitButtonIcon />
256258
</button>
257259
<DialogAnchor
260+
dialogManagerId={dialogManager?.id}
258261
id={menuDialogId}
259262
placement='top-start'
260263
referenceElement={menuButtonRef.current}

src/components/Modal/GlobalModal.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { FocusScope } from '@react-aria/focus';
66

77
import { CloseIconRound } from './icons';
88

9-
import { useTranslationContext } from '../../context';
9+
import { modalDialogManagerId, useTranslationContext } from '../../context';
1010
import {
1111
DialogPortalEntry,
1212
modalDialogId,
@@ -72,7 +72,7 @@ export const GlobalModal = ({
7272
if (!open || !isOpen) return null;
7373

7474
return (
75-
<DialogPortalEntry dialogId={modalDialogId}>
75+
<DialogPortalEntry dialogId={modalDialogId} dialogManagerId={modalDialogManagerId}>
7676
<div
7777
className={clsx(
7878
'str-chat str-chat__modal str-chat-react__modal str-chat__modal--open',

src/components/Reactions/ReactionSelectorWithButton.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { ElementRef } from 'react';
22
import React, { useRef } from 'react';
33
import { ReactionSelector as DefaultReactionSelector } from './ReactionSelector';
4-
import { DialogAnchor, useDialog, useDialogIsOpen } from '../Dialog';
4+
import { DialogAnchor, useDialogIsOpen, useDialogOnNearestManager } from '../Dialog';
55
import {
66
useComponentContext,
77
useMessageContext,
@@ -29,11 +29,13 @@ export const ReactionSelectorWithButton = ({
2929
const buttonRef = useRef<ElementRef<'button'>>(null);
3030
const dialogIdNamespace = threadList ? '-thread-' : '';
3131
const dialogId = `reaction-selector${dialogIdNamespace}--${message.id}`;
32-
const dialog = useDialog({ id: dialogId });
33-
const dialogIsOpen = useDialogIsOpen(dialogId);
32+
const { dialog, dialogManager } = useDialogOnNearestManager({ id: dialogId });
33+
const dialogIsOpen = useDialogIsOpen(dialogId, dialogManager?.id);
34+
3435
return (
3536
<>
3637
<DialogAnchor
38+
dialogManagerId={dialogManager?.id}
3739
id={dialogId}
3840
placement={isMyMessage() ? 'top-end' : 'top-start'}
3941
referenceElement={buttonRef.current}

src/context/DialogManagerContext.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,3 +188,6 @@ export const ModalDialogManagerProvider = ({ children }: PropsWithChildrenOnly)
188188

189189
export const useModalDialogManager = () =>
190190
useMemo(() => getDialogManager(modalDialogManagerId), []);
191+
192+
export const useNearestDialogManagerContext = () =>
193+
useContext(DialogManagerProviderContext);

0 commit comments

Comments
 (0)