Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/components/Dialog/ButtonWithSubmenu.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -24,8 +24,8 @@ export const ButtonWithSubmenu = ({
const keepSubmenuOpen = useRef(false);
const dialogCloseTimeout = useRef<NodeJS.Timeout | null>(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<HTMLDivElement>({
open: dialogIsOpen,
placement,
Expand Down
8 changes: 5 additions & 3 deletions src/components/Dialog/DialogAnchor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export function useDialogAnchor<T extends HTMLElement>({

export type DialogAnchorProps = PropsWithChildren<Partial<DialogAnchorOptions>> & {
id: string;
dialogManagerId?: string;
focus?: boolean;
trapFocus?: boolean;
} & ComponentProps<'div'>;
Expand All @@ -68,6 +69,7 @@ export const DialogAnchor = ({
allowFlip = true,
children,
className,
dialogManagerId,
focus = true,
id,
placement = 'auto',
Expand All @@ -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<HTMLDivElement>({
allowFlip,
open,
Expand Down Expand Up @@ -105,7 +107,7 @@ export const DialogAnchor = ({
}

return (
<DialogPortalEntry dialogId={id}>
<DialogPortalEntry dialogId={id} dialogManagerId={dialogManagerId}>
<FocusScope autoFocus={focus} contain={trapFocus} restoreFocus>
<div
{...restDivProps}
Expand Down
32 changes: 25 additions & 7 deletions src/components/Dialog/DialogPortal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,36 @@ import type { PropsWithChildren } from 'react';
import React, { useCallback } from 'react';
import { useDialogIsOpen, useOpenedDialogCount } from './hooks';
import { Portal } from '../Portal/Portal';
import { useDialogManager } from '../../context';
import { useDialogManager, useNearestDialogManagerContext } from '../../context';

export const DialogPortalDestination = () => {
const { dialogManager } = useDialogManager();
const openedDialogCount = useOpenedDialogCount();
const { dialogManager } = useNearestDialogManagerContext() ?? {};
const openedDialogCount = useOpenedDialogCount({ dialogManagerId: dialogManager?.id });
// const [destinationRoot, setDestinationRoot] = useState<HTMLDivElement | null>(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 (
<div
className='str-chat__dialog-overlay'
data-str-chat__portal-id={dialogManager.id}
data-str-chat__portal-id={dialogManager?.id}
data-testid='str-chat__dialog-overlay'
onClick={() => dialogManager.closeAll()}
onClick={() => dialogManager?.closeAll()}
// ref={setDestinationRoot}
style={
{
'--str-chat__dialog-overlay-height': openedDialogCount > 0 ? '100%' : '0',
Expand All @@ -27,14 +43,16 @@ export const DialogPortalDestination = () => {

type DialogPortalEntryProps = {
dialogId: string;
dialogManagerId?: string;
};

export const DialogPortalEntry = ({
children,
dialogId,
dialogManagerId,
}: PropsWithChildren<DialogPortalEntryProps>) => {
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}"]`),
Expand Down
16 changes: 15 additions & 1 deletion src/components/Dialog/hooks/useDialog.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -25,6 +29,16 @@ export const useDialog = ({ dialogManagerId, id }: UseDialogParams) => {
return dialogManager.getOrCreate({ id });
};

export const useDialogOnNearestManager = ({ id }: Pick<UseDialogParams, 'id'>) => {
const { dialogManager } = useNearestDialogManagerContext() ?? {};
const dialog = useDialog({ dialogManagerId: dialogManager?.id, id });

return {
dialog,
dialogManager,
};
};

export const modalDialogId = 'modal-dialog' as const;

export const useModalDialog = () =>
Expand Down
7 changes: 4 additions & 3 deletions src/components/MessageActions/MessageActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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();

Expand All @@ -108,6 +108,7 @@ export const MessageActions = (props: MessageActionsProps) => {
toggleOpen={dialog?.toggle}
>
<DialogAnchor
dialogManagerId={dialogManager?.id}
id={dialogId}
placement={isMine ? 'top-end' : 'top-start'}
referenceElement={actionsBoxButtonRef.current}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import '@testing-library/jest-dom';
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react';
import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';

import { MessageActions } from '../MessageActions';
import { MessageActionsBox as MessageActionsBoxMock } from '../MessageActionsBox';
Expand Down Expand Up @@ -137,14 +137,17 @@ describe('<MessageActions /> 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 () => {
Expand Down
9 changes: 6 additions & 3 deletions src/components/MessageInput/AttachmentSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<AttachmentSelectorAction>();
Expand Down Expand Up @@ -255,6 +257,7 @@ export const AttachmentSelector = ({
<AttachmentSelectorMenuInitButtonIcon />
</button>
<DialogAnchor
dialogManagerId={dialogManager?.id}
id={menuDialogId}
placement='top-start'
referenceElement={menuButtonRef.current}
Expand Down
4 changes: 2 additions & 2 deletions src/components/Modal/GlobalModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { FocusScope } from '@react-aria/focus';

import { CloseIconRound } from './icons';

import { useTranslationContext } from '../../context';
import { modalDialogManagerId, useTranslationContext } from '../../context';
import {
DialogPortalEntry,
modalDialogId,
Expand Down Expand Up @@ -72,7 +72,7 @@ export const GlobalModal = ({
if (!open || !isOpen) return null;

return (
<DialogPortalEntry dialogId={modalDialogId}>
<DialogPortalEntry dialogId={modalDialogId} dialogManagerId={modalDialogManagerId}>
<div
className={clsx(
'str-chat str-chat__modal str-chat-react__modal str-chat__modal--open',
Expand Down
8 changes: 5 additions & 3 deletions src/components/Reactions/ReactionSelectorWithButton.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ElementRef } from 'react';
import React, { useRef } from 'react';
import { ReactionSelector as DefaultReactionSelector } from './ReactionSelector';
import { DialogAnchor, useDialog, useDialogIsOpen } from '../Dialog';
import { DialogAnchor, useDialogIsOpen, useDialogOnNearestManager } from '../Dialog';
import {
useComponentContext,
useMessageContext,
Expand Down Expand Up @@ -29,11 +29,13 @@ export const ReactionSelectorWithButton = ({
const buttonRef = useRef<ElementRef<'button'>>(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 (
<>
<DialogAnchor
dialogManagerId={dialogManager?.id}
id={dialogId}
placement={isMyMessage() ? 'top-end' : 'top-start'}
referenceElement={buttonRef.current}
Expand Down
3 changes: 3 additions & 0 deletions src/context/DialogManagerContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -188,3 +188,6 @@ export const ModalDialogManagerProvider = ({ children }: PropsWithChildrenOnly)

export const useModalDialogManager = () =>
useMemo(() => getDialogManager(modalDialogManagerId), []);

export const useNearestDialogManagerContext = () =>
useContext(DialogManagerProviderContext);
16 changes: 12 additions & 4 deletions src/experimental/MessageActions/MessageActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -78,6 +85,7 @@ export const MessageActions = ({
</button>

<DialogAnchor
dialogManagerId={dialogManager?.id}
id={dropdownDialogId}
placement={isMyMessage() ? 'top-end' : 'top-start'}
referenceElement={actionsBoxButtonElement}
Expand Down