Add tab hotkey on table page (#457)

* wip

* wip

* - Added scopes on useHotkeys
- Use new EditableCellV2
- Implemented Recoil Scoped State with specific context
- Implemented soft focus position
- Factorized open/close editable cell
- Removed editable relation old components
- Broke down entity table into multiple components
- Added Recoil Scope by CellContext
- Added Recoil Scope by RowContext

* First working version

* Use a new EditableCellSoftFocusMode

* Fixed initialize soft focus

* Fixed enter mode

* Added TODO

* Fix

* Fixes

* Fix tests

* Fix lint

* Fixes

---------

Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
Charles Bochet
2023-06-28 14:06:44 +02:00
committed by GitHub
parent a6b2fd75ba
commit aa612b5fc9
58 changed files with 958 additions and 332 deletions

View File

@ -1,5 +1,5 @@
import { PersonChip } from '@/people/components/PersonChip';
import { EditableCellV2 } from '@/ui/components/editable-cell/EditableCellV2';
import { EditableCell } from '@/ui/components/editable-cell/EditableCell';
import { Company, User } from '~/generated/graphql';
import { CompanyAccountOwnerPicker } from './CompanyAccountOwnerPicker';
@ -12,7 +12,7 @@ export type OwnProps = {
export function CompanyAccountOwnerCell({ company }: OwnProps) {
return (
<EditableCellV2
<EditableCell
editModeContent={<CompanyAccountOwnerPicker company={company} />}
nonEditModeContent={
company.accountOwner?.displayName ? (

View File

@ -1,10 +1,10 @@
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
import { SingleEntitySelect } from '@/relation-picker/components/SingleEntitySelect';
import { useFilteredSearchEntityQuery } from '@/relation-picker/hooks/useFilteredSearchEntityQuery';
import { relationPickerSearchFilterScopedState } from '@/relation-picker/states/relationPickerSearchFilterScopedState';
import { EntityForSelect } from '@/relation-picker/types/EntityForSelect';
import { Entity } from '@/relation-picker/types/EntityTypeForSelect';
import { useCloseEditableCell } from '@/ui/components/editable-cell/hooks/useCloseEditableCell';
import { useRecoilScopedState } from '@/ui/hooks/useRecoilScopedState';
import { useEditableCell } from '@/ui/components/editable-cell/hooks/useCloseEditableCell';
import {
Company,
User,
@ -28,7 +28,7 @@ export function CompanyAccountOwnerPicker({ company }: OwnProps) {
);
const [updateCompany] = useUpdateCompanyMutation();
const closeEditableCell = useCloseEditableCell();
const { closeEditableCell } = useEditableCell();
const companies = useFilteredSearchEntityQuery({
queryHook: useSearchUserQuery,

View File

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

View File

@ -1,6 +1,6 @@
import { atom } from 'recoil';
export const pendingHotkeyState = atom<string | null>({
key: 'command-menu/pendingHotkeyState',
key: 'pendingHotkeyState',
default: null,
});

View File

@ -2,9 +2,9 @@ import { useCallback, useState } from 'react';
import { useRecoilState } from 'recoil';
import { v4 as uuidv4 } from 'uuid';
import { RecoilScope } from '@/recoil-scope/components/RecoilScope';
import { Column } from '@/ui/components/board/Board';
import { NewButton as UINewButton } from '@/ui/components/board/NewButton';
import { RecoilScope } from '@/ui/hooks/RecoilScope';
import {
Company,
PipelineProgressableType,

View File

@ -1,7 +1,7 @@
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
import { SingleEntitySelect } from '@/relation-picker/components/SingleEntitySelect';
import { useFilteredSearchEntityQuery } from '@/relation-picker/hooks/useFilteredSearchEntityQuery';
import { relationPickerSearchFilterScopedState } from '@/relation-picker/states/relationPickerSearchFilterScopedState';
import { useRecoilScopedState } from '@/ui/hooks/useRecoilScopedState';
import { getLogoUrlFromDomainName } from '@/utils/utils';
import {
CommentableType,

View File

@ -1,7 +1,7 @@
import CompanyChip from '@/companies/components/CompanyChip';
import { EditableCellV2 } from '@/ui/components/editable-cell/EditableCellV2';
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
import { EditableCell } from '@/ui/components/editable-cell/EditableCell';
import { isCreateModeScopedState } from '@/ui/components/editable-cell/states/isCreateModeScopedState';
import { useRecoilScopedState } from '@/ui/hooks/useRecoilScopedState';
import { getLogoUrlFromDomainName } from '@/utils/utils';
import { Company, Person } from '~/generated/graphql';
@ -18,7 +18,7 @@ export function PeopleCompanyCell({ people }: OwnProps) {
const [isCreating] = useRecoilScopedState(isCreateModeScopedState);
return (
<EditableCellV2
<EditableCell
editModeContent={
isCreating ? (
<PeopleCompanyCreateCell people={people} />

View File

@ -2,11 +2,11 @@ import { useRef, useState } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { v4 } from 'uuid';
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
import { relationPickerSearchFilterScopedState } from '@/relation-picker/states/relationPickerSearchFilterScopedState';
import { isCreateModeScopedState } from '@/ui/components/editable-cell/states/isCreateModeScopedState';
import { DoubleTextInput } from '@/ui/components/inputs/DoubleTextInput';
import { useListenClickOutsideArrayOfRef } from '@/ui/hooks/useListenClickOutsideArrayOfRef';
import { useRecoilScopedState } from '@/ui/hooks/useRecoilScopedState';
import { logError } from '@/utils/logs/logError';
import {
Person,

View File

@ -1,9 +1,9 @@
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
import { SingleEntitySelect } from '@/relation-picker/components/SingleEntitySelect';
import { useFilteredSearchEntityQuery } from '@/relation-picker/hooks/useFilteredSearchEntityQuery';
import { relationPickerSearchFilterScopedState } from '@/relation-picker/states/relationPickerSearchFilterScopedState';
import { useCloseEditableCell } from '@/ui/components/editable-cell/hooks/useCloseEditableCell';
import { useEditableCell } from '@/ui/components/editable-cell/hooks/useCloseEditableCell';
import { isCreateModeScopedState } from '@/ui/components/editable-cell/states/isCreateModeScopedState';
import { useRecoilScopedState } from '@/ui/hooks/useRecoilScopedState';
import { getLogoUrlFromDomainName } from '@/utils/utils';
import {
CommentableType,
@ -25,7 +25,7 @@ export function PeopleCompanyPicker({ people }: OwnProps) {
);
const [updatePeople] = useUpdatePeopleMutation();
const closeEditableCell = useCloseEditableCell();
const { closeEditableCell } = useEditableCell();
const companies = useFilteredSearchEntityQuery({
queryHook: useSearchCompanyQuery,

View File

@ -0,0 +1,24 @@
import { Context, useRef } from 'react';
import { v4 } from 'uuid';
import { RecoilScopeContext } from '../states/RecoilScopeContext';
export function RecoilScope({
SpecificContext,
children,
}: {
SpecificContext?: Context<string | null>;
children: React.ReactNode;
}) {
const currentScopeId = useRef(v4());
return SpecificContext ? (
<SpecificContext.Provider value={currentScopeId.current}>
{children}
</SpecificContext.Provider>
) : (
<RecoilScopeContext.Provider value={currentScopeId.current}>
{children}
</RecoilScopeContext.Provider>
);
}

View File

@ -0,0 +1,20 @@
import { Context, useContext } from 'react';
import { RecoilState, useRecoilState } from 'recoil';
import { RecoilScopeContext } from '../states/RecoilScopeContext';
export function useRecoilScopedState<StateType>(
recoilState: (param: string) => RecoilState<StateType>,
SpecificContext?: Context<string | null>,
) {
const recoilScopeId = useContext(SpecificContext ?? RecoilScopeContext);
if (!recoilScopeId)
throw new Error(
`Using a scoped atom without a RecoilScope : ${
recoilState('').key
}, verify that you are using a RecoilScope with a specific context if you intended to do so.`,
);
return useRecoilState<StateType>(recoilState(recoilScopeId));
}

View File

@ -1,7 +1,7 @@
import { useContext } from 'react';
import { RecoilState, useRecoilValue } from 'recoil';
import { RecoilScopeContext } from './RecoilScopeContext';
import { RecoilScopeContext } from '../states/RecoilScopeContext';
export function useRecoilScopedValue<T>(
recoilState: (param: string) => RecoilState<T>,

View File

@ -3,7 +3,7 @@ import { debounce } from 'lodash';
import scrollIntoView from 'scroll-into-view';
import { useUpDownHotkeys } from '@/hotkeys/hooks/useUpDownHotkeys';
import { useRecoilScopedState } from '@/ui/hooks/useRecoilScopedState';
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
import { relationPickerSearchFilterScopedState } from '../states/relationPickerSearchFilterScopedState';
import { EntityForSelect } from '../types/EntityForSelect';

View File

@ -1,11 +1,15 @@
import { ReactElement } from 'react';
import styled from '@emotion/styled';
import { useRecoilState } from 'recoil';
import { isSomeInputInEditModeState } from '../../tables/states/isSomeInputInEditModeState';
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
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;
@ -23,43 +27,48 @@ type OwnProps = {
nonEditModeContent: ReactElement;
editModeHorizontalAlign?: 'left' | 'right';
editModeVerticalPosition?: 'over' | 'below';
isEditMode?: boolean;
isCreateMode?: boolean;
onOutsideClick?: () => void;
onInsideClick?: () => void;
};
export function EditableCell({
editModeContent,
nonEditModeContent,
editModeHorizontalAlign = 'left',
editModeVerticalPosition = 'over',
isEditMode = false,
onOutsideClick,
onInsideClick,
editModeContent,
nonEditModeContent,
}: OwnProps) {
const [isSomeInputInEditMode, setIsSomeInputInEditMode] = useRecoilState(
isSomeInputInEditModeState,
);
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() {
if (!isSomeInputInEditMode) {
onInsideClick?.();
setIsSomeInputInEditMode(true);
}
openEditableCell();
setSoftFocusOnCurrentCell();
}
function handleOnOutsideClick() {
closeEditableCell();
}
const hasSoftFocus = useIsSoftFocusOnCurrentCell();
return (
<CellBaseContainer onClick={handleOnClick}>
{isEditMode ? (
<EditableCellEditMode
editModeHorizontalAlign={editModeHorizontalAlign}
editModeVerticalPosition={editModeVerticalPosition}
isEditMode={isEditMode}
onOutsideClick={onOutsideClick}
onOutsideClick={handleOnOutsideClick}
>
{editModeContent}
</EditableCellEditMode>
) : hasSoftFocus ? (
<EditableCellSoftFocusMode>
{nonEditModeContent}
</EditableCellSoftFocusMode>
) : (
<EditableCellDisplayMode>{nonEditModeContent}</EditableCellDisplayMode>
)}

View File

@ -1,7 +1,12 @@
import { ReactElement } from 'react';
import styled from '@emotion/styled';
export const EditableCellNormalModeOuterContainer = styled.div`
import { useIsSoftFocusOnCurrentCell } from './hooks/useIsSoftFocusOnCurrentCell';
type Props = {
softFocus: boolean;
};
export const EditableCellNormalModeOuterContainer = styled.div<Props>`
align-items: center;
display: flex;
height: 100%;
@ -11,17 +16,12 @@ export const EditableCellNormalModeOuterContainer = styled.div`
padding-right: ${({ theme }) => theme.spacing(1)};
width: 100%;
&:hover {
-moz-box-shadow: inset 0 0 0 1px
${({ theme }) => theme.font.color.extraLight};
-webkit-box-shadow: inset 0 0 0 1px
${({ theme }) => theme.font.color.extraLight};
background: ${({ theme }) => theme.background.transparent.secondary};
border-radius: ${({ theme }) => theme.border.radius.md};
box-shadow: inset 0 0 0 1px ${({ theme }) => theme.font.color.extraLight};
}
${(props) =>
props.softFocus
? `background: ${props.theme.background.transparent.secondary};
border-radius: ${props.theme.border.radius.md};
box-shadow: inset 0 0 0 1px ${props.theme.grayScale.gray30};`
: ''}
`;
export const EditableCellNormalModeInnerContainer = styled.div`
@ -32,13 +32,13 @@ export const EditableCellNormalModeInnerContainer = styled.div`
width: 100%;
`;
type OwnProps = {
children: ReactElement;
};
export function EditableCellDisplayMode({
children,
}: React.PropsWithChildren<unknown>) {
const hasSoftFocus = useIsSoftFocusOnCurrentCell();
export function EditableCellDisplayMode({ children }: OwnProps) {
return (
<EditableCellNormalModeOuterContainer>
<EditableCellNormalModeOuterContainer softFocus={hasSoftFocus}>
<EditableCellNormalModeInnerContainer>
{children}
</EditableCellNormalModeInnerContainer>

View File

@ -1,13 +1,12 @@
import { ReactElement, useMemo, useRef } from 'react';
import { ReactElement, useRef } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import styled from '@emotion/styled';
import { useRecoilState } from 'recoil';
import { useListenClickOutsideArrayOfRef } from '@/ui/hooks/useListenClickOutsideArrayOfRef';
import { useMoveSoftFocus } from '@/ui/tables/hooks/useMoveSoftFocus';
import { overlayBackground } from '@/ui/themes/effects';
import { debounce } from '@/utils/debounce';
import { useListenClickOutsideArrayOfRef } from '../../hooks/useListenClickOutsideArrayOfRef';
import { isSomeInputInEditModeState } from '../../tables/states/isSomeInputInEditModeState';
import { useEditableCell } from './hooks/useCloseEditableCell';
export const EditableCellEditModeContainer = styled.div<OwnProps>`
align-items: center;
@ -32,67 +31,77 @@ type OwnProps = {
children: ReactElement;
editModeHorizontalAlign?: 'left' | 'right';
editModeVerticalPosition?: 'over' | 'below';
isEditMode?: boolean;
onOutsideClick?: () => void;
onInsideClick?: () => void;
};
export function EditableCellEditMode({
editModeHorizontalAlign,
editModeVerticalPosition,
children,
isEditMode,
onOutsideClick,
}: OwnProps) {
const wrapperRef = useRef(null);
const [, setIsSomeInputInEditMode] = useRecoilState(
isSomeInputInEditModeState,
);
const debouncedSetIsSomeInputInEditMode = useMemo(() => {
return debounce(setIsSomeInputInEditMode, 20);
}, [setIsSomeInputInEditMode]);
const { closeEditableCell } = useEditableCell();
const { moveRight, moveLeft, moveDown } = useMoveSoftFocus();
useListenClickOutsideArrayOfRef([wrapperRef], () => {
if (isEditMode) {
debouncedSetIsSomeInputInEditMode(false);
onOutsideClick?.();
}
onOutsideClick?.();
});
useHotkeys(
'esc',
() => {
if (isEditMode) {
onOutsideClick?.();
debouncedSetIsSomeInputInEditMode(false);
}
},
{
preventDefault: true,
enableOnContentEditable: true,
enableOnFormTags: true,
},
[isEditMode, onOutsideClick, debouncedSetIsSomeInputInEditMode],
);
useHotkeys(
'enter',
() => {
if (isEditMode) {
onOutsideClick?.();
debouncedSetIsSomeInputInEditMode(false);
}
closeEditableCell();
moveDown();
},
{
preventDefault: true,
enableOnContentEditable: true,
enableOnFormTags: true,
preventDefault: true,
},
[isEditMode, onOutsideClick, debouncedSetIsSomeInputInEditMode],
[closeEditableCell],
);
useHotkeys(
'esc',
() => {
closeEditableCell();
},
{
enableOnContentEditable: true,
enableOnFormTags: true,
preventDefault: true,
},
[closeEditableCell],
);
useHotkeys(
'tab',
() => {
closeEditableCell();
moveRight();
},
{
enableOnContentEditable: true,
enableOnFormTags: true,
preventDefault: true,
},
[closeEditableCell, moveRight],
);
useHotkeys(
'shift+tab',
() => {
closeEditableCell();
moveLeft();
},
{
enableOnContentEditable: true,
enableOnFormTags: true,
preventDefault: true,
},
[closeEditableCell, moveRight],
);
return (

View File

@ -0,0 +1,57 @@
import React from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
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';
export function EditableCellSoftFocusMode({
children,
}: React.PropsWithChildren<unknown>) {
const { closeEditableCell, openEditableCell } = useEditableCell();
const [captureHotkeyTypeInFocus] = useRecoilState(
captureHotkeyTypeInFocusState,
);
useHotkeys(
'enter',
() => {
openEditableCell();
},
{
enableOnContentEditable: true,
enableOnFormTags: true,
preventDefault: true,
},
[closeEditableCell],
);
useHotkeys(
'*',
(keyboardEvent) => {
const isWritingText =
!isNonTextWritingKey(keyboardEvent.key) &&
!keyboardEvent.ctrlKey &&
!keyboardEvent.metaKey;
if (!isWritingText) {
return;
}
if (captureHotkeyTypeInFocus) {
return;
}
openEditableCell();
},
{
enableOnContentEditable: true,
enableOnFormTags: true,
preventDefault: false,
},
);
return <EditableCellDisplayMode>{children}</EditableCellDisplayMode>;
}

View File

@ -1,71 +0,0 @@
import { ReactElement } from 'react';
import styled from '@emotion/styled';
import { useRecoilState } from 'recoil';
import { useRecoilScopedState } from '@/ui/hooks/useRecoilScopedState';
import { isSomeInputInEditModeState } from '../../tables/states/isSomeInputInEditModeState';
import { isEditModeScopedState } from './states/isEditModeScopedState';
import { EditableCellDisplayMode } from './EditableCellDisplayMode';
import { EditableCellEditMode } from './EditableCellEditMode';
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;
nonEditModeContent: ReactElement;
editModeHorizontalAlign?: 'left' | 'right';
editModeVerticalPosition?: 'over' | 'below';
};
export function EditableCellV2({
editModeHorizontalAlign = 'left',
editModeVerticalPosition = 'over',
editModeContent,
nonEditModeContent,
}: OwnProps) {
const [isEditMode, setIsEditMode] = useRecoilScopedState(
isEditModeScopedState,
);
const [isSomeInputInEditMode, setIsSomeInputInEditMode] = useRecoilState(
isSomeInputInEditModeState,
);
function handleOnClick() {
if (!isSomeInputInEditMode) {
setIsSomeInputInEditMode(true);
setIsEditMode(true);
}
}
function handleOnOutsideClick() {
setIsEditMode(false);
}
return (
<CellBaseContainer onClick={handleOnClick}>
{isEditMode ? (
<EditableCellEditMode
editModeHorizontalAlign={editModeHorizontalAlign}
editModeVerticalPosition={editModeVerticalPosition}
isEditMode={isEditMode}
onOutsideClick={handleOnOutsideClick}
>
{editModeContent}
</EditableCellEditMode>
) : (
<EditableCellDisplayMode>{nonEditModeContent}</EditableCellDisplayMode>
)}
</CellBaseContainer>
);
}

View File

@ -1,19 +1,43 @@
import { useCallback } from 'react';
import { useRecoilState } from 'recoil';
import { useRecoilCallback } from 'recoil';
import { useRecoilScopedState } from '@/ui/hooks/useRecoilScopedState';
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
import { isSomeInputInEditModeState } from '@/ui/tables/states/isSomeInputInEditModeState';
import { isEditModeScopedState } from '../states/isEditModeScopedState';
export function useCloseEditableCell() {
const [, setIsSomeInputInEditMode] = useRecoilState(
isSomeInputInEditModeState,
);
export function useEditableCell() {
const [, setIsEditMode] = useRecoilScopedState(isEditModeScopedState);
return useCallback(() => {
setIsSomeInputInEditMode(false);
setIsEditMode(false);
}, [setIsEditMode, setIsSomeInputInEditMode]);
const closeEditableCell = useRecoilCallback(
({ set }) =>
async () => {
setIsEditMode(false);
await new Promise((resolve) => setTimeout(resolve, 20));
set(isSomeInputInEditModeState, false);
},
[setIsEditMode],
);
const openEditableCell = useRecoilCallback(
({ snapshot, set }) =>
() => {
const isSomeInputInEditMode = snapshot
.getLoadable(isSomeInputInEditModeState)
.valueOrThrow();
if (!isSomeInputInEditMode) {
set(isSomeInputInEditModeState, true);
setIsEditMode(true);
}
},
[setIsEditMode],
);
return {
closeEditableCell,
openEditableCell,
};
}

View File

@ -0,0 +1,36 @@
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';
export function useIsSoftFocusOnCurrentCell() {
const [currentRowNumber] = useRecoilScopedState(
currentRowNumberScopedState,
RowContext,
);
const [currentColumnNumber] = useRecoilScopedState(
currentColumnNumberScopedState,
CellContext,
);
const currentTablePosition: TablePosition = useMemo(
() => ({
column: currentColumnNumber,
row: currentRowNumber,
}),
[currentColumnNumber, currentRowNumber],
);
const isSoftFocusOnCell = useRecoilValue(
isSoftFocusOnCellFamilyState(currentTablePosition),
);
return isSoftFocusOnCell;
}

View File

@ -0,0 +1,34 @@
import { useCallback, useMemo } from 'react';
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 { RowContext } from '@/ui/tables/states/RowContext';
import { TablePosition } from '@/ui/tables/types/TablePosition';
export function useSetSoftFocusOnCurrentCell() {
const setSoftFocusPosition = useSetSoftFocusPosition();
const [currentRowNumber] = useRecoilScopedState(
currentRowNumberScopedState,
RowContext,
);
const [currentColumnNumber] = useRecoilScopedState(
currentColumnNumberScopedState,
CellContext,
);
const currentTablePosition: TablePosition = useMemo(
() => ({
column: currentColumnNumber,
row: currentRowNumber,
}),
[currentColumnNumber, currentRowNumber],
);
return useCallback(() => {
setSoftFocusPosition(currentTablePosition);
}, [setSoftFocusPosition, currentTablePosition]);
}

View File

@ -50,7 +50,6 @@ function EditableChip({
}: EditableChipProps) {
const inputRef = useRef<HTMLInputElement>(null);
const [inputValue, setInputValue] = useState(value);
const [isEditMode, setIsEditMode] = useState(false);
const handleRightEndContentClick = (
event: React.MouseEvent<HTMLDivElement>,
@ -60,9 +59,6 @@ function EditableChip({
return (
<EditableCell
onOutsideClick={() => setIsEditMode(false)}
onInsideClick={() => setIsEditMode(true)}
isEditMode={isEditMode}
editModeHorizontalAlign={editModeHorizontalAlign}
editModeContent={
<StyledInplaceInput

View File

@ -38,7 +38,6 @@ export function EditableDate({
editModeHorizontalAlign,
}: EditableDateProps) {
const [inputValue, setInputValue] = useState(value);
const [isEditMode, setIsEditMode] = useState(false);
type DivProps = React.HTMLProps<HTMLDivElement>;
@ -60,9 +59,6 @@ export function EditableDate({
return (
<EditableCell
isEditMode={isEditMode}
onOutsideClick={() => setIsEditMode(false)}
onInsideClick={() => setIsEditMode(true)}
editModeHorizontalAlign={editModeHorizontalAlign}
editModeContent={
<StyledContainer>

View File

@ -1,4 +1,4 @@
import { ChangeEvent, ReactElement, useRef, useState } from 'react';
import { ChangeEvent, ReactElement, useRef } from 'react';
import styled from '@emotion/styled';
import { textInputStyle } from '@/ui/themes/effects';
@ -42,13 +42,9 @@ export function EditableDoubleText({
onChange,
}: OwnProps) {
const firstValueInputRef = useRef<HTMLInputElement>(null);
const [isEditMode, setIsEditMode] = useState(false);
return (
<EditableCell
onInsideClick={() => setIsEditMode(true)}
onOutsideClick={() => setIsEditMode(false)}
isEditMode={isEditMode}
editModeContent={
<StyledContainer>
<StyledEditInplaceInput

View File

@ -13,10 +13,6 @@ type OwnProps = {
changeHandler: (updated: string) => void;
};
type StyledEditModeProps = {
isEditMode: boolean;
};
const StyledRawLink = styled(RawLink)`
overflow: hidden;
@ -28,7 +24,7 @@ const StyledRawLink = styled(RawLink)`
`;
// TODO: refactor
const StyledEditInplaceInput = styled.input<StyledEditModeProps>`
const StyledEditInplaceInput = styled.input`
margin: 0;
width: 100%;
${textInputStyle}
@ -37,17 +33,12 @@ const StyledEditInplaceInput = styled.input<StyledEditModeProps>`
export function EditablePhone({ value, placeholder, changeHandler }: OwnProps) {
const inputRef = useRef<HTMLInputElement>(null);
const [inputValue, setInputValue] = useState(value);
const [isEditMode, setIsEditMode] = useState(false);
return (
<EditableCell
isEditMode={isEditMode}
onOutsideClick={() => setIsEditMode(false)}
onInsideClick={() => setIsEditMode(true)}
editModeContent={
<StyledEditInplaceInput
autoFocus
isEditMode={isEditMode}
placeholder={placeholder || ''}
ref={inputRef}
value={inputValue}

View File

@ -12,12 +12,8 @@ type OwnProps = {
editModeHorizontalAlign?: 'left' | 'right';
};
type StyledEditModeProps = {
isEditMode: boolean;
};
// TODO: refactor
const StyledInplaceInput = styled.input<StyledEditModeProps>`
const StyledInplaceInput = styled.input`
margin: 0;
width: 100%;
${textInputStyle}
@ -38,17 +34,12 @@ export function EditableText({
}: OwnProps) {
const inputRef = useRef<HTMLInputElement>(null);
const [inputValue, setInputValue] = useState(content);
const [isEditMode, setIsEditMode] = useState(false);
return (
<EditableCell
isEditMode={isEditMode}
onOutsideClick={() => setIsEditMode(false)}
onInsideClick={() => setIsEditMode(true)}
editModeHorizontalAlign={editModeHorizontalAlign}
editModeContent={
<StyledInplaceInput
isEditMode={isEditMode}
placeholder={placeholder || ''}
autoFocus
ref={inputRef}

View File

@ -6,7 +6,7 @@ import {
getCoreRowModel,
useReactTable,
} from '@tanstack/react-table';
import { useRecoilState, useSetRecoilState } from 'recoil';
import { useRecoilState } from 'recoil';
import {
FilterConfigType,
@ -16,12 +16,13 @@ import {
SelectedSortType,
SortType,
} from '@/filters-and-sorts/interfaces/sorts/interface';
import { contextMenuPositionState } from '@/ui/tables/states/contextMenuPositionState';
import { RecoilScope } from '@/recoil-scope/components/RecoilScope';
import { RowContext } from '@/ui/tables/states/RowContext';
import { useResetTableRowSelection } from '../../tables/hooks/useResetTableRowSelection';
import { currentRowSelectionState } from '../../tables/states/rowSelectionState';
import { TableHeader } from './table-header/TableHeader';
import { EntityTableRow } from './EntityTableRow';
type OwnProps<TData extends { id: string }, SortField> = {
data: Array<TData>;
@ -100,11 +101,6 @@ const StyledTableScrollableContainer = styled.div`
overflow: auto;
`;
const StyledRow = styled.tr<{ selected: boolean }>`
background: ${(props) =>
props.selected ? props.theme.background.secondary : 'none'};
`;
export function EntityTable<TData extends { id: string }, SortField>({
data,
columns,
@ -118,13 +114,6 @@ export function EntityTable<TData extends { id: string }, SortField>({
const [currentRowSelection, setCurrentRowSelection] = useRecoilState(
currentRowSelectionState,
);
const setContextMenuPosition = useSetRecoilState(contextMenuPositionState);
const resetTableRowSelection = useResetTableRowSelection();
React.useEffect(() => {
resetTableRowSelection();
}, [resetTableRowSelection]);
const table = useReactTable<TData>({
data,
@ -138,16 +127,6 @@ export function EntityTable<TData extends { id: string }, SortField>({
getRowId: (row) => row.id,
});
function handleContextMenu(event: React.MouseEvent, id: string) {
event.preventDefault();
setCurrentRowSelection((prev) => ({ ...prev, [id]: true }));
setContextMenuPosition({
x: event.clientX,
y: event.clientY,
});
}
return (
<StyledTableWithHeader>
<TableHeader
@ -186,33 +165,9 @@ export function EntityTable<TData extends { id: string }, SortField>({
</thead>
<tbody>
{table.getRowModel().rows.map((row, index) => (
<StyledRow
key={row.id}
data-testid={`row-id-${row.index}`}
selected={!!currentRowSelection[row.id]}
>
{row.getVisibleCells().map((cell) => {
return (
<td
key={cell.id + row.original.id}
onContextMenu={(event) =>
handleContextMenu(event, row.original.id)
}
style={{
width: cell.column.getSize(),
minWidth: cell.column.getSize(),
maxWidth: cell.column.getSize(),
}}
>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</td>
);
})}
<td></td>
</StyledRow>
<RecoilScope SpecificContext={RowContext} key={row.id}>
<EntityTableRow row={row} index={index} />
</RecoilScope>
))}
</tbody>
</StyledTable>

View File

@ -0,0 +1,56 @@
import { useEffect } from 'react';
import { flexRender } from '@tanstack/react-table';
import { Cell, Row } from '@tanstack/table-core';
import { useRecoilState, useSetRecoilState } from 'recoil';
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
import { CellContext } from '@/ui/tables/states/CellContext';
import { contextMenuPositionState } from '@/ui/tables/states/contextMenuPositionState';
import { currentColumnNumberScopedState } from '@/ui/tables/states/currentColumnNumberScopedState';
import { currentRowSelectionState } from '@/ui/tables/states/rowSelectionState';
export function EntityTableCell<TData extends { id: string }>({
row,
cell,
cellIndex,
}: {
row: Row<TData>;
cell: Cell<TData, unknown>;
cellIndex: number;
}) {
const [, setCurrentRowSelection] = useRecoilState(currentRowSelectionState);
const [, setCurrentColumnNumber] = useRecoilScopedState(
currentColumnNumberScopedState,
CellContext,
);
useEffect(() => {
setCurrentColumnNumber(cellIndex);
}, [cellIndex, setCurrentColumnNumber]);
const setContextMenuPosition = useSetRecoilState(contextMenuPositionState);
function handleContextMenu(event: React.MouseEvent, id: string) {
event.preventDefault();
setCurrentRowSelection((prev) => ({ ...prev, [id]: true }));
setContextMenuPosition({
x: event.clientX,
y: event.clientY,
});
}
return (
<td
onContextMenu={(event) => handleContextMenu(event, row.original.id)}
style={{
width: cell.column.getSize(),
minWidth: cell.column.getSize(),
maxWidth: cell.column.getSize(),
}}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
);
}

View File

@ -0,0 +1,63 @@
import { useEffect } from 'react';
import styled from '@emotion/styled';
import { Row } from '@tanstack/table-core';
import { useRecoilState } from 'recoil';
import { RecoilScope } from '@/recoil-scope/components/RecoilScope';
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
import { CellContext } from '@/ui/tables/states/CellContext';
import { currentRowNumberScopedState } from '@/ui/tables/states/currentRowNumberScopedState';
import { RowContext } from '@/ui/tables/states/RowContext';
import { currentRowSelectionState } from '@/ui/tables/states/rowSelectionState';
import { EntityTableCell } from './EntityTableCell';
const StyledRow = styled.tr<{ selected: boolean }>`
background: ${(props) =>
props.selected ? props.theme.background.secondary : 'none'};
`;
export function EntityTableRow<TData extends { id: string }>({
row,
index,
}: {
row: Row<TData>;
index: number;
}) {
const [currentRowSelection] = useRecoilState(currentRowSelectionState);
const [, setCurrentRowNumber] = useRecoilScopedState(
currentRowNumberScopedState,
RowContext,
);
useEffect(() => {
setCurrentRowNumber(index);
}, [index, setCurrentRowNumber]);
return (
<StyledRow
key={row.id}
data-testid={`row-id-${row.index}`}
selected={!!currentRowSelection[row.id]}
>
{row.getVisibleCells().map((cell, cellIndex) => {
return (
<RecoilScope
SpecificContext={CellContext}
key={cell.id + row.original.id}
>
<RecoilScope>
<EntityTableCell<TData>
row={row}
cell={cell}
cellIndex={cellIndex}
/>
</RecoilScope>
</RecoilScope>
);
})}
<td></td>
</StyledRow>
);
}

View File

@ -0,0 +1,19 @@
import { useInitializeEntityTable } from '@/ui/tables/hooks/useInitializeEntityTable';
import { useMapKeyboardToSoftFocus } from '@/ui/tables/hooks/useMapKeyboardToSoftFocus';
export function HooksEntityTable({
numberOfColumns,
numberOfRows,
}: {
numberOfColumns: number;
numberOfRows: number;
}) {
useMapKeyboardToSoftFocus();
useInitializeEntityTable({
numberOfColumns,
numberOfRows,
});
return <></>;
}

View File

@ -1,6 +1,8 @@
import { ReactNode, useRef } from 'react';
import styled from '@emotion/styled';
import { useRecoilState } from 'recoil';
import { captureHotkeyTypeInFocusState } from '@/hotkeys/states/captureHotkeyTypeInFocusState';
import { IconChevronDown } from '@/ui/icons/index';
import { overlayBackground, textInputStyle } from '@/ui/themes/effects';
@ -159,11 +161,17 @@ function DropdownButton({
setIsUnfolded,
resetState,
}: OwnProps) {
const [, setCaptureHotkeyTypeInFocus] = useRecoilState(
captureHotkeyTypeInFocusState,
);
const onButtonClick = () => {
setIsUnfolded && setIsUnfolded(!isUnfolded);
setCaptureHotkeyTypeInFocus(!isUnfolded);
};
const onOutsideClick = () => {
setCaptureHotkeyTypeInFocus(false);
setIsUnfolded && setIsUnfolded(false);
resetState && resetState();
};

View File

@ -1,11 +1,9 @@
import React from 'react';
import { ApolloProvider } from '@apollo/client';
import type { Meta, StoryObj } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library';
import { IconList } from '@/ui/icons/index';
import { FullHeightStorybookLayout } from '~/testing/FullHeightStorybookLayout';
import { mockedClient } from '~/testing/mockedClient';
import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
import { availableFilters } from '../../../../../../pages/companies/companies-filters';
import { availableSorts } from '../../../../../../pages/companies/companies-sorts';
@ -20,32 +18,24 @@ export default meta;
type Story = StoryObj<typeof TableHeader>;
export const Empty: Story = {
render: () => (
<ApolloProvider client={mockedClient}>
<FullHeightStorybookLayout>
<TableHeader
viewName="ViewName"
viewIcon={<IconList />}
availableSorts={availableSorts}
availableFilters={availableFilters}
/>
</FullHeightStorybookLayout>
</ApolloProvider>
render: getRenderWrapperForComponent(
<TableHeader
viewName="ViewName"
viewIcon={<IconList />}
availableSorts={availableSorts}
availableFilters={availableFilters}
/>,
),
};
export const WithSortsAndFilters: Story = {
render: () => (
<ApolloProvider client={mockedClient}>
<FullHeightStorybookLayout>
<TableHeader
viewName="ViewName"
viewIcon={<IconList />}
availableSorts={availableSorts}
availableFilters={availableFilters}
/>
</FullHeightStorybookLayout>
</ApolloProvider>
render: getRenderWrapperForComponent(
<TableHeader
viewName="ViewName"
viewIcon={<IconList />}
availableSorts={availableSorts}
availableFilters={availableFilters}
/>,
),
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);

View File

@ -1,14 +0,0 @@
import { useState } from 'react';
import { v4 } from 'uuid';
import { RecoilScopeContext } from './RecoilScopeContext';
export function RecoilScope({ children }: { children: React.ReactNode }) {
const [currentScopeId] = useState(v4());
return (
<RecoilScopeContext.Provider value={currentScopeId}>
{children}
</RecoilScopeContext.Provider>
);
}

View File

@ -1,17 +0,0 @@
import { useContext } from 'react';
import { RecoilState, useRecoilState } from 'recoil';
import { RecoilScopeContext } from './RecoilScopeContext';
export function useRecoilScopedState<T>(
recoilState: (param: string) => RecoilState<T>,
) {
const recoilScopeId = useContext(RecoilScopeContext);
if (!recoilScopeId)
throw new Error(
`Using a scoped atom without a RecoilScope : ${recoilState('').key}`,
);
return useRecoilState<T>(recoilState(recoilScopeId));
}

View File

@ -0,0 +1 @@
export const TABLE_MIN_COLUMN_NUMBER_BECAUSE_OF_CHECKBOX_COLUMN = 1;

View File

@ -0,0 +1,42 @@
import { useEffect } from 'react';
import { useRecoilState } from 'recoil';
import { TABLE_MIN_COLUMN_NUMBER_BECAUSE_OF_CHECKBOX_COLUMN } from '../constants';
import { entityTableDimensionsState } from '../states/entityTableDimensionsState';
import { useResetTableRowSelection } from './useResetTableRowSelection';
import { useSetSoftFocusPosition } from './useSetSoftFocusPosition';
export type TableDimensions = {
numberOfRows: number;
numberOfColumns: number;
};
export function useInitializeEntityTable({
numberOfRows,
numberOfColumns,
}: TableDimensions) {
const resetTableRowSelection = useResetTableRowSelection();
useEffect(() => {
resetTableRowSelection();
}, [resetTableRowSelection]);
const [, setTableDimensions] = useRecoilState(entityTableDimensionsState);
useEffect(() => {
setTableDimensions({
numberOfColumns,
numberOfRows,
});
}, [numberOfRows, numberOfColumns, setTableDimensions]);
const setSoftFocusPosition = useSetSoftFocusPosition();
useEffect(() => {
setSoftFocusPosition({
row: 0,
column: TABLE_MIN_COLUMN_NUMBER_BECAUSE_OF_CHECKBOX_COLUMN,
});
}, [setSoftFocusPosition]);
}

View File

@ -0,0 +1,72 @@
import { useHotkeys } from 'react-hotkeys-hook';
import { useRecoilState } from 'recoil';
import { isSomeInputInEditModeState } from '../states/isSomeInputInEditModeState';
import { useMoveSoftFocus } from './useMoveSoftFocus';
export function useMapKeyboardToSoftFocus() {
const { moveDown, moveLeft, moveRight, moveUp } = useMoveSoftFocus();
const [isSomeInputInEditMode] = useRecoilState(isSomeInputInEditModeState);
useHotkeys(
'up, shift+enter',
() => {
if (!isSomeInputInEditMode) {
moveUp();
}
},
[moveUp, isSomeInputInEditMode],
{
preventDefault: true,
enableOnContentEditable: true,
enableOnFormTags: true,
},
);
useHotkeys(
'down',
() => {
if (!isSomeInputInEditMode) {
moveDown();
}
},
[moveDown, isSomeInputInEditMode],
{
preventDefault: true,
enableOnContentEditable: true,
enableOnFormTags: true,
},
);
useHotkeys(
['left', 'shift+tab'],
() => {
if (!isSomeInputInEditMode) {
moveLeft();
}
},
[moveLeft, isSomeInputInEditMode],
{
preventDefault: true,
enableOnContentEditable: true,
enableOnFormTags: true,
},
);
useHotkeys(
['right', 'tab'],
() => {
if (!isSomeInputInEditMode) {
moveRight();
}
},
[moveRight, isSomeInputInEditMode],
{
preventDefault: true,
enableOnContentEditable: true,
enableOnFormTags: true,
},
);
}

View File

@ -0,0 +1,161 @@
import { useRecoilCallback } from 'recoil';
import { TABLE_MIN_COLUMN_NUMBER_BECAUSE_OF_CHECKBOX_COLUMN } from '../constants';
import { numberOfTableColumnsSelectorState } from '../states/numberOfTableColumnsSelectorState';
import { numberOfTableRowsSelectorState } from '../states/numberOfTableRowsSelectorState';
import { softFocusPositionState } from '../states/softFocusPositionState';
import { useSetSoftFocusPosition } from './useSetSoftFocusPosition';
// TODO: stories
export function useMoveSoftFocus() {
const setSoftFocusPosition = useSetSoftFocusPosition();
const moveUp = useRecoilCallback(
({ snapshot }) =>
() => {
const softFocusPosition = snapshot
.getLoadable(softFocusPositionState)
.valueOrThrow();
let newRowNumber = softFocusPosition.row - 1;
if (newRowNumber < 0) {
newRowNumber = 0;
}
setSoftFocusPosition({
...softFocusPosition,
row: newRowNumber,
});
},
[setSoftFocusPosition],
);
const moveDown = useRecoilCallback(
({ snapshot }) =>
() => {
const softFocusPosition = snapshot
.getLoadable(softFocusPositionState)
.valueOrThrow();
const numberOfTableRows = snapshot
.getLoadable(numberOfTableRowsSelectorState)
.valueOrThrow();
let newRowNumber = softFocusPosition.row + 1;
if (newRowNumber >= numberOfTableRows) {
newRowNumber = numberOfTableRows - 1;
}
setSoftFocusPosition({
...softFocusPosition,
row: newRowNumber,
});
},
[setSoftFocusPosition],
);
const moveRight = useRecoilCallback(
({ snapshot }) =>
() => {
const softFocusPosition = snapshot
.getLoadable(softFocusPositionState)
.valueOrThrow();
const numberOfTableColumns = snapshot
.getLoadable(numberOfTableColumnsSelectorState)
.valueOrThrow();
const numberOfTableRows = snapshot
.getLoadable(numberOfTableRowsSelectorState)
.valueOrThrow();
const currentColumnNumber = softFocusPosition.column;
const currentRowNumber = softFocusPosition.row;
const isLastRowAndLastColumn =
currentColumnNumber === numberOfTableColumns - 1 &&
currentRowNumber === numberOfTableRows - 1;
const isLastColumnButNotLastRow =
currentColumnNumber === numberOfTableColumns - 1 &&
currentRowNumber !== numberOfTableRows - 1;
const isNotLastColumn =
currentColumnNumber !== numberOfTableColumns - 1;
if (isLastRowAndLastColumn) {
return;
}
if (isNotLastColumn) {
setSoftFocusPosition({
row: currentRowNumber,
column: currentColumnNumber + 1,
});
} else if (isLastColumnButNotLastRow) {
setSoftFocusPosition({
row: currentRowNumber + 1,
column: TABLE_MIN_COLUMN_NUMBER_BECAUSE_OF_CHECKBOX_COLUMN,
});
}
},
[setSoftFocusPosition],
);
const moveLeft = useRecoilCallback(
({ snapshot }) =>
() => {
const softFocusPosition = snapshot
.getLoadable(softFocusPositionState)
.valueOrThrow();
const numberOfTableColumns = snapshot
.getLoadable(numberOfTableColumnsSelectorState)
.valueOrThrow();
const currentColumnNumber = softFocusPosition.column;
const currentRowNumber = softFocusPosition.row;
const isFirstRowAndFirstColumn =
currentColumnNumber ===
TABLE_MIN_COLUMN_NUMBER_BECAUSE_OF_CHECKBOX_COLUMN &&
currentRowNumber === 0;
const isFirstColumnButNotFirstRow =
currentColumnNumber ===
TABLE_MIN_COLUMN_NUMBER_BECAUSE_OF_CHECKBOX_COLUMN &&
currentRowNumber > 0;
const isNotFirstColumn =
currentColumnNumber >
TABLE_MIN_COLUMN_NUMBER_BECAUSE_OF_CHECKBOX_COLUMN;
if (isFirstRowAndFirstColumn) {
return;
}
if (isNotFirstColumn) {
setSoftFocusPosition({
row: currentRowNumber,
column: currentColumnNumber - 1,
});
} else if (isFirstColumnButNotFirstRow) {
setSoftFocusPosition({
row: currentRowNumber - 1,
column: numberOfTableColumns - 1,
});
}
},
[setSoftFocusPosition, TABLE_MIN_COLUMN_NUMBER_BECAUSE_OF_CHECKBOX_COLUMN],
);
return {
moveDown,
moveLeft,
moveRight,
moveUp,
};
}

View File

@ -0,0 +1,21 @@
import { useRecoilCallback } from 'recoil';
import { isSoftFocusOnCellFamilyState } from '../states/isSoftFocusOnCellFamilyState';
import { softFocusPositionState } from '../states/softFocusPositionState';
import { TablePosition } from '../types/TablePosition';
export function useSetSoftFocusPosition() {
return useRecoilCallback(({ set, snapshot }) => {
return (newPosition: TablePosition) => {
const currentPosition = snapshot
.getLoadable(softFocusPositionState)
.valueOrThrow();
set(isSoftFocusOnCellFamilyState(currentPosition), false);
set(softFocusPositionState, newPosition);
set(isSoftFocusOnCellFamilyState(newPosition), true);
};
}, []);
}

View File

@ -0,0 +1,3 @@
import { createContext } from 'react';
export const CellContext = createContext<string | null>(null);

View File

@ -0,0 +1,3 @@
import { createContext } from 'react';
export const RowContext = createContext<string | null>(null);

View File

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

View File

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

View File

@ -0,0 +1,11 @@
import { atom } from 'recoil';
import { TableDimensions } from '../hooks/useInitializeEntityTable';
export const entityTableDimensionsState = atom<TableDimensions>({
key: 'entityTableDimensionsState',
default: {
numberOfRows: 0,
numberOfColumns: 0,
},
});

View File

@ -0,0 +1,8 @@
import { atomFamily } from 'recoil';
import { TablePosition } from '../types/TablePosition';
export const isSoftFocusOnCellFamilyState = atomFamily<boolean, TablePosition>({
key: 'isSoftFocusOnCellFamilyState',
default: false,
});

View File

@ -0,0 +1,12 @@
import { selector } from 'recoil';
import { entityTableDimensionsState } from './entityTableDimensionsState';
export const numberOfTableColumnsSelectorState = selector<number>({
key: 'numberOfTableColumnsState',
get: ({ get }) => {
const { numberOfColumns } = get(entityTableDimensionsState);
return numberOfColumns;
},
});

View File

@ -0,0 +1,12 @@
import { selector } from 'recoil';
import { entityTableDimensionsState } from './entityTableDimensionsState';
export const numberOfTableRowsSelectorState = selector<number>({
key: 'numberOfTableRowsState',
get: ({ get }) => {
const { numberOfRows } = get(entityTableDimensionsState);
return numberOfRows;
},
});

View File

@ -0,0 +1,11 @@
import { atom } from 'recoil';
import { TablePosition } from '../types/TablePosition';
export const softFocusPositionState = atom<TablePosition>({
key: 'softFocusPositionState',
default: {
row: 0,
column: 1,
},
});

View File

@ -0,0 +1,4 @@
export type TablePosition = {
numberOfRows: number;
numberOfColumns: number;
};

View File

@ -0,0 +1,4 @@
export type TablePosition = {
row: number;
column: number;
};

View File

@ -0,0 +1,7 @@
import { TablePosition } from '../TablePosition';
export function isTablePosition(value: any): value is TablePosition {
return (
value && typeof value.row === 'number' && typeof value.column === 'number'
);
}

View File

@ -0,0 +1,57 @@
export function isNonTextWritingKey(key: string) {
const nonTextWritingKeys = [
'Enter',
'Tab',
'Shift',
'Escape',
'ArrowUp',
'ArrowDown',
'ArrowLeft',
'ArrowRight',
'Delete',
'Backspace',
'F1',
'F2',
'F3',
'F4',
'F5',
'F6',
'F7',
'F8',
'F9',
'F10',
'F11',
'F12',
'Meta',
'Alt',
'Control',
'CapsLock',
'NumLock',
'ScrollLock',
'Pause',
'Insert',
'Home',
'PageUp',
'Delete',
'End',
'PageDown',
'ContextMenu',
'PrintScreen',
'BrowserBack',
'BrowserForward',
'BrowserRefresh',
'BrowserStop',
'BrowserSearch',
'BrowserFavorites',
'BrowserHome',
'VolumeMute',
'VolumeDown',
'VolumeUp',
'MediaTrackNext',
'MediaTrackPrevious',
'MediaStop',
'MediaPlayPause',
];
return nonTextWritingKeys.includes(key);
}

View File

@ -17,6 +17,7 @@ import {
import { SelectedFilterType } from '@/filters-and-sorts/interfaces/filters/interface';
import { EntityTableActionBar } from '@/ui/components/table/action-bar/EntityTableActionBar';
import { EntityTable } from '@/ui/components/table/EntityTable';
import { HooksEntityTable } from '@/ui/components/table/HooksEntityTable';
import { IconBuildingSkyscraper } from '@/ui/icons/index';
import { IconList } from '@/ui/icons/index';
import { WithTopBarContainer } from '@/ui/layout/containers/WithTopBarContainer';
@ -85,6 +86,10 @@ export function Companies() {
>
<>
<StyledCompaniesContainer>
<HooksEntityTable
numberOfColumns={companiesColumns.length}
numberOfRows={companies.length}
/>
<EntityTable
data={companies}
columns={companiesColumns}

View File

@ -6,7 +6,6 @@ import { CompanyEditableNameChipCell } from '@/companies/components/CompanyEdita
import { EditableDate } from '@/ui/components/editable-cell/types/EditableDate';
import { EditableText } from '@/ui/components/editable-cell/types/EditableText';
import { ColumnHead } from '@/ui/components/table/ColumnHead';
import { RecoilScope } from '@/ui/hooks/RecoilScope';
import {
IconBuildingSkyscraper,
IconCalendarEvent,
@ -143,9 +142,7 @@ export const useCompaniesColumns = () => {
/>
),
cell: (props) => (
<RecoilScope>
<CompanyAccountOwnerCell company={props.row.original} />
</RecoilScope>
<CompanyAccountOwnerCell company={props.row.original} />
),
}),
];

View File

@ -17,6 +17,7 @@ import {
} from '@/people/services';
import { EntityTableActionBar } from '@/ui/components/table/action-bar/EntityTableActionBar';
import { EntityTable } from '@/ui/components/table/EntityTable';
import { HooksEntityTable } from '@/ui/components/table/HooksEntityTable';
import { IconList, IconUser } from '@/ui/icons/index';
import { WithTopBarContainer } from '@/ui/layout/containers/WithTopBarContainer';
import {
@ -76,6 +77,7 @@ export function People() {
const peopleColumns = usePeopleColumns();
const theme = useTheme();
return (
<WithTopBarContainer
title="People"
@ -84,6 +86,10 @@ export function People() {
>
<>
<StyledPeopleContainer>
<HooksEntityTable
numberOfColumns={peopleColumns.length}
numberOfRows={people.length}
/>
<EntityTable
data={people}
columns={peopleColumns}

View File

@ -52,11 +52,15 @@ export const InteractWithManyRows: Story = {
await sleep(25);
const secondRowEmailCellFocused = await canvas.findByText(
mockedPeopleData[1].email,
);
expect(
canvas.queryByTestId('editable-cell-edit-mode-container'),
).toBeNull();
await userEvent.click(secondRowEmailCell);
await userEvent.click(secondRowEmailCellFocused);
await sleep(25);

View File

@ -7,7 +7,6 @@ import { EditableDate } from '@/ui/components/editable-cell/types/EditableDate';
import { EditablePhone } from '@/ui/components/editable-cell/types/EditablePhone';
import { EditableText } from '@/ui/components/editable-cell/types/EditableText';
import { ColumnHead } from '@/ui/components/table/ColumnHead';
import { RecoilScope } from '@/ui/hooks/RecoilScope';
import {
IconBuildingSkyscraper,
IconCalendarEvent,
@ -80,11 +79,7 @@ export const usePeopleColumns = () => {
viewIcon={<IconBuildingSkyscraper size={16} />}
/>
),
cell: (props) => (
<RecoilScope>
<PeopleCompanyCell people={props.row.original} />
</RecoilScope>
),
cell: (props) => <PeopleCompanyCell people={props.row.original} />,
size: 150,
}),
columnHelper.accessor('phone', {