Skip to content

Commit

Permalink
refactor: do not register event listeners by MessageActions component
Browse files Browse the repository at this point in the history
  • Loading branch information
MartinCupela committed Sep 6, 2024
1 parent 0a14548 commit 07eb261
Show file tree
Hide file tree
Showing 4 changed files with 42 additions and 140 deletions.
4 changes: 1 addition & 3 deletions src/components/Message/__tests__/MessageOptions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,7 @@ const defaultMessageProps = {
onReactionListClick: () => {},
threadList: false,
};
const defaultOptionsProps = {
messageWrapperRef: { current: document.createElement('div') },
};
const defaultOptionsProps = {};

function generateAliceMessage(messageOptions) {
return generateMessage({
Expand Down
1 change: 0 additions & 1 deletion src/components/Message/__tests__/MessageText.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ const onMentionsClickMock = jest.fn();
const defaultProps = {
initialMessage: false,
message: generateMessage(),
messageWrapperRef: { current: document.createElement('div') },
onReactionListClick: () => {},
threadList: false,
};
Expand Down
50 changes: 4 additions & 46 deletions src/components/MessageActions/MessageActions.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import clsx from 'clsx';
import React, { ElementRef, PropsWithChildren, useCallback, useEffect, useRef } from 'react';
import React, { ElementRef, PropsWithChildren, useCallback, useRef } from 'react';

import { MessageActionsBox } from './MessageActionsBox';

Expand Down Expand Up @@ -31,8 +31,6 @@ export type MessageActionsProps<
customWrapperClass?: string;
/* If true, renders the wrapper component as a `span`, not a `div` */
inline?: boolean;
/* React mutable ref that can be placed on the message root `div` of MessageActions component */
messageWrapperRef?: React.RefObject<HTMLDivElement>;
/* Function that returns whether the message was sent by the connected user */
mine?: () => boolean;
};
Expand All @@ -53,7 +51,6 @@ export const MessageActions = <
handlePin: propHandlePin,
inline,
message: propMessage,
messageWrapperRef,
mine,
} = props;

Expand Down Expand Up @@ -101,50 +98,12 @@ export const MessageActions = <
messageActions,
});

const messageDeletedAt = !!message?.deleted_at;

const hideOptions = useCallback(
(event: MouseEvent | KeyboardEvent) => {
if (event instanceof KeyboardEvent && event.key !== 'Escape') {
return;
}
dialog?.close();
},
[dialog],
);

useEffect(() => {
if (messageWrapperRef?.current) {
messageWrapperRef.current.addEventListener('mouseleave', hideOptions);
}
}, [hideOptions, messageWrapperRef]);

useEffect(() => {
if (messageDeletedAt) {
document.removeEventListener('click', hideOptions);
}
}, [hideOptions, messageDeletedAt]);

useEffect(() => {
if (!dialogIsOpen) return;

document.addEventListener('keyup', hideOptions);

return () => {
document.removeEventListener('keyup', hideOptions);
};
}, [dialog, dialogIsOpen, hideOptions]);

const actionsBoxButtonRef = useRef<ElementRef<'button'>>(null);

if (!renderMessageActions) return null;

return (
<MessageActionsWrapper
customWrapperClass={customWrapperClass}
inline={inline}
toggleOpen={dialog?.toggleSingle}
>
<MessageActionsWrapper customWrapperClass={customWrapperClass} inline={inline}>
<DialogAnchor
id={dialogId}
placement={isMine ? 'top-end' : 'top-start'}
Expand All @@ -169,6 +128,7 @@ export const MessageActions = <
aria-haspopup='true'
aria-label={t('aria/Open Message Actions Menu')}
className='str-chat__message-actions-box-button'
onClick={dialog?.toggleSingle}
ref={actionsBoxButtonRef}
>
<ActionsIcon className='str-chat__message-action-icon' />
Expand All @@ -180,11 +140,10 @@ export const MessageActions = <
export type MessageActionsWrapperProps = {
customWrapperClass?: string;
inline?: boolean;
toggleOpen?: () => void;
};

const MessageActionsWrapper = (props: PropsWithChildren<MessageActionsWrapperProps>) => {
const { children, customWrapperClass, inline, toggleOpen } = props;
const { children, customWrapperClass, inline } = props;

const defaultWrapperClass = clsx(
'str-chat__message-simple__actions__action',
Expand All @@ -195,7 +154,6 @@ const MessageActionsWrapper = (props: PropsWithChildren<MessageActionsWrapperPro
const wrapperProps = {
className: customWrapperClass || defaultWrapperClass,
'data-testid': 'message-actions',
onClick: toggleOpen,
};

if (inline) return <span {...wrapperProps}>{children}</span>;
Expand Down
127 changes: 37 additions & 90 deletions src/components/MessageActions/__tests__/MessageActions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,24 +66,30 @@ function renderMessageActions(customProps, renderer = render) {

const dialogOverlayTestId = 'str-chat__dialog-overlay';
const messageActionsTestId = 'message-actions';

const toggleOpenMessageActions = async () => {
await act(async () => {
await fireEvent.click(screen.getByRole('button'));
});
};
describe('<MessageActions /> component', () => {
afterEach(cleanup);
beforeEach(jest.clearAllMocks);

it('should render correctly', () => {
it('should render correctly when not open', () => {
const tree = renderMessageActions({}, testRenderer.create);
expect(tree.toJSON()).toMatchInlineSnapshot(`
Array [
<div
className="str-chat__message-simple__actions__action str-chat__message-simple__actions__action--options str-chat__message-actions-container"
data-testid="message-actions"
onClick={[Function]}
>
<button
aria-expanded={false}
aria-haspopup="true"
aria-label="Open Message Actions Menu"
className="str-chat__message-actions-box-button"
onClick={[Function]}
>
<svg
className="str-chat__message-action-icon"
Expand Down Expand Up @@ -123,50 +129,34 @@ describe('<MessageActions /> component', () => {
});

it('should open message actions box on click', async () => {
const { getByTestId } = renderMessageActions();
expect(MessageActionsBoxMock).toHaveBeenCalledWith(
expect.objectContaining({ open: false }),
{},
);
renderMessageActions();
expect(MessageActionsBoxMock).not.toHaveBeenCalled();
const dialogOverlay = screen.getByTestId(dialogOverlayTestId);
expect(dialogOverlay.children).toHaveLength(1);
await act(async () => {
await fireEvent.click(getByTestId(messageActionsTestId));
});
expect(dialogOverlay.children).toHaveLength(0);
await toggleOpenMessageActions();
expect(MessageActionsBoxMock).toHaveBeenLastCalledWith(
expect.objectContaining({ open: true }),
{},
);
expect(dialogOverlay.children).toHaveLength(1);
expect(dialogOverlay.children.length).toBeGreaterThan(0);
});

it('should close message actions box on icon click if already opened', async () => {
const { getByTestId } = renderMessageActions();
expect(MessageActionsBoxMock).toHaveBeenLastCalledWith(
expect.objectContaining({ open: false }),
{},
);
await act(async () => {
await fireEvent.click(getByTestId(messageActionsTestId));
});
renderMessageActions();
const dialogOverlay = screen.getByTestId(dialogOverlayTestId);
expect(MessageActionsBoxMock).not.toHaveBeenCalled();
await toggleOpenMessageActions();
expect(MessageActionsBoxMock).toHaveBeenLastCalledWith(
expect.objectContaining({ open: true }),
{},
);
await act(async () => {
await fireEvent.click(getByTestId(messageActionsTestId));
});
expect(MessageActionsBoxMock).toHaveBeenLastCalledWith(
expect.objectContaining({ open: false }),
{},
);
await toggleOpenMessageActions();
expect(dialogOverlay.children).toHaveLength(0);
});

it('should close message actions box when user clicks overlay if it is already opened', async () => {
const { getByRole } = renderMessageActions();
await act(async () => {
await fireEvent.click(getByRole('button'));
});
renderMessageActions();
await toggleOpenMessageActions();
expect(MessageActionsBoxMock).toHaveBeenLastCalledWith(
expect.objectContaining({ open: true }),
{},
Expand All @@ -175,53 +165,24 @@ describe('<MessageActions /> component', () => {
await act(async () => {
await fireEvent.click(dialogOverlay);
});
expect(MessageActionsBoxMock).toHaveBeenLastCalledWith(
expect.objectContaining({ open: false }),
{},
);
expect(MessageActionsBoxMock).toHaveBeenCalledTimes(1);
expect(dialogOverlay.children).toHaveLength(0);
});

it('should close message actions box when user presses Escape key', async () => {
const { getByRole } = renderMessageActions();
await act(async () => {
await fireEvent.click(getByRole('button'));
});
expect(MessageActionsBoxMock).toHaveBeenLastCalledWith(
expect.objectContaining({ open: true }),
{},
);
renderMessageActions();
const dialogOverlay = screen.getByTestId(dialogOverlayTestId);
await toggleOpenMessageActions();
await act(async () => {
await fireEvent.keyUp(document, { charCode: 27, code: 'Escape', key: 'Escape' });
});
expect(MessageActionsBoxMock).toHaveBeenLastCalledWith(
expect.objectContaining({ open: false }),
{},
);
});

it('should close actions box open on mouseleave if container ref provided', async () => {
const customProps = {
messageWrapperRef: { current: wrapperMock },
};
const { getByRole } = renderMessageActions(customProps);
await act(async () => {
await fireEvent.click(getByRole('button'));
});
expect(MessageActionsBoxMock).toHaveBeenLastCalledWith(
expect.objectContaining({ open: true }),
{},
);
await act(async () => {
await fireEvent.mouseLeave(customProps.messageWrapperRef.current);
});
expect(MessageActionsBoxMock).toHaveBeenLastCalledWith(
expect.objectContaining({ open: false }),
{},
);
expect(MessageActionsBoxMock).toHaveBeenCalledTimes(1);
expect(dialogOverlay.children).toHaveLength(0);
});

it('should render the message actions box correctly', () => {
it('should render the message actions box correctly', async () => {
renderMessageActions();
await toggleOpenMessageActions();
expect(MessageActionsBoxMock).toHaveBeenLastCalledWith(
expect.objectContaining({
getMessageActions: defaultProps.getMessageActions,
Expand All @@ -232,40 +193,26 @@ describe('<MessageActions /> component', () => {
handlePin: defaultProps.handlePin,
isUserMuted: expect.any(Function),
mine: false,
open: false,
open: true,
}),
{},
);
});

it('should not register click and keyup event listeners to close actions box until opened', async () => {
const { getByRole } = renderMessageActions();
renderMessageActions();
const addEventListener = jest.spyOn(document, 'addEventListener');
expect(document.addEventListener).not.toHaveBeenCalled();
await act(async () => {
await fireEvent.click(getByRole('button'));
});
await toggleOpenMessageActions();
expect(document.addEventListener).toHaveBeenCalledWith('keyup', expect.any(Function));
addEventListener.mockClear();
});

it('should not remove click and keyup event listeners when unmounted if actions box not opened', () => {
it('should remove keyup event listener when unmounted if actions box not opened', async () => {
const { unmount } = renderMessageActions();
const removeEventListener = jest.spyOn(document, 'removeEventListener');
expect(document.removeEventListener).not.toHaveBeenCalled();
unmount();
expect(document.removeEventListener).not.toHaveBeenCalledWith('click', expect.any(Function));
expect(document.removeEventListener).not.toHaveBeenCalledWith('keyup', expect.any(Function));
removeEventListener.mockClear();
});

it('should remove event listener when unmounted', async () => {
const { getByRole, unmount } = renderMessageActions();
const removeEventListener = jest.spyOn(document, 'removeEventListener');
await act(async () => {
await fireEvent.click(getByRole('button'));
});
expect(document.removeEventListener).not.toHaveBeenCalled();
await toggleOpenMessageActions();
unmount();
expect(document.removeEventListener).toHaveBeenCalledWith('keyup', expect.any(Function));
removeEventListener.mockClear();
Expand All @@ -283,13 +230,13 @@ describe('<MessageActions /> component', () => {
<div
className="custom-wrapper-class"
data-testid="message-actions"
onClick={[Function]}
>
<button
aria-expanded={false}
aria-haspopup="true"
aria-label="Open Message Actions Menu"
className="str-chat__message-actions-box-button"
onClick={[Function]}
>
<svg
className="str-chat__message-action-icon"
Expand Down Expand Up @@ -332,13 +279,13 @@ describe('<MessageActions /> component', () => {
<span
className="str-chat__message-simple__actions__action str-chat__message-simple__actions__action--options str-chat__message-actions-container"
data-testid="message-actions"
onClick={[Function]}
>
<button
aria-expanded={false}
aria-haspopup="true"
aria-label="Open Message Actions Menu"
className="str-chat__message-actions-box-button"
onClick={[Function]}
>
<svg
className="str-chat__message-action-icon"
Expand Down

0 comments on commit 07eb261

Please sign in to comment.