Improve test coverage and refactor storybook arch (#723)
* Improve test coverage and refactor storybook arch * Fix coverage * Fix tests * Fix lint * Fix lint
This commit is contained in:
@ -30,7 +30,7 @@ const StyledCommentBody = styled.div`
|
||||
text-align: left;
|
||||
`;
|
||||
|
||||
export function CommentThreadItem({ comment, actionBar }: OwnProps) {
|
||||
export function Comment({ comment, actionBar }: OwnProps) {
|
||||
return (
|
||||
<StyledContainer>
|
||||
<CommentHeader comment={comment} actionBar={actionBar} />
|
||||
@ -0,0 +1,25 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
|
||||
|
||||
import { Comment } from '../Comment';
|
||||
|
||||
import { mockComment, mockCommentWithLongValues } from './mock-comment';
|
||||
|
||||
const meta: Meta<typeof Comment> = {
|
||||
title: 'Modules/Activity/Comment/Comment',
|
||||
component: Comment,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Comment>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: getRenderWrapperForComponent(<Comment comment={mockComment} />),
|
||||
};
|
||||
|
||||
export const WithLongValues: Story = {
|
||||
render: getRenderWrapperForComponent(
|
||||
<Comment comment={mockCommentWithLongValues} />,
|
||||
),
|
||||
};
|
||||
@ -1,92 +1,34 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { CommentThreadActionBar } from '@/activities/right-drawer/components/CommentThreadActionBar';
|
||||
import { CommentForDrawer } from '@/activities/types/CommentForDrawer';
|
||||
import { mockedUsersData } from '~/testing/mock-data/users';
|
||||
import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
|
||||
|
||||
import { CommentHeader } from '../CommentHeader';
|
||||
|
||||
import { mockComment, mockCommentWithLongValues } from './mock-comment';
|
||||
|
||||
const meta: Meta<typeof CommentHeader> = {
|
||||
title: 'Modules/Comments/CommentHeader',
|
||||
title: 'Modules/Activity/Comment/CommentHeader',
|
||||
component: CommentHeader,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof CommentHeader>;
|
||||
|
||||
const mockUser = mockedUsersData[0];
|
||||
|
||||
const mockComment: Pick<CommentForDrawer, 'id' | 'author' | 'createdAt'> = {
|
||||
id: 'fake_comment_1_uuid',
|
||||
author: {
|
||||
id: 'fake_comment_1_author_uuid',
|
||||
displayName: mockUser.displayName ?? '',
|
||||
firstName: mockUser.firstName ?? '',
|
||||
lastName: mockUser.lastName ?? '',
|
||||
avatarUrl: mockUser.avatarUrl,
|
||||
},
|
||||
createdAt: DateTime.now().minus({ hours: 2 }).toISO() ?? '',
|
||||
};
|
||||
|
||||
const mockCommentWithLongName: Pick<
|
||||
CommentForDrawer,
|
||||
'id' | 'author' | 'createdAt'
|
||||
> = {
|
||||
id: 'fake_comment_2_uuid',
|
||||
author: {
|
||||
id: 'fake_comment_2_author_uuid',
|
||||
displayName: mockUser.displayName + ' with a very long suffix' ?? '',
|
||||
firstName: mockUser.firstName ?? '',
|
||||
lastName: mockUser.lastName ?? '',
|
||||
avatarUrl: mockUser.avatarUrl,
|
||||
},
|
||||
createdAt: DateTime.now().minus({ hours: 2 }).toISO() ?? '',
|
||||
};
|
||||
|
||||
export const Default: Story = {
|
||||
render: getRenderWrapperForComponent(
|
||||
<CommentHeader
|
||||
comment={{
|
||||
...mockComment,
|
||||
createdAt: DateTime.now().minus({ hours: 2 }).toISO() ?? '',
|
||||
}}
|
||||
/>,
|
||||
),
|
||||
render: getRenderWrapperForComponent(<CommentHeader comment={mockComment} />),
|
||||
};
|
||||
|
||||
export const FewDaysAgo: Story = {
|
||||
render: getRenderWrapperForComponent(
|
||||
<CommentHeader
|
||||
comment={{
|
||||
...mockComment,
|
||||
createdAt: DateTime.now().minus({ days: 2 }).toISO() ?? '',
|
||||
}}
|
||||
/>,
|
||||
),
|
||||
render: getRenderWrapperForComponent(<CommentHeader comment={mockComment} />),
|
||||
};
|
||||
|
||||
export const FewMonthsAgo: Story = {
|
||||
render: getRenderWrapperForComponent(
|
||||
<CommentHeader
|
||||
comment={{
|
||||
...mockComment,
|
||||
createdAt: DateTime.now().minus({ months: 2 }).toISO() ?? '',
|
||||
}}
|
||||
/>,
|
||||
),
|
||||
render: getRenderWrapperForComponent(<CommentHeader comment={mockComment} />),
|
||||
};
|
||||
|
||||
export const FewYearsAgo: Story = {
|
||||
render: getRenderWrapperForComponent(
|
||||
<CommentHeader
|
||||
comment={{
|
||||
...mockComment,
|
||||
createdAt: DateTime.now().minus({ years: 2 }).toISO() ?? '',
|
||||
}}
|
||||
/>,
|
||||
),
|
||||
render: getRenderWrapperForComponent(<CommentHeader comment={mockComment} />),
|
||||
};
|
||||
|
||||
export const WithoutAvatar: Story = {
|
||||
@ -98,7 +40,6 @@ export const WithoutAvatar: Story = {
|
||||
...mockComment.author,
|
||||
avatarUrl: '',
|
||||
},
|
||||
createdAt: DateTime.now().minus({ hours: 2 }).toISO() ?? '',
|
||||
}}
|
||||
/>,
|
||||
),
|
||||
@ -108,12 +49,11 @@ export const WithLongUserName: Story = {
|
||||
render: getRenderWrapperForComponent(
|
||||
<CommentHeader
|
||||
comment={{
|
||||
...mockCommentWithLongName,
|
||||
...mockCommentWithLongValues,
|
||||
author: {
|
||||
...mockCommentWithLongName.author,
|
||||
...mockCommentWithLongValues.author,
|
||||
avatarUrl: '',
|
||||
},
|
||||
createdAt: DateTime.now().minus({ hours: 2 }).toISO() ?? '',
|
||||
}}
|
||||
/>,
|
||||
),
|
||||
@ -122,10 +62,7 @@ export const WithLongUserName: Story = {
|
||||
export const WithActionBar: Story = {
|
||||
render: getRenderWrapperForComponent(
|
||||
<CommentHeader
|
||||
comment={{
|
||||
...mockComment,
|
||||
createdAt: DateTime.now().minus({ days: 2 }).toISO() ?? '',
|
||||
}}
|
||||
comment={mockComment}
|
||||
actionBar={<CommentThreadActionBar commentThreadId="test-id" />}
|
||||
/>,
|
||||
),
|
||||
|
||||
@ -0,0 +1,40 @@
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { CommentForDrawer } from '@/activities/types/CommentForDrawer';
|
||||
import { mockedUsersData } from '~/testing/mock-data/users';
|
||||
|
||||
const mockUser = mockedUsersData[0];
|
||||
|
||||
export const mockComment: Pick<
|
||||
CommentForDrawer,
|
||||
'id' | 'author' | 'createdAt' | 'body' | 'updatedAt'
|
||||
> = {
|
||||
id: 'fake_comment_1_uuid',
|
||||
body: 'Hello, this is a comment.',
|
||||
author: {
|
||||
id: 'fake_comment_1_author_uuid',
|
||||
displayName: mockUser.displayName ?? '',
|
||||
firstName: mockUser.firstName ?? '',
|
||||
lastName: mockUser.lastName ?? '',
|
||||
avatarUrl: mockUser.avatarUrl,
|
||||
},
|
||||
createdAt: DateTime.fromFormat('2021-03-12', 'yyyy-MM-dd').toISO() ?? '',
|
||||
updatedAt: DateTime.fromFormat('2021-03-13', 'yyyy-MM-dd').toISO() ?? '',
|
||||
};
|
||||
|
||||
export const mockCommentWithLongValues: Pick<
|
||||
CommentForDrawer,
|
||||
'id' | 'author' | 'createdAt' | 'body' | 'updatedAt'
|
||||
> = {
|
||||
id: 'fake_comment_2_uuid',
|
||||
body: 'Hello, this is a comment. Hello, this is a comment. Hello, this is a comment. Hello, this is a comment. Hello, this is a comment. Hello, this is a comment.',
|
||||
author: {
|
||||
id: 'fake_comment_2_author_uuid',
|
||||
displayName: mockUser.displayName + ' with a very long suffix' ?? '',
|
||||
firstName: mockUser.firstName ?? '',
|
||||
lastName: mockUser.lastName ?? '',
|
||||
avatarUrl: mockUser.avatarUrl,
|
||||
},
|
||||
createdAt: DateTime.fromFormat('2021-03-12', 'yyyy-MM-dd').toISO() ?? '',
|
||||
updatedAt: DateTime.fromFormat('2021-03-13', 'yyyy-MM-dd').toISO() ?? '',
|
||||
};
|
||||
@ -8,7 +8,7 @@ import { AutosizeTextInput } from '@/ui/input/components/AutosizeTextInput';
|
||||
import { CommentThread, useCreateCommentMutation } from '~/generated/graphql';
|
||||
import { isNonEmptyString } from '~/utils/isNonEmptyString';
|
||||
|
||||
import { CommentThreadItem } from '../comment/CommentThreadItem';
|
||||
import { Comment } from '../comment/Comment';
|
||||
import { GET_COMMENT_THREAD } from '../queries';
|
||||
import { CommentForDrawer } from '../types/CommentForDrawer';
|
||||
|
||||
@ -80,7 +80,7 @@ export function CommentThreadComments({ commentThread }: OwnProps) {
|
||||
<StyledThreadItemListContainer>
|
||||
<StyledThreadCommentTitle>Comments</StyledThreadCommentTitle>
|
||||
{commentThread?.comments?.map((comment, index) => (
|
||||
<CommentThreadItem key={comment.id} comment={comment} />
|
||||
<Comment key={comment.id} comment={comment} />
|
||||
))}
|
||||
</StyledThreadItemListContainer>
|
||||
</>
|
||||
|
||||
@ -152,8 +152,11 @@ export function CommentThreadRelationPicker({ commentThread }: OwnProps) {
|
||||
placement: 'bottom-start',
|
||||
});
|
||||
|
||||
useListenClickOutsideArrayOfRef([refs.floating, refs.domReference], () => {
|
||||
exitEditMode();
|
||||
useListenClickOutsideArrayOfRef({
|
||||
refs: [refs.floating, refs.domReference],
|
||||
callback: () => {
|
||||
exitEditMode();
|
||||
},
|
||||
});
|
||||
|
||||
const selectedEntities = flatMapAndSortEntityForSelectArrayOfArrayByName([
|
||||
|
||||
@ -7,7 +7,7 @@ import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
|
||||
|
||||
const meta: Meta<typeof EntityBoard> = {
|
||||
title: 'UI/Board/Board',
|
||||
title: 'Modules/Companies/Board',
|
||||
component: EntityBoard,
|
||||
decorators: [BoardDecorator],
|
||||
};
|
||||
|
||||
@ -6,7 +6,7 @@ import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
|
||||
|
||||
const meta: Meta<typeof CompanyBoardCard> = {
|
||||
title: 'UI/Board/CompanyBoardCard',
|
||||
title: 'Modules/Companies/CompanyBoardCard',
|
||||
component: CompanyBoardCard,
|
||||
decorators: [BoardCardDecorator],
|
||||
};
|
||||
|
||||
@ -7,7 +7,7 @@ import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
|
||||
import { PersonChip } from '../PersonChip';
|
||||
|
||||
const meta: Meta<typeof PersonChip> = {
|
||||
title: 'Modules/Companies/PersonChip',
|
||||
title: 'Modules/People/PersonChip',
|
||||
component: PersonChip,
|
||||
};
|
||||
|
||||
|
||||
@ -72,9 +72,13 @@ export function NameFields({
|
||||
}, 500);
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
currentUser?.firstName !== firstName ||
|
||||
currentUser?.lastName !== lastName
|
||||
currentUser.firstName !== firstName ||
|
||||
currentUser.lastName !== lastName
|
||||
) {
|
||||
debouncedUpdate();
|
||||
}
|
||||
|
||||
@ -43,8 +43,11 @@ export function BoardCardEditableFieldEditMode({
|
||||
}: OwnProps) {
|
||||
const wrapperRef = useRef(null);
|
||||
|
||||
useListenClickOutsideArrayOfRef([wrapperRef], () => {
|
||||
onExit();
|
||||
useListenClickOutsideArrayOfRef({
|
||||
refs: [wrapperRef],
|
||||
callback: () => {
|
||||
onExit();
|
||||
},
|
||||
});
|
||||
|
||||
useScopedHotkeys(
|
||||
|
||||
@ -40,8 +40,11 @@ export function EditColumnTitleInput({
|
||||
}) {
|
||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
useListenClickOutsideArrayOfRef([inputRef], () => {
|
||||
onFocusLeave();
|
||||
useListenClickOutsideArrayOfRef({
|
||||
refs: [inputRef],
|
||||
callback: () => {
|
||||
onFocusLeave();
|
||||
},
|
||||
});
|
||||
const setHotkeyScope = useSetHotkeyScope();
|
||||
setHotkeyScope(ColumnHotkeyScope.EditColumnName, { goto: false });
|
||||
|
||||
@ -83,14 +83,20 @@ const StyledIconButton = styled.button<Pick<ButtonProps, 'variant' | 'size'>>`
|
||||
}
|
||||
}};
|
||||
justify-content: center;
|
||||
outline: none;
|
||||
padding: 0;
|
||||
transition: background 0.1s ease;
|
||||
user-select: none;
|
||||
&:hover {
|
||||
background: ${({ theme, disabled }) => {
|
||||
return disabled ? 'auto' : theme.background.transparent.light;
|
||||
}};
|
||||
}
|
||||
user-select: none;
|
||||
&:active {
|
||||
background: ${({ theme, disabled }) => {
|
||||
return disabled ? 'auto' : theme.background.transparent.medium;
|
||||
}};
|
||||
}
|
||||
width: ${({ size }) => {
|
||||
switch (size) {
|
||||
case 'large':
|
||||
@ -102,11 +108,6 @@ const StyledIconButton = styled.button<Pick<ButtonProps, 'variant' | 'size'>>`
|
||||
return '20px';
|
||||
}
|
||||
}};
|
||||
&:active {
|
||||
background: ${({ theme, disabled }) => {
|
||||
return disabled ? 'auto' : theme.background.transparent.medium;
|
||||
}};
|
||||
}
|
||||
`;
|
||||
|
||||
export function IconButton({
|
||||
|
||||
@ -14,15 +14,16 @@ const StyledIconButton = styled.button`
|
||||
|
||||
justify-content: center;
|
||||
|
||||
outline: none;
|
||||
padding: 0;
|
||||
transition: color 0.1s ease-in-out, background 0.1s ease-in-out;
|
||||
width: 20px;
|
||||
|
||||
&:disabled {
|
||||
background: ${({ theme }) => theme.background.quaternary};
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
cursor: default;
|
||||
}
|
||||
width: 20px;
|
||||
`;
|
||||
|
||||
export function RoundedIconButton({
|
||||
|
||||
@ -56,7 +56,7 @@ const StyledButtonContainer = styled.div`
|
||||
`;
|
||||
|
||||
const meta: Meta<typeof Button> = {
|
||||
title: 'UI/Buttons/Button',
|
||||
title: 'UI/Button/Button',
|
||||
component: Button,
|
||||
decorators: [withKnobs],
|
||||
};
|
||||
|
||||
@ -53,7 +53,7 @@ const StyledIconButtonContainer = styled.div`
|
||||
`;
|
||||
|
||||
const meta: Meta<typeof IconButton> = {
|
||||
title: 'UI/Buttons/IconButton',
|
||||
title: 'UI/Button/IconButton',
|
||||
component: IconButton,
|
||||
decorators: [withKnobs],
|
||||
};
|
||||
|
||||
@ -8,7 +8,7 @@ import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
|
||||
import { MainButton } from '../MainButton';
|
||||
|
||||
const meta: Meta<typeof MainButton> = {
|
||||
title: 'UI/Buttons/MainButton',
|
||||
title: 'UI/Button/MainButton',
|
||||
component: MainButton,
|
||||
};
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@ import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
|
||||
import { RoundedIconButton } from '../RoundedIconButton';
|
||||
|
||||
const meta: Meta<typeof RoundedIconButton> = {
|
||||
title: 'UI/Buttons/RoundedIconButton',
|
||||
title: 'UI/Button/RoundedIconButton',
|
||||
component: RoundedIconButton,
|
||||
};
|
||||
|
||||
|
||||
@ -15,7 +15,7 @@ import { DropdownMenuSelectableItem } from '../DropdownMenuSelectableItem';
|
||||
import { DropdownMenuSeparator } from '../DropdownMenuSeparator';
|
||||
|
||||
const meta: Meta<typeof DropdownMenu> = {
|
||||
title: 'UI/Menu/DropdownMenu',
|
||||
title: 'UI/Dropdown/DropdownMenu',
|
||||
component: DropdownMenu,
|
||||
};
|
||||
|
||||
|
||||
@ -12,11 +12,14 @@ export function useRegisterCloseFieldHandlers(
|
||||
) {
|
||||
const { closeEditableField, isFieldInEditMode } = useEditableField();
|
||||
|
||||
useListenClickOutsideArrayOfRef([wrapperRef], () => {
|
||||
if (isFieldInEditMode) {
|
||||
onSubmit?.();
|
||||
closeEditableField();
|
||||
}
|
||||
useListenClickOutsideArrayOfRef({
|
||||
refs: [wrapperRef],
|
||||
callback: () => {
|
||||
if (isFieldInEditMode) {
|
||||
onSubmit?.();
|
||||
closeEditableField();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
useScopedHotkeys(
|
||||
|
||||
@ -3,7 +3,7 @@ import styled from '@emotion/styled';
|
||||
import { Key } from 'ts-key-enum';
|
||||
|
||||
import { DropdownMenu } from '@/ui/dropdown/components/DropdownMenu';
|
||||
import { useOutsideAlerter } from '@/ui/hooks/useOutsideAlerter';
|
||||
import { useListenClickOutsideArrayOfRef } from '@/ui/hooks/useListenClickOutsideArrayOfRef';
|
||||
import { useScopedHotkeys } from '@/ui/hotkey/hooks/useScopedHotkeys';
|
||||
import { IconChevronDown } from '@/ui/icon/index';
|
||||
|
||||
@ -105,7 +105,10 @@ function DropdownButton({
|
||||
};
|
||||
|
||||
const dropdownRef = useRef(null);
|
||||
useOutsideAlerter({ ref: dropdownRef, callback: onOutsideClick });
|
||||
useListenClickOutsideArrayOfRef({
|
||||
refs: [dropdownRef],
|
||||
callback: onOutsideClick,
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledDropdownButtonContainer>
|
||||
|
||||
@ -0,0 +1,39 @@
|
||||
import { useRef } from 'react';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
|
||||
import { useListenClickOutsideArrayOfRef } from '../useListenClickOutsideArrayOfRef';
|
||||
|
||||
const onOutsideClick = jest.fn();
|
||||
|
||||
function TestComponentDomMode() {
|
||||
const buttonRef = useRef(null);
|
||||
const buttonRef2 = useRef(null);
|
||||
useListenClickOutsideArrayOfRef({
|
||||
refs: [buttonRef, buttonRef2],
|
||||
callback: onOutsideClick,
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span>Outside</span>
|
||||
<button ref={buttonRef}>Inside</button>
|
||||
<button ref={buttonRef2}>Inside 2</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
test('useListenClickOutsideArrayOfRef hook works in dom mode', async () => {
|
||||
const { getByText } = render(<TestComponentDomMode />);
|
||||
const inside = getByText('Inside');
|
||||
const inside2 = getByText('Inside 2');
|
||||
const outside = getByText('Outside');
|
||||
|
||||
fireEvent.click(inside);
|
||||
expect(onOutsideClick).toHaveBeenCalledTimes(0);
|
||||
|
||||
fireEvent.click(inside2);
|
||||
expect(onOutsideClick).toHaveBeenCalledTimes(0);
|
||||
|
||||
fireEvent.click(outside);
|
||||
expect(onOutsideClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@ -1,31 +0,0 @@
|
||||
import { useRef } from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
|
||||
import { useOutsideAlerter } from '../useOutsideAlerter';
|
||||
const onOutsideClick = jest.fn();
|
||||
|
||||
function TestComponent() {
|
||||
const buttonRef = useRef(null);
|
||||
useOutsideAlerter({ ref: buttonRef, callback: onOutsideClick });
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span>Outside</span>
|
||||
<button ref={buttonRef}>Inside</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
test('useOutsideAlerter hook works properly', async () => {
|
||||
const { getByText } = render(<TestComponent />);
|
||||
const inside = getByText('Inside');
|
||||
const outside = getByText('Outside');
|
||||
await act(() => Promise.resolve());
|
||||
|
||||
fireEvent.mouseDown(inside);
|
||||
expect(onOutsideClick).toHaveBeenCalledTimes(0);
|
||||
|
||||
fireEvent.mouseDown(outside);
|
||||
expect(onOutsideClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@ -2,34 +2,75 @@ import React, { useEffect } from 'react';
|
||||
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export function useListenClickOutsideArrayOfRef<T extends Element>(
|
||||
arrayOfRef: Array<React.RefObject<T>>,
|
||||
outsideClickCallback: (event?: MouseEvent | TouchEvent) => void,
|
||||
) {
|
||||
export enum ClickOutsideMode {
|
||||
absolute = 'absolute',
|
||||
dom = 'dom',
|
||||
}
|
||||
|
||||
export function useListenClickOutsideArrayOfRef<T extends Element>({
|
||||
refs,
|
||||
callback,
|
||||
mode = ClickOutsideMode.dom,
|
||||
}: {
|
||||
refs: Array<React.RefObject<T>>;
|
||||
callback: (event?: MouseEvent | TouchEvent) => void;
|
||||
mode?: ClickOutsideMode;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event: MouseEvent | TouchEvent) {
|
||||
const clickedOnAtLeastOneRef = arrayOfRef
|
||||
.filter((ref) => !!ref.current)
|
||||
.some((ref) => ref.current?.contains(event.target as Node));
|
||||
if (mode === ClickOutsideMode.dom) {
|
||||
const clickedOnAtLeastOneRef = refs
|
||||
.filter((ref) => !!ref.current)
|
||||
.some((ref) => ref.current?.contains(event.target as Node));
|
||||
|
||||
if (!clickedOnAtLeastOneRef) {
|
||||
outsideClickCallback(event);
|
||||
if (!clickedOnAtLeastOneRef) {
|
||||
callback(event);
|
||||
}
|
||||
}
|
||||
|
||||
if (mode === ClickOutsideMode.absolute) {
|
||||
const clickedOnAtLeastOneRef = refs
|
||||
.filter((ref) => !!ref.current)
|
||||
.some((ref) => {
|
||||
if (!ref.current) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { x, y, width, height } = ref.current.getBoundingClientRect();
|
||||
|
||||
const clientX =
|
||||
'clientX' in event ? event.clientX : event.touches[0].clientX;
|
||||
const clientY =
|
||||
'clientY' in event ? event.clientY : event.touches[0].clientY;
|
||||
|
||||
console.log(clientX, clientY, x, y, width, height);
|
||||
|
||||
if (
|
||||
clientX < x ||
|
||||
clientX > x + width ||
|
||||
clientY < y ||
|
||||
clientY > y + height
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if (!clickedOnAtLeastOneRef) {
|
||||
callback(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const hasAtLeastOneRefDefined = arrayOfRef.some((ref) =>
|
||||
isDefined(ref.current),
|
||||
);
|
||||
const hasAtLeastOneRefDefined = refs.some((ref) => isDefined(ref.current));
|
||||
|
||||
if (hasAtLeastOneRefDefined) {
|
||||
document.addEventListener('mouseup', handleClickOutside);
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
document.addEventListener('touchend', handleClickOutside);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mouseup', handleClickOutside);
|
||||
document.removeEventListener('click', handleClickOutside);
|
||||
document.removeEventListener('touchend', handleClickOutside);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [arrayOfRef, outsideClickCallback]);
|
||||
}, [refs, callback, mode]);
|
||||
}
|
||||
|
||||
@ -1,52 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export enum OutsideClickAlerterMode {
|
||||
absolute = 'absolute',
|
||||
dom = 'dom',
|
||||
}
|
||||
|
||||
type OwnProps = {
|
||||
ref: React.RefObject<HTMLInputElement>;
|
||||
callback: () => void;
|
||||
mode?: OutsideClickAlerterMode;
|
||||
};
|
||||
|
||||
export function useOutsideAlerter({
|
||||
ref,
|
||||
mode = OutsideClickAlerterMode.dom,
|
||||
callback,
|
||||
}: OwnProps) {
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
const target = event.target as HTMLButtonElement;
|
||||
|
||||
if (!ref.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
mode === OutsideClickAlerterMode.dom &&
|
||||
!ref.current.contains(target)
|
||||
) {
|
||||
callback();
|
||||
}
|
||||
|
||||
if (mode === OutsideClickAlerterMode.absolute) {
|
||||
const { x, y, width, height } = ref.current.getBoundingClientRect();
|
||||
const { clientX, clientY } = event;
|
||||
if (
|
||||
clientX < x ||
|
||||
clientX > x + width ||
|
||||
clientY < y ||
|
||||
clientY > y + height
|
||||
) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, [ref, callback, mode]);
|
||||
}
|
||||
@ -46,8 +46,11 @@ export function SingleEntitySelect<
|
||||
|
||||
const showCreateButton = isDefined(onCreate) && searchFilter !== '';
|
||||
|
||||
useListenClickOutsideArrayOfRef([containerRef], () => {
|
||||
onCancel?.();
|
||||
useListenClickOutsideArrayOfRef({
|
||||
refs: [containerRef],
|
||||
callback: () => {
|
||||
onCancel?.();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@ -5,9 +5,9 @@ import { motion } from 'framer-motion';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import {
|
||||
OutsideClickAlerterMode,
|
||||
useOutsideAlerter,
|
||||
} from '@/ui/hooks/useOutsideAlerter';
|
||||
ClickOutsideMode,
|
||||
useListenClickOutsideArrayOfRef,
|
||||
} from '@/ui/hooks/useListenClickOutsideArrayOfRef';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
import { isRightDrawerOpenState } from '../states/isRightDrawerOpenState';
|
||||
@ -41,10 +41,10 @@ export function RightDrawer() {
|
||||
const [rightDrawerPage] = useRecoilState(rightDrawerPageState);
|
||||
|
||||
const rightDrawerRef = useRef(null);
|
||||
useOutsideAlerter({
|
||||
ref: rightDrawerRef,
|
||||
useListenClickOutsideArrayOfRef({
|
||||
refs: [rightDrawerRef],
|
||||
callback: () => setIsRightDrawerOpen(false),
|
||||
mode: OutsideClickAlerterMode.absolute,
|
||||
mode: ClickOutsideMode.absolute,
|
||||
});
|
||||
const theme = useTheme();
|
||||
if (!isDefined(rightDrawerPage)) {
|
||||
|
||||
@ -90,8 +90,11 @@ export function EntityTable<SortField>({
|
||||
|
||||
const leaveTableFocus = useLeaveTableFocus();
|
||||
|
||||
useListenClickOutsideArrayOfRef([tableBodyRef], () => {
|
||||
leaveTableFocus();
|
||||
useListenClickOutsideArrayOfRef({
|
||||
refs: [tableBodyRef],
|
||||
callback: () => {
|
||||
leaveTableFocus();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@ -14,11 +14,14 @@ export function useRegisterCloseCellHandlers(
|
||||
) {
|
||||
const { closeEditableCell } = useEditableCell();
|
||||
const { isCurrentCellInEditMode } = useCurrentCellEditMode();
|
||||
useListenClickOutsideArrayOfRef([wrapperRef], () => {
|
||||
if (isCurrentCellInEditMode) {
|
||||
onSubmit?.();
|
||||
closeEditableCell();
|
||||
}
|
||||
useListenClickOutsideArrayOfRef({
|
||||
refs: [wrapperRef],
|
||||
callback: () => {
|
||||
if (isCurrentCellInEditMode) {
|
||||
onSubmit?.();
|
||||
closeEditableCell();
|
||||
}
|
||||
},
|
||||
});
|
||||
const { moveRight, moveLeft, moveDown } = useMoveSoftFocus();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user