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

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