Feat/better hotkeys scope (#526)

* Working version

* fix

* Fixed console log

* Fix lint

* wip

* Fix

* Fix

* consolelog

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Lucas Bordeau
2023-07-08 03:53:05 +02:00
committed by GitHub
parent 611cda1f41
commit 66dcc9b2e1
77 changed files with 1240 additions and 454 deletions

View File

@ -1,12 +1,16 @@
import { ReactElement } from 'react';
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
import { useAddToHotkeysScopeStack } from '@/hotkeys/hooks/useAddToHotkeysScopeStack';
import { HotkeysScopeStackItem } from '@/hotkeys/types/internal/HotkeysScopeStackItems';
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { isSoftFocusActiveState } from '@/ui/tables/states/isSoftFocusActiveState';
import { useEditableCell } from './hooks/useCloseEditableCell';
import { useCurrentCellEditMode } from './hooks/useCurrentCellEditMode';
import { useIsSoftFocusOnCurrentCell } from './hooks/useIsSoftFocusOnCurrentCell';
import { useSetSoftFocusOnCurrentCell } from './hooks/useSetSoftFocusOnCurrentCell';
import { isEditModeScopedState } from './states/isEditModeScopedState';
import { useSoftFocusOnCurrentCell } from './hooks/useSetSoftFocusOnCurrentCell';
import { EditableCellDisplayMode } from './EditableCellDisplayMode';
import { EditableCellEditMode } from './EditableCellEditMode';
import { EditableCellSoftFocusMode } from './EditableCellSoftFocusMode';
@ -27,6 +31,7 @@ type OwnProps = {
nonEditModeContent: ReactElement;
editModeHorizontalAlign?: 'left' | 'right';
editModeVerticalPosition?: 'over' | 'below';
editHotkeysScope?: HotkeysScopeStackItem;
};
export function EditableCell({
@ -34,39 +39,51 @@ export function EditableCell({
editModeVerticalPosition = 'over',
editModeContent,
nonEditModeContent,
editHotkeysScope,
}: OwnProps) {
const [isEditMode] = useRecoilScopedState(isEditModeScopedState);
const { isCurrentCellInEditMode } = useCurrentCellEditMode();
const setSoftFocusOnCurrentCell = useSetSoftFocusOnCurrentCell();
const setSoftFocusOnCurrentCell = useSoftFocusOnCurrentCell();
const { closeEditableCell, openEditableCell } = useEditableCell();
const { openEditableCell } = useEditableCell();
const isSoftFocusActive = useRecoilValue(isSoftFocusActiveState);
const addToHotkeysScopeStack = useAddToHotkeysScopeStack();
// 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();
}
if (isCurrentCellInEditMode) {
return;
}
function handleOnOutsideClick() {
closeEditableCell();
if (isSoftFocusActive) {
openEditableCell();
addToHotkeysScopeStack(
editHotkeysScope ?? {
scope: InternalHotkeysScope.CellEditMode,
},
);
}
setSoftFocusOnCurrentCell();
}
const hasSoftFocus = useIsSoftFocusOnCurrentCell();
return (
<CellBaseContainer onClick={handleOnClick}>
{isEditMode ? (
{isCurrentCellInEditMode ? (
<EditableCellEditMode
editModeHorizontalAlign={editModeHorizontalAlign}
editModeVerticalPosition={editModeVerticalPosition}
onOutsideClick={handleOnOutsideClick}
>
{editModeContent}
</EditableCellEditMode>
) : hasSoftFocus ? (
<EditableCellSoftFocusMode>
<EditableCellSoftFocusMode editHotkeysScope={editHotkeysScope}>
{nonEditModeContent}
</EditableCellSoftFocusMode>
) : (

View File

@ -1,7 +1,8 @@
import { ReactElement, useRef } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import styled from '@emotion/styled';
import { useScopedHotkeys } from '@/hotkeys/hooks/useScopedHotkeys';
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { useListenClickOutsideArrayOfRef } from '@/ui/hooks/useListenClickOutsideArrayOfRef';
import { useMoveSoftFocus } from '@/ui/tables/hooks/useMoveSoftFocus';
import { overlayBackground } from '@/ui/themes/effects';
@ -38,7 +39,6 @@ export function EditableCellEditMode({
editModeHorizontalAlign,
editModeVerticalPosition,
children,
onOutsideClick,
}: OwnProps) {
const wrapperRef = useRef(null);
@ -46,61 +46,45 @@ export function EditableCellEditMode({
const { moveRight, moveLeft, moveDown } = useMoveSoftFocus();
useListenClickOutsideArrayOfRef([wrapperRef], () => {
onOutsideClick?.();
closeEditableCell();
});
useHotkeys(
useScopedHotkeys(
'enter',
() => {
closeEditableCell();
moveDown();
},
{
enableOnContentEditable: true,
enableOnFormTags: true,
preventDefault: true,
},
InternalHotkeysScope.CellEditMode,
[closeEditableCell],
);
useHotkeys(
useScopedHotkeys(
'esc',
() => {
closeEditableCell();
},
{
enableOnContentEditable: true,
enableOnFormTags: true,
preventDefault: true,
},
InternalHotkeysScope.CellEditMode,
[closeEditableCell],
);
useHotkeys(
useScopedHotkeys(
'tab',
() => {
closeEditableCell();
moveRight();
},
{
enableOnContentEditable: true,
enableOnFormTags: true,
preventDefault: true,
},
InternalHotkeysScope.CellEditMode,
[closeEditableCell, moveRight],
);
useHotkeys(
useScopedHotkeys(
'shift+tab',
() => {
closeEditableCell();
moveLeft();
},
{
enableOnContentEditable: true,
enableOnFormTags: true,
preventDefault: true,
},
InternalHotkeysScope.CellEditMode,
[closeEditableCell, moveRight],
);

View File

@ -1,8 +1,9 @@
import React from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useRecoilState } from 'recoil';
import { captureHotkeyTypeInFocusState } from '@/hotkeys/states/captureHotkeyTypeInFocusState';
import { useAddToHotkeysScopeStack } from '@/hotkeys/hooks/useAddToHotkeysScopeStack';
import { useScopedHotkeys } from '@/hotkeys/hooks/useScopedHotkeys';
import { HotkeysScopeStackItem } from '@/hotkeys/types/internal/HotkeysScopeStackItems';
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { isNonTextWritingKey } from '@/utils/hotkeys/isNonTextWritingKey';
import { useEditableCell } from './hooks/useCloseEditableCell';
@ -10,26 +11,26 @@ import { EditableCellDisplayMode } from './EditableCellDisplayMode';
export function EditableCellSoftFocusMode({
children,
}: React.PropsWithChildren<unknown>) {
editHotkeysScope,
}: React.PropsWithChildren<{ editHotkeysScope?: HotkeysScopeStackItem }>) {
const { closeEditableCell, openEditableCell } = useEditableCell();
const [captureHotkeyTypeInFocus] = useRecoilState(
captureHotkeyTypeInFocusState,
);
const addToHotkeysScopeStack = useAddToHotkeysScopeStack();
useHotkeys(
useScopedHotkeys(
'enter',
() => {
openEditableCell();
addToHotkeysScopeStack(
editHotkeysScope ?? {
scope: InternalHotkeysScope.CellEditMode,
},
);
},
{
enableOnContentEditable: true,
enableOnFormTags: true,
preventDefault: true,
},
InternalHotkeysScope.TableSoftFocus,
[closeEditableCell],
);
useHotkeys(
useScopedHotkeys(
'*',
(keyboardEvent) => {
const isWritingText =
@ -41,14 +42,16 @@ export function EditableCellSoftFocusMode({
return;
}
if (captureHotkeyTypeInFocus) {
return;
}
openEditableCell();
addToHotkeysScopeStack(
editHotkeysScope ?? {
scope: InternalHotkeysScope.CellEditMode,
},
);
},
InternalHotkeysScope.TableSoftFocus,
[openEditableCell, addToHotkeysScopeStack, editHotkeysScope],
{
enableOnContentEditable: true,
enableOnFormTags: true,
preventDefault: false,
},
);

View File

@ -1,24 +1,24 @@
import { useRecoilCallback } from 'recoil';
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
import { useRemoveHighestHotkeysScopeStackItem } from '@/hotkeys/hooks/useRemoveHighestHotkeysScopeStackItem';
import { useCloseCurrentCellInEditMode } from '@/ui/tables/hooks/useClearCellInEditMode';
import { isSoftFocusActiveState } from '@/ui/tables/states/isSoftFocusActiveState';
import { isSomeInputInEditModeState } from '@/ui/tables/states/isSomeInputInEditModeState';
import { isEditModeScopedState } from '../states/isEditModeScopedState';
import { useCurrentCellEditMode } from './useCurrentCellEditMode';
export function useEditableCell() {
const [, setIsEditMode] = useRecoilScopedState(isEditModeScopedState);
const { setCurrentCellInEditMode } = useCurrentCellEditMode();
const closeEditableCell = useRecoilCallback(
({ set }) =>
async () => {
setIsEditMode(false);
const closeCurrentCellInEditMode = useCloseCurrentCellInEditMode();
await new Promise((resolve) => setTimeout(resolve, 20));
const removeHighestHotkeysScopedStackItem =
useRemoveHighestHotkeysScopeStackItem();
set(isSomeInputInEditModeState, false);
},
[setIsEditMode],
);
function closeEditableCell() {
closeCurrentCellInEditMode();
removeHighestHotkeysScopedStackItem();
}
const openEditableCell = useRecoilCallback(
({ snapshot, set }) =>
@ -29,11 +29,12 @@ export function useEditableCell() {
if (!isSomeInputInEditMode) {
set(isSomeInputInEditModeState, true);
set(isSoftFocusActiveState, false);
setIsEditMode(true);
setCurrentCellInEditMode();
}
},
[setIsEditMode],
[setCurrentCellInEditMode],
);
return {

View File

@ -0,0 +1,23 @@
import { useCallback } from 'react';
import { useRecoilState } from 'recoil';
import { useMoveEditModeToCellPosition } from '@/ui/tables/hooks/useMoveEditModeToCellPosition';
import { isCellInEditModeFamilyState } from '@/ui/tables/states/isCellInEditModeFamilyState';
import { useCurrentCellPosition } from './useCurrentCellPosition';
export function useCurrentCellEditMode() {
const moveEditModeToCellPosition = useMoveEditModeToCellPosition();
const currentCellPosition = useCurrentCellPosition();
const [isCurrentCellInEditMode] = useRecoilState(
isCellInEditModeFamilyState(currentCellPosition),
);
const setCurrentCellInEditMode = useCallback(() => {
moveEditModeToCellPosition(currentCellPosition);
}, [currentCellPosition, moveEditModeToCellPosition]);
return { isCurrentCellInEditMode, setCurrentCellInEditMode };
}

View File

@ -0,0 +1,30 @@
import { useMemo } from 'react';
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
import { CellContext } from '@/ui/tables/states/CellContext';
import { currentColumnNumberScopedState } from '@/ui/tables/states/currentColumnNumberScopedState';
import { currentRowNumberScopedState } from '@/ui/tables/states/currentRowNumberScopedState';
import { RowContext } from '@/ui/tables/states/RowContext';
import { CellPosition } from '@/ui/tables/types/CellPosition';
export function useCurrentCellPosition() {
const [currentRowNumber] = useRecoilScopedState(
currentRowNumberScopedState,
RowContext,
);
const [currentColumnNumber] = useRecoilScopedState(
currentColumnNumberScopedState,
CellContext,
);
const currentCellPosition: CellPosition = useMemo(
() => ({
column: currentColumnNumber,
row: currentRowNumber,
}),
[currentColumnNumber, currentRowNumber],
);
return currentCellPosition;
}

View File

@ -1,35 +1,14 @@
import { useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
import { CellContext } from '@/ui/tables/states/CellContext';
import { currentColumnNumberScopedState } from '@/ui/tables/states/currentColumnNumberScopedState';
import { currentRowNumberScopedState } from '@/ui/tables/states/currentRowNumberScopedState';
import { isSoftFocusOnCellFamilyState } from '@/ui/tables/states/isSoftFocusOnCellFamilyState';
import { RowContext } from '@/ui/tables/states/RowContext';
import { TablePosition } from '@/ui/tables/types/TablePosition';
import { useCurrentCellPosition } from './useCurrentCellPosition';
export function useIsSoftFocusOnCurrentCell() {
const [currentRowNumber] = useRecoilScopedState(
currentRowNumberScopedState,
RowContext,
);
const [currentColumnNumber] = useRecoilScopedState(
currentColumnNumberScopedState,
CellContext,
);
const currentTablePosition: TablePosition = useMemo(
() => ({
column: currentColumnNumber,
row: currentRowNumber,
}),
[currentColumnNumber, currentRowNumber],
);
const currentCellPosition = useCurrentCellPosition();
const isSoftFocusOnCell = useRecoilValue(
isSoftFocusOnCellFamilyState(currentTablePosition),
isSoftFocusOnCellFamilyState(currentCellPosition),
);
return isSoftFocusOnCell;

View File

@ -1,14 +1,18 @@
import { useCallback, useMemo } from 'react';
import { useRecoilState } from 'recoil';
import { useAddToHotkeysScopeStack } from '@/hotkeys/hooks/useAddToHotkeysScopeStack';
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
import { useSetSoftFocusPosition } from '@/ui/tables/hooks/useSetSoftFocusPosition';
import { CellContext } from '@/ui/tables/states/CellContext';
import { currentColumnNumberScopedState } from '@/ui/tables/states/currentColumnNumberScopedState';
import { currentRowNumberScopedState } from '@/ui/tables/states/currentRowNumberScopedState';
import { isSoftFocusActiveState } from '@/ui/tables/states/isSoftFocusActiveState';
import { RowContext } from '@/ui/tables/states/RowContext';
import { TablePosition } from '@/ui/tables/types/TablePosition';
import { CellPosition } from '@/ui/tables/types/CellPosition';
export function useSetSoftFocusOnCurrentCell() {
export function useSoftFocusOnCurrentCell() {
const setSoftFocusPosition = useSetSoftFocusPosition();
const [currentRowNumber] = useRecoilScopedState(
currentRowNumberScopedState,
@ -20,7 +24,7 @@ export function useSetSoftFocusOnCurrentCell() {
CellContext,
);
const currentTablePosition: TablePosition = useMemo(
const currentTablePosition: CellPosition = useMemo(
() => ({
column: currentColumnNumber,
row: currentRowNumber,
@ -28,7 +32,18 @@ export function useSetSoftFocusOnCurrentCell() {
[currentColumnNumber, currentRowNumber],
);
const [, setIsSoftFocusActive] = useRecoilState(isSoftFocusActiveState);
const addToHotkeysScopeStack = useAddToHotkeysScopeStack();
return useCallback(() => {
setSoftFocusPosition(currentTablePosition);
}, [setSoftFocusPosition, currentTablePosition]);
setIsSoftFocusActive(true);
addToHotkeysScopeStack({ scope: InternalHotkeysScope.TableSoftFocus });
}, [
setSoftFocusPosition,
currentTablePosition,
setIsSoftFocusActive,
addToHotkeysScopeStack,
]);
}

View File

@ -1,6 +0,0 @@
import { atomFamily } from 'recoil';
export const isEditModeScopedState = atomFamily<boolean, string>({
key: 'isEditModeScopedState',
default: false,
});

View File

@ -1,10 +1,11 @@
import { ChangeEvent, ReactElement, useRef } from 'react';
import styled from '@emotion/styled';
import { ReactElement } from 'react';
import { textInputStyle } from '@/ui/themes/effects';
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { EditableCell } from '../EditableCell';
import { EditableDoubleTextEditMode } from './EditableDoubleTextEditMode';
type OwnProps = {
firstValue: string;
secondValue: string;
@ -14,57 +15,25 @@ type OwnProps = {
onChange: (firstValue: string, secondValue: string) => void;
};
const StyledContainer = styled.div`
align-items: center;
display: flex;
justify-content: space-between;
& > input:last-child {
border-left: 1px solid ${({ theme }) => theme.border.color.medium};
padding-left: ${({ theme }) => theme.spacing(2)};
}
`;
const StyledEditInplaceInput = styled.input`
height: 18px;
margin: 0;
width: 45%;
${textInputStyle}
`;
export function EditableDoubleText({
firstValue,
secondValue,
firstValuePlaceholder,
secondValuePlaceholder,
nonEditModeContent,
onChange,
nonEditModeContent,
}: OwnProps) {
const firstValueInputRef = useRef<HTMLInputElement>(null);
return (
<EditableCell
editHotkeysScope={{ scope: InternalHotkeysScope.CellDoubleTextInput }}
editModeContent={
<StyledContainer>
<StyledEditInplaceInput
autoFocus
placeholder={firstValuePlaceholder}
ref={firstValueInputRef}
value={firstValue}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
onChange(event.target.value, secondValue);
}}
/>
<StyledEditInplaceInput
placeholder={secondValuePlaceholder}
ref={firstValueInputRef}
value={secondValue}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
onChange(firstValue, event.target.value);
}}
/>
</StyledContainer>
<EditableDoubleTextEditMode
firstValue={firstValue}
secondValue={secondValue}
firstValuePlaceholder={firstValuePlaceholder}
secondValuePlaceholder={secondValuePlaceholder}
onChange={onChange}
/>
}
nonEditModeContent={nonEditModeContent}
></EditableCell>

View File

@ -0,0 +1,129 @@
import { ChangeEvent, useRef, useState } from 'react';
import styled from '@emotion/styled';
import { Key } from 'ts-key-enum';
import { useScopedHotkeys } from '@/hotkeys/hooks/useScopedHotkeys';
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { useMoveSoftFocus } from '@/ui/tables/hooks/useMoveSoftFocus';
import { textInputStyle } from '@/ui/themes/effects';
import { useEditableCell } from '../hooks/useCloseEditableCell';
type OwnProps = {
firstValue: string;
secondValue: string;
firstValuePlaceholder: string;
secondValuePlaceholder: string;
onChange: (firstValue: string, secondValue: string) => void;
};
const StyledContainer = styled.div`
align-items: center;
display: flex;
justify-content: space-between;
& > input:last-child {
border-left: 1px solid ${({ theme }) => theme.border.color.medium};
padding-left: ${({ theme }) => theme.spacing(2)};
}
`;
const StyledEditInplaceInput = styled.input`
height: 18px;
margin: 0;
width: 45%;
${textInputStyle}
`;
export function EditableDoubleTextEditMode({
firstValue,
secondValue,
firstValuePlaceholder,
secondValuePlaceholder,
onChange,
}: OwnProps) {
const [focusPosition, setFocusPosition] = useState<'left' | 'right'>('left');
const firstValueInputRef = useRef<HTMLInputElement>(null);
const secondValueInputRef = useRef<HTMLInputElement>(null);
const { closeEditableCell } = useEditableCell();
const { moveRight, moveLeft, moveDown } = useMoveSoftFocus();
function closeCell() {
setFocusPosition('left');
closeEditableCell();
}
useScopedHotkeys(
Key.Enter,
() => {
closeCell();
moveDown();
},
InternalHotkeysScope.CellDoubleTextInput,
[closeCell],
);
useScopedHotkeys(
Key.Escape,
() => {
closeCell();
},
InternalHotkeysScope.CellDoubleTextInput,
[closeCell],
);
useScopedHotkeys(
'tab',
async (keyboardEvent, hotkeyEvent) => {
if (focusPosition === 'left') {
setFocusPosition('right');
secondValueInputRef.current?.focus();
} else {
closeCell();
moveRight();
}
},
InternalHotkeysScope.CellDoubleTextInput,
[closeCell, moveRight, focusPosition],
);
useScopedHotkeys(
'shift+tab',
() => {
if (focusPosition === 'right') {
setFocusPosition('left');
firstValueInputRef.current?.focus();
} else {
closeCell();
moveLeft();
}
},
InternalHotkeysScope.CellDoubleTextInput,
[closeCell, moveRight, focusPosition],
);
return (
<StyledContainer>
<StyledEditInplaceInput
autoFocus
placeholder={firstValuePlaceholder}
ref={firstValueInputRef}
value={firstValue}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
onChange(event.target.value, secondValue);
}}
/>
<StyledEditInplaceInput
placeholder={secondValuePlaceholder}
ref={secondValueInputRef}
value={secondValue}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
onChange(firstValue, event.target.value);
}}
/>
</StyledContainer>
);
}