Lucas/t 352 i dont want another input cell to open when i click outside (#163)
* Added logic to handle global edit mode * Added recoil global edit mode state into generic editable components * Fix lint * Added tests
This commit is contained in:
1
front/.gitignore
vendored
1
front/.gitignore
vendored
@ -7,6 +7,7 @@
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
storybook-static
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { ReactElement, useRef } from 'react';
|
||||
import { useOutsideAlerter } from '../../hooks/useOutsideAlerter';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { ReactElement } from 'react';
|
||||
import { CellBaseContainer } from './CellBaseContainer';
|
||||
import { CellEditModeContainer } from './CellEditModeContainer';
|
||||
import { CellNormalModeContainer } from './CellNormalModeContainer';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { isSomeInputInEditModeState } from '../../modules/ui/tables/states/isSomeInputInEditModeState';
|
||||
import { EditableCellEditMode } from './EditableCellEditMode';
|
||||
|
||||
type OwnProps = {
|
||||
editModeContent: ReactElement;
|
||||
@ -25,58 +25,27 @@ export function EditableCell({
|
||||
onOutsideClick,
|
||||
onInsideClick,
|
||||
}: OwnProps) {
|
||||
const wrapperRef = useRef(null);
|
||||
const editableContainerRef = useRef(null);
|
||||
|
||||
useOutsideAlerter(wrapperRef, () => {
|
||||
onOutsideClick?.();
|
||||
});
|
||||
|
||||
useHotkeys(
|
||||
'esc',
|
||||
() => {
|
||||
if (isEditMode) {
|
||||
onOutsideClick?.();
|
||||
}
|
||||
},
|
||||
{
|
||||
preventDefault: true,
|
||||
enableOnContentEditable: true,
|
||||
enableOnFormTags: true,
|
||||
},
|
||||
[isEditMode, onOutsideClick],
|
||||
const [isSomeInputInEditMode, setIsSomeInputInEditMode] = useRecoilState(
|
||||
isSomeInputInEditModeState,
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
'enter',
|
||||
() => {
|
||||
if (isEditMode) {
|
||||
onOutsideClick?.();
|
||||
}
|
||||
},
|
||||
{
|
||||
preventDefault: true,
|
||||
enableOnContentEditable: true,
|
||||
enableOnFormTags: true,
|
||||
},
|
||||
[isEditMode, onOutsideClick],
|
||||
);
|
||||
function handleOnClick() {
|
||||
if (!isSomeInputInEditMode) {
|
||||
onInsideClick?.();
|
||||
setIsSomeInputInEditMode(true);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<CellBaseContainer
|
||||
ref={wrapperRef}
|
||||
onClick={() => {
|
||||
onInsideClick && onInsideClick();
|
||||
}}
|
||||
>
|
||||
<CellBaseContainer onClick={handleOnClick}>
|
||||
{isEditMode ? (
|
||||
<CellEditModeContainer
|
||||
ref={editableContainerRef}
|
||||
<EditableCellEditMode
|
||||
editModeContent={editModeContent}
|
||||
editModeHorizontalAlign={editModeHorizontalAlign}
|
||||
editModeVerticalPosition={editModeVerticalPosition}
|
||||
>
|
||||
{editModeContent}
|
||||
</CellEditModeContainer>
|
||||
isEditMode={isEditMode}
|
||||
onOutsideClick={onOutsideClick}
|
||||
/>
|
||||
) : (
|
||||
<CellNormalModeContainer>{nonEditModeContent}</CellNormalModeContainer>
|
||||
)}
|
||||
|
||||
86
front/src/components/editable-cell/EditableCellEditMode.tsx
Normal file
86
front/src/components/editable-cell/EditableCellEditMode.tsx
Normal file
@ -0,0 +1,86 @@
|
||||
import { ReactElement, useMemo, useRef } from 'react';
|
||||
import { CellEditModeContainer } from './CellEditModeContainer';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { isSomeInputInEditModeState } from '../../modules/ui/tables/states/isSomeInputInEditModeState';
|
||||
import { useListenClickOutsideArrayOfRef } from '../../modules/ui/common/hooks/useListenClickOutsideArrayOfRef';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { debounce } from '../../modules/utils/debounce';
|
||||
|
||||
type OwnProps = {
|
||||
editModeContent: ReactElement;
|
||||
editModeHorizontalAlign?: 'left' | 'right';
|
||||
editModeVerticalPosition?: 'over' | 'below';
|
||||
isEditMode?: boolean;
|
||||
onOutsideClick?: () => void;
|
||||
onInsideClick?: () => void;
|
||||
};
|
||||
|
||||
export function EditableCellEditMode({
|
||||
editModeHorizontalAlign,
|
||||
editModeVerticalPosition,
|
||||
editModeContent,
|
||||
isEditMode,
|
||||
onOutsideClick,
|
||||
}: OwnProps) {
|
||||
const wrapperRef = useRef(null);
|
||||
|
||||
const [, setIsSomeInputInEditMode] = useRecoilState(
|
||||
isSomeInputInEditModeState,
|
||||
);
|
||||
|
||||
const debouncedSetIsSomeInputInEditMode = useMemo(() => {
|
||||
return debounce(setIsSomeInputInEditMode, 20);
|
||||
}, [setIsSomeInputInEditMode]);
|
||||
|
||||
useListenClickOutsideArrayOfRef([wrapperRef], () => {
|
||||
if (isEditMode) {
|
||||
debouncedSetIsSomeInputInEditMode(false);
|
||||
onOutsideClick?.();
|
||||
}
|
||||
});
|
||||
|
||||
useHotkeys(
|
||||
'esc',
|
||||
() => {
|
||||
if (isEditMode) {
|
||||
onOutsideClick?.();
|
||||
|
||||
debouncedSetIsSomeInputInEditMode(false);
|
||||
}
|
||||
},
|
||||
{
|
||||
preventDefault: true,
|
||||
enableOnContentEditable: true,
|
||||
enableOnFormTags: true,
|
||||
},
|
||||
[isEditMode, onOutsideClick, debouncedSetIsSomeInputInEditMode],
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
'enter',
|
||||
() => {
|
||||
if (isEditMode) {
|
||||
onOutsideClick?.();
|
||||
|
||||
debouncedSetIsSomeInputInEditMode(false);
|
||||
}
|
||||
},
|
||||
{
|
||||
preventDefault: true,
|
||||
enableOnContentEditable: true,
|
||||
enableOnFormTags: true,
|
||||
},
|
||||
[isEditMode, onOutsideClick, debouncedSetIsSomeInputInEditMode],
|
||||
);
|
||||
|
||||
return (
|
||||
<CellEditModeContainer
|
||||
data-testid="editable-cell-edit-mode-container"
|
||||
ref={wrapperRef}
|
||||
editModeHorizontalAlign={editModeHorizontalAlign}
|
||||
editModeVerticalPosition={editModeVerticalPosition}
|
||||
>
|
||||
{editModeContent}
|
||||
</CellEditModeContainer>
|
||||
);
|
||||
}
|
||||
@ -1,9 +1,9 @@
|
||||
import { ReactElement, useRef } from 'react';
|
||||
import { useOutsideAlerter } from '../../hooks/useOutsideAlerter';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { ReactElement } from 'react';
|
||||
import { CellBaseContainer } from './CellBaseContainer';
|
||||
import styled from '@emotion/styled';
|
||||
import { EditableCellMenuEditModeContainer } from './EditableCellMenuEditModeContainer';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { isSomeInputInEditModeState } from '../../modules/ui/tables/states/isSomeInputInEditModeState';
|
||||
import { EditableCellMenuEditMode } from './EditableCellMenuEditMode';
|
||||
|
||||
const EditableCellMenuNormalModeContainer = styled.div`
|
||||
display: flex;
|
||||
@ -34,61 +34,31 @@ export function EditableCellMenu({
|
||||
onOutsideClick,
|
||||
onInsideClick,
|
||||
}: OwnProps) {
|
||||
const wrapperRef = useRef(null);
|
||||
const editableContainerRef = useRef(null);
|
||||
|
||||
useOutsideAlerter(wrapperRef, () => {
|
||||
onOutsideClick?.();
|
||||
});
|
||||
|
||||
useHotkeys(
|
||||
'esc',
|
||||
() => {
|
||||
if (isEditMode) {
|
||||
onOutsideClick?.();
|
||||
}
|
||||
},
|
||||
{
|
||||
preventDefault: true,
|
||||
enableOnContentEditable: true,
|
||||
enableOnFormTags: true,
|
||||
},
|
||||
[isEditMode, onOutsideClick],
|
||||
const [isSomeInputInEditMode, setIsSomeInputInEditMode] = useRecoilState(
|
||||
isSomeInputInEditModeState,
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
'enter',
|
||||
() => {
|
||||
if (isEditMode) {
|
||||
onOutsideClick?.();
|
||||
}
|
||||
},
|
||||
{
|
||||
preventDefault: true,
|
||||
enableOnContentEditable: true,
|
||||
enableOnFormTags: true,
|
||||
},
|
||||
[isEditMode, onOutsideClick],
|
||||
);
|
||||
function handleOnClick() {
|
||||
if (!isSomeInputInEditMode) {
|
||||
onInsideClick?.();
|
||||
setIsSomeInputInEditMode(true);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<CellBaseContainer
|
||||
ref={wrapperRef}
|
||||
onClick={() => {
|
||||
onInsideClick && onInsideClick();
|
||||
}}
|
||||
>
|
||||
<CellBaseContainer onClick={handleOnClick}>
|
||||
<EditableCellMenuNormalModeContainer>
|
||||
{nonEditModeContent}
|
||||
</EditableCellMenuNormalModeContainer>
|
||||
{isEditMode && (
|
||||
<EditableCellMenuEditModeContainer
|
||||
ref={editableContainerRef}
|
||||
<EditableCellMenuEditMode
|
||||
editModeContent={editModeContent}
|
||||
editModeHorizontalAlign={editModeHorizontalAlign}
|
||||
editModeVerticalPosition={editModeVerticalPosition}
|
||||
>
|
||||
{editModeContent}
|
||||
</EditableCellMenuEditModeContainer>
|
||||
isEditMode={isEditMode}
|
||||
onOutsideClick={onOutsideClick}
|
||||
onInsideClick={onInsideClick}
|
||||
/>
|
||||
)}
|
||||
</CellBaseContainer>
|
||||
);
|
||||
|
||||
@ -0,0 +1,85 @@
|
||||
import { ReactElement, useMemo, useRef } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { isSomeInputInEditModeState } from '../../modules/ui/tables/states/isSomeInputInEditModeState';
|
||||
import { useListenClickOutsideArrayOfRef } from '../../modules/ui/common/hooks/useListenClickOutsideArrayOfRef';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { debounce } from '../../modules/utils/debounce';
|
||||
import { EditableCellMenuEditModeContainer } from './EditableCellMenuEditModeContainer';
|
||||
|
||||
type OwnProps = {
|
||||
editModeContent: ReactElement;
|
||||
editModeHorizontalAlign?: 'left' | 'right';
|
||||
editModeVerticalPosition?: 'over' | 'below';
|
||||
isEditMode?: boolean;
|
||||
onOutsideClick?: () => void;
|
||||
onInsideClick?: () => void;
|
||||
};
|
||||
|
||||
export function EditableCellMenuEditMode({
|
||||
editModeHorizontalAlign,
|
||||
editModeVerticalPosition,
|
||||
editModeContent,
|
||||
isEditMode,
|
||||
onOutsideClick,
|
||||
}: OwnProps) {
|
||||
const wrapperRef = useRef(null);
|
||||
|
||||
const [, setIsSomeInputInEditMode] = useRecoilState(
|
||||
isSomeInputInEditModeState,
|
||||
);
|
||||
|
||||
const debouncedSetIsSomeInputInEditMode = useMemo(() => {
|
||||
return debounce(setIsSomeInputInEditMode, 20);
|
||||
}, [setIsSomeInputInEditMode]);
|
||||
|
||||
useListenClickOutsideArrayOfRef([wrapperRef], () => {
|
||||
if (isEditMode) {
|
||||
debouncedSetIsSomeInputInEditMode(false);
|
||||
onOutsideClick?.();
|
||||
}
|
||||
});
|
||||
|
||||
useHotkeys(
|
||||
'esc',
|
||||
() => {
|
||||
if (isEditMode) {
|
||||
onOutsideClick?.();
|
||||
|
||||
debouncedSetIsSomeInputInEditMode(false);
|
||||
}
|
||||
},
|
||||
{
|
||||
preventDefault: true,
|
||||
enableOnContentEditable: true,
|
||||
enableOnFormTags: true,
|
||||
},
|
||||
[isEditMode, onOutsideClick, debouncedSetIsSomeInputInEditMode],
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
'enter',
|
||||
() => {
|
||||
if (isEditMode) {
|
||||
onOutsideClick?.();
|
||||
|
||||
debouncedSetIsSomeInputInEditMode(false);
|
||||
}
|
||||
},
|
||||
{
|
||||
preventDefault: true,
|
||||
enableOnContentEditable: true,
|
||||
enableOnFormTags: true,
|
||||
},
|
||||
[isEditMode, onOutsideClick, debouncedSetIsSomeInputInEditMode],
|
||||
);
|
||||
|
||||
return (
|
||||
<EditableCellMenuEditModeContainer
|
||||
ref={wrapperRef}
|
||||
editModeHorizontalAlign={editModeHorizontalAlign}
|
||||
editModeVerticalPosition={editModeVerticalPosition}
|
||||
>
|
||||
{editModeContent}
|
||||
</EditableCellMenuEditModeContainer>
|
||||
);
|
||||
}
|
||||
@ -10,6 +10,8 @@ import { FaPlus } from 'react-icons/fa';
|
||||
import { HoverableMenuItem } from './HoverableMenuItem';
|
||||
import { EditableCellMenu } from './EditableCellMenu';
|
||||
import { CellNormalModeContainer } from './CellNormalModeContainer';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { isSomeInputInEditModeState } from '../../modules/ui/tables/states/isSomeInputInEditModeState';
|
||||
|
||||
const StyledEditModeContainer = styled.div`
|
||||
width: 200px;
|
||||
@ -112,6 +114,9 @@ function EditableRelation<
|
||||
onCreate,
|
||||
}: EditableRelationProps<RelationType, ChipComponentPropsType>) {
|
||||
const [isEditMode, setIsEditMode] = useState(false);
|
||||
const [, setIsSomeInputInEditMode] = useRecoilState(
|
||||
isSomeInputInEditModeState,
|
||||
);
|
||||
|
||||
// TODO: Tie this to a react context
|
||||
const [filterSearchResults, setSearchInput, setFilterSearch, searchInput] =
|
||||
@ -130,7 +135,12 @@ function EditableRelation<
|
||||
|
||||
function handleCreateNewRelationButtonClick() {
|
||||
onCreate?.(searchInput);
|
||||
closeEditMode();
|
||||
}
|
||||
|
||||
function closeEditMode() {
|
||||
setIsEditMode(false);
|
||||
setIsSomeInputInEditMode(false);
|
||||
}
|
||||
|
||||
return (
|
||||
@ -155,6 +165,7 @@ function EditableRelation<
|
||||
</StyledEditModeSelectedContainer>
|
||||
<StyledEditModeSearchContainer>
|
||||
<StyledEditModeSearchInput
|
||||
autoFocus
|
||||
placeholder={searchPlaceholder}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
setFilterSearch(searchConfig);
|
||||
@ -183,7 +194,7 @@ function EditableRelation<
|
||||
key={index}
|
||||
onClick={() => {
|
||||
onChange(result.value);
|
||||
setIsEditMode(false);
|
||||
closeEditMode();
|
||||
}}
|
||||
>
|
||||
<HoverableMenuItem>
|
||||
|
||||
@ -3,10 +3,10 @@ import { isDefined } from '../../../utils/type-guards/isDefined';
|
||||
|
||||
export function useListenClickOutsideArrayOfRef<T extends HTMLElement>(
|
||||
arrayOfRef: Array<React.RefObject<T>>,
|
||||
outsideClickCallback: (event?: MouseEvent) => void,
|
||||
outsideClickCallback: (event?: MouseEvent | TouchEvent) => void,
|
||||
) {
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event: any) {
|
||||
function handleClickOutside(event: MouseEvent | TouchEvent) {
|
||||
const clickedOnAtLeastOneRef = arrayOfRef
|
||||
.filter((ref) => !!ref.current)
|
||||
.some((ref) => ref.current?.contains(event.target as Node));
|
||||
@ -21,13 +21,13 @@ export function useListenClickOutsideArrayOfRef<T extends HTMLElement>(
|
||||
);
|
||||
|
||||
if (hasAtLeastOneRefDefined) {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
document.addEventListener('touchstart', handleClickOutside);
|
||||
document.addEventListener('mouseup', handleClickOutside);
|
||||
document.addEventListener('touchend', handleClickOutside);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
document.removeEventListener('touchstart', handleClickOutside);
|
||||
document.removeEventListener('mouseup', handleClickOutside);
|
||||
document.removeEventListener('touchend', handleClickOutside);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [arrayOfRef, outsideClickCallback]);
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const isSomeInputInEditModeState = atom<boolean>({
|
||||
key: 'ui/table/is-in-edit-mode',
|
||||
default: false,
|
||||
});
|
||||
59
front/src/pages/people/__stories__/People.inputs.stories.tsx
Normal file
59
front/src/pages/people/__stories__/People.inputs.stories.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import { expect } from '@storybook/jest';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import { userEvent, within } from '@storybook/testing-library';
|
||||
|
||||
import People from '../People';
|
||||
import { Story } from './People.stories';
|
||||
import { mocks, render } from './shared';
|
||||
import { mockedPeopleData } from '../../../testing/mock-data/people';
|
||||
|
||||
const meta: Meta<typeof People> = {
|
||||
title: 'Pages/People',
|
||||
component: People,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
export const ChangeEmail: Story = {
|
||||
render,
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const firstRowEmailCell = await canvas.findByText(
|
||||
mockedPeopleData[0].email,
|
||||
);
|
||||
|
||||
const secondRowEmailCell = await canvas.findByText(
|
||||
mockedPeopleData[1].email,
|
||||
);
|
||||
|
||||
expect(
|
||||
canvas.queryByTestId('editable-cell-edit-mode-container'),
|
||||
).toBeNull();
|
||||
|
||||
await userEvent.click(firstRowEmailCell);
|
||||
|
||||
expect(
|
||||
canvas.queryByTestId('editable-cell-edit-mode-container'),
|
||||
).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(secondRowEmailCell);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 25));
|
||||
|
||||
expect(
|
||||
canvas.queryByTestId('editable-cell-edit-mode-container'),
|
||||
).toBeNull();
|
||||
|
||||
await userEvent.click(secondRowEmailCell);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 25));
|
||||
|
||||
expect(
|
||||
canvas.queryByTestId('editable-cell-edit-mode-container'),
|
||||
).toBeInTheDocument();
|
||||
},
|
||||
parameters: {
|
||||
msw: mocks,
|
||||
},
|
||||
};
|
||||
@ -1,6 +1,6 @@
|
||||
import { GraphqlQueryPerson } from '../../interfaces/entities/person.interface';
|
||||
|
||||
export const mockedPeopleData: Array<GraphqlQueryPerson> = [
|
||||
export const mockedPeopleData = [
|
||||
{
|
||||
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b',
|
||||
__typename: 'Person',
|
||||
@ -70,4 +70,4 @@ export const mockedPeopleData: Array<GraphqlQueryPerson> = [
|
||||
|
||||
city: 'Paris',
|
||||
},
|
||||
];
|
||||
] satisfies Array<GraphqlQueryPerson>;
|
||||
|
||||
Reference in New Issue
Block a user