Refacto/abstract inplace input (#530)

* Move code to new folder

* Deduplicate code, remove dependancy on table

* Remove more table dependency

* Move close logic to input

* Migrate editable text cell

* Rename EditableTextInput

* Fix component test id
This commit is contained in:
Emilien Chauvet
2023-07-07 12:11:57 -07:00
committed by GitHub
parent 26b033abc9
commit c847bca293
19 changed files with 189 additions and 138 deletions

View File

@ -1,26 +1,9 @@
import { ReactElement } from 'react';
import styled from '@emotion/styled';
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
import { InplaceInput } from '../inplace-input/InplaceInput';
import { useEditableCell } from './hooks/useCloseEditableCell';
import { useIsSoftFocusOnCurrentCell } from './hooks/useIsSoftFocusOnCurrentCell';
import { useSetSoftFocusOnCurrentCell } from './hooks/useSetSoftFocusOnCurrentCell';
import { isEditModeScopedState } from './states/isEditModeScopedState';
import { EditableCellDisplayMode } from './EditableCellDisplayMode';
import { EditableCellEditMode } from './EditableCellEditMode';
import { EditableCellSoftFocusMode } from './EditableCellSoftFocusMode';
export const CellBaseContainer = styled.div`
align-items: center;
box-sizing: border-box;
cursor: pointer;
display: flex;
height: 32px;
position: relative;
user-select: none;
width: 100%;
`;
type OwnProps = {
editModeContent: ReactElement;
@ -35,43 +18,16 @@ export function EditableCell({
editModeContent,
nonEditModeContent,
}: OwnProps) {
const [isEditMode] = useRecoilScopedState(isEditModeScopedState);
const setSoftFocusOnCurrentCell = useSetSoftFocusOnCurrentCell();
const { closeEditableCell, openEditableCell } = useEditableCell();
// TODO: we might have silent problematic behavior because of the setTimeout in openEditableCell, investigate
// Maybe we could build a switchEditableCell to handle the case where we go from one cell to another.
// See https://github.com/twentyhq/twenty/issues/446
function handleOnClick() {
openEditableCell();
setSoftFocusOnCurrentCell();
}
function handleOnOutsideClick() {
closeEditableCell();
}
const hasSoftFocus = useIsSoftFocusOnCurrentCell();
return (
<CellBaseContainer onClick={handleOnClick}>
{isEditMode ? (
<EditableCellEditMode
editModeHorizontalAlign={editModeHorizontalAlign}
editModeVerticalPosition={editModeVerticalPosition}
onOutsideClick={handleOnOutsideClick}
>
{editModeContent}
</EditableCellEditMode>
) : hasSoftFocus ? (
<EditableCellSoftFocusMode>
{nonEditModeContent}
</EditableCellSoftFocusMode>
) : (
<EditableCellDisplayMode>{nonEditModeContent}</EditableCellDisplayMode>
)}
</CellBaseContainer>
<InplaceInput
editModeHorizontalAlign={editModeHorizontalAlign}
editModeVerticalPosition={editModeVerticalPosition}
editModeContent={editModeContent}
nonEditModeContent={nonEditModeContent}
setSoftFocusOnCurrentInplaceInput={setSoftFocusOnCurrentCell}
hasSoftFocus={!!hasSoftFocus}
/>
);
}

View File

@ -1,19 +0,0 @@
import styled from '@emotion/styled';
export const HoverableMenuItem = styled.div`
align-items: center;
background: ${({ theme }) => theme.background.primary};
border-radius: 4px;
box-sizing: border-box;
cursor: pointer;
display: flex;
height: 100%;
position: relative;
transition: background 0.1s ease;
user-select: none;
width: 100%;
&:hover {
background: ${({ theme }) => theme.background.transparent.light};
}
`;

View File

@ -0,0 +1,30 @@
import { InplaceTextInput } from '../../inplace-input/types/InplaceTextInput';
import { useIsSoftFocusOnCurrentCell } from '../hooks/useIsSoftFocusOnCurrentCell';
import { useSetSoftFocusOnCurrentCell } from '../hooks/useSetSoftFocusOnCurrentCell';
type OwnProps = {
placeholder?: string;
content: string;
changeHandler: (updated: string) => void;
editModeHorizontalAlign?: 'left' | 'right';
};
export function EditableTextCell({
editModeHorizontalAlign = 'left',
content,
changeHandler,
placeholder,
}: OwnProps) {
const setSoftFocusOnCurrentCell = useSetSoftFocusOnCurrentCell();
const hasSoftFocus = useIsSoftFocusOnCurrentCell();
return (
<InplaceTextInput
editModeHorizontalAlign={editModeHorizontalAlign}
content={content}
changeHandler={changeHandler}
placeholder={placeholder}
setSoftFocusOnCurrentInplaceInput={setSoftFocusOnCurrentCell}
hasSoftFocus={hasSoftFocus}
/>
);
}

View File

@ -0,0 +1,76 @@
import { ReactElement } from 'react';
import styled from '@emotion/styled';
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
import { useInplaceInput } from './hooks/useCloseInplaceInput';
import { isEditModeScopedState } from './states/isEditModeScopedState';
import { InplaceInputDisplayMode } from './InplaceInputDisplayMode';
import { InplaceInputEditMode } from './InplaceInputEditMode';
import { InplaceInputSoftFocusMode } from './InplaceInputSoftFocusMode';
export const InplaceInputBaseContainer = styled.div`
align-items: center;
box-sizing: border-box;
cursor: pointer;
display: flex;
height: 32px;
position: relative;
user-select: none;
width: 100%;
`;
type OwnProps = {
editModeContent: ReactElement;
nonEditModeContent: ReactElement;
editModeHorizontalAlign?: 'left' | 'right';
editModeVerticalPosition?: 'over' | 'below';
setSoftFocusOnCurrentInplaceInput?: () => void;
hasSoftFocus?: boolean;
};
export function InplaceInput({
editModeHorizontalAlign = 'left',
editModeVerticalPosition = 'over',
editModeContent,
nonEditModeContent,
setSoftFocusOnCurrentInplaceInput,
hasSoftFocus,
}: OwnProps) {
const [isEditMode] = useRecoilScopedState(isEditModeScopedState);
const { closeInplaceInput, openInplaceInput } = useInplaceInput();
// TODO: we might have silent problematic behavior because of the setTimeout in openInplaceInput, investigate
// Maybe we could build a switchInplaceInput to handle the case where we go from one InplaceInput to another.
// See https://github.com/twentyhq/twenty/issues/446
function handleOnClick() {
openInplaceInput();
setSoftFocusOnCurrentInplaceInput && setSoftFocusOnCurrentInplaceInput();
}
function handleOnOutsideClick() {
closeInplaceInput();
}
return (
<InplaceInputBaseContainer onClick={handleOnClick}>
{isEditMode ? (
<InplaceInputEditMode
editModeHorizontalAlign={editModeHorizontalAlign}
editModeVerticalPosition={editModeVerticalPosition}
onOutsideClick={handleOnOutsideClick}
>
{editModeContent}
</InplaceInputEditMode>
) : hasSoftFocus ? (
<InplaceInputSoftFocusMode>
{nonEditModeContent}
</InplaceInputSoftFocusMode>
) : (
<InplaceInputDisplayMode hasSoftFocus={!!hasSoftFocus}>
{nonEditModeContent}
</InplaceInputDisplayMode>
)}
</InplaceInputBaseContainer>
);
}

View File

@ -1,12 +1,10 @@
import styled from '@emotion/styled';
import { useIsSoftFocusOnCurrentCell } from './hooks/useIsSoftFocusOnCurrentCell';
type Props = {
softFocus: boolean;
};
export const EditableCellNormalModeOuterContainer = styled.div<Props>`
export const InplaceInputNormalModeOuterContainer = styled.div<Props>`
align-items: center;
display: flex;
height: 100%;
@ -24,7 +22,7 @@ export const EditableCellNormalModeOuterContainer = styled.div<Props>`
: ''}
`;
export const EditableCellNormalModeInnerContainer = styled.div`
export const InplaceInputNormalModeInnerContainer = styled.div`
align-items: center;
display: flex;
height: 100%;
@ -32,16 +30,17 @@ export const EditableCellNormalModeInnerContainer = styled.div`
width: 100%;
`;
export function EditableCellDisplayMode({
export function InplaceInputDisplayMode({
children,
}: React.PropsWithChildren<unknown>) {
const hasSoftFocus = useIsSoftFocusOnCurrentCell();
hasSoftFocus,
}: React.PropsWithChildren & {
hasSoftFocus: boolean;
}) {
return (
<EditableCellNormalModeOuterContainer softFocus={hasSoftFocus}>
<EditableCellNormalModeInnerContainer>
<InplaceInputNormalModeOuterContainer softFocus={hasSoftFocus}>
<InplaceInputNormalModeInnerContainer>
{children}
</EditableCellNormalModeInnerContainer>
</EditableCellNormalModeOuterContainer>
</InplaceInputNormalModeInnerContainer>
</InplaceInputNormalModeOuterContainer>
);
}

View File

@ -6,9 +6,9 @@ import { useListenClickOutsideArrayOfRef } from '@/ui/hooks/useListenClickOutsid
import { useMoveSoftFocus } from '@/ui/tables/hooks/useMoveSoftFocus';
import { overlayBackground } from '@/ui/themes/effects';
import { useEditableCell } from './hooks/useCloseEditableCell';
import { useInplaceInput } from './hooks/useCloseInplaceInput';
export const EditableCellEditModeContainer = styled.div<OwnProps>`
export const InplaceInputEditModeContainer = styled.div<OwnProps>`
align-items: center;
border: 1px solid ${({ theme }) => theme.border.color.light};
border-radius: ${({ theme }) => theme.border.radius.sm};
@ -34,7 +34,7 @@ type OwnProps = {
onOutsideClick?: () => void;
};
export function EditableCellEditMode({
export function InplaceInputEditMode({
editModeHorizontalAlign,
editModeVerticalPosition,
children,
@ -42,7 +42,7 @@ export function EditableCellEditMode({
}: OwnProps) {
const wrapperRef = useRef(null);
const { closeEditableCell } = useEditableCell();
const { closeInplaceInput } = useInplaceInput();
const { moveRight, moveLeft, moveDown } = useMoveSoftFocus();
useListenClickOutsideArrayOfRef([wrapperRef], () => {
@ -52,7 +52,7 @@ export function EditableCellEditMode({
useHotkeys(
'enter',
() => {
closeEditableCell();
closeInplaceInput();
moveDown();
},
{
@ -60,26 +60,26 @@ export function EditableCellEditMode({
enableOnFormTags: true,
preventDefault: true,
},
[closeEditableCell],
[closeInplaceInput],
);
useHotkeys(
'esc',
() => {
closeEditableCell();
closeInplaceInput();
},
{
enableOnContentEditable: true,
enableOnFormTags: true,
preventDefault: true,
},
[closeEditableCell],
[closeInplaceInput],
);
useHotkeys(
'tab',
() => {
closeEditableCell();
closeInplaceInput();
moveRight();
},
{
@ -87,13 +87,13 @@ export function EditableCellEditMode({
enableOnFormTags: true,
preventDefault: true,
},
[closeEditableCell, moveRight],
[closeInplaceInput, moveRight],
);
useHotkeys(
'shift+tab',
() => {
closeEditableCell();
closeInplaceInput();
moveLeft();
},
{
@ -101,17 +101,17 @@ export function EditableCellEditMode({
enableOnFormTags: true,
preventDefault: true,
},
[closeEditableCell, moveRight],
[closeInplaceInput, moveRight],
);
return (
<EditableCellEditModeContainer
data-testid="editable-cell-edit-mode-container"
<InplaceInputEditModeContainer
data-testid="inplace-input-edit-mode-container"
ref={wrapperRef}
editModeHorizontalAlign={editModeHorizontalAlign}
editModeVerticalPosition={editModeVerticalPosition}
>
{children}
</EditableCellEditModeContainer>
</InplaceInputEditModeContainer>
);
}

View File

@ -5,13 +5,13 @@ import { useRecoilState } from 'recoil';
import { captureHotkeyTypeInFocusState } from '@/hotkeys/states/captureHotkeyTypeInFocusState';
import { isNonTextWritingKey } from '@/utils/hotkeys/isNonTextWritingKey';
import { useEditableCell } from './hooks/useCloseEditableCell';
import { EditableCellDisplayMode } from './EditableCellDisplayMode';
import { useInplaceInput } from './hooks/useCloseInplaceInput';
import { InplaceInputDisplayMode } from './InplaceInputDisplayMode';
export function EditableCellSoftFocusMode({
export function InplaceInputSoftFocusMode({
children,
}: React.PropsWithChildren<unknown>) {
const { closeEditableCell, openEditableCell } = useEditableCell();
const { closeInplaceInput, openInplaceInput } = useInplaceInput();
const [captureHotkeyTypeInFocus] = useRecoilState(
captureHotkeyTypeInFocusState,
);
@ -19,14 +19,14 @@ export function EditableCellSoftFocusMode({
useHotkeys(
'enter',
() => {
openEditableCell();
openInplaceInput();
},
{
enableOnContentEditable: true,
enableOnFormTags: true,
preventDefault: true,
},
[closeEditableCell],
[closeInplaceInput],
);
useHotkeys(
@ -44,7 +44,7 @@ export function EditableCellSoftFocusMode({
if (captureHotkeyTypeInFocus) {
return;
}
openEditableCell();
openInplaceInput();
},
{
enableOnContentEditable: true,
@ -53,5 +53,7 @@ export function EditableCellSoftFocusMode({
},
);
return <EditableCellDisplayMode>{children}</EditableCellDisplayMode>;
return (
<InplaceInputDisplayMode hasSoftFocus>{children}</InplaceInputDisplayMode>
);
}

View File

@ -1,14 +1,15 @@
import { useRecoilCallback } from 'recoil';
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
// TODO: Remove dependancy to table
import { isSomeInputInEditModeState } from '@/ui/tables/states/isSomeInputInEditModeState';
import { isEditModeScopedState } from '../states/isEditModeScopedState';
export function useEditableCell() {
export function useInplaceInput() {
const [, setIsEditMode] = useRecoilScopedState(isEditModeScopedState);
const closeEditableCell = useRecoilCallback(
const closeInplaceInput = useRecoilCallback(
({ set }) =>
async () => {
setIsEditMode(false);
@ -20,7 +21,7 @@ export function useEditableCell() {
[setIsEditMode],
);
const openEditableCell = useRecoilCallback(
const openInplaceInput = useRecoilCallback(
({ snapshot, set }) =>
() => {
const isSomeInputInEditMode = snapshot
@ -37,7 +38,7 @@ export function useEditableCell() {
);
return {
closeEditableCell,
openEditableCell,
closeInplaceInput,
openInplaceInput,
};
}

View File

@ -3,13 +3,15 @@ import styled from '@emotion/styled';
import { textInputStyle } from '@/ui/themes/effects';
import { EditableCell } from '../EditableCell';
import { InplaceInput } from '../InplaceInput';
type OwnProps = {
placeholder?: string;
content: string;
changeHandler: (updated: string) => void;
editModeHorizontalAlign?: 'left' | 'right';
setSoftFocusOnCurrentInplaceInput?: () => void;
hasSoftFocus?: boolean;
};
// TODO: refactor
@ -26,17 +28,19 @@ const StyledNoEditText = styled.div`
width: 100%;
`;
export function EditableText({
export function InplaceTextInput({
content,
placeholder,
changeHandler,
editModeHorizontalAlign,
setSoftFocusOnCurrentInplaceInput,
hasSoftFocus,
}: OwnProps) {
const inputRef = useRef<HTMLInputElement>(null);
const [inputValue, setInputValue] = useState(content);
return (
<EditableCell
<InplaceInput
editModeHorizontalAlign={editModeHorizontalAlign}
editModeContent={
<StyledInplaceInput
@ -50,7 +54,9 @@ export function EditableText({
}}
/>
}
setSoftFocusOnCurrentInplaceInput={setSoftFocusOnCurrentInplaceInput}
hasSoftFocus={hasSoftFocus}
nonEditModeContent={<StyledNoEditText>{inputValue}</StyledNoEditText>}
></EditableCell>
></InplaceInput>
);
}