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

@ -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);