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:
1
front/src/modules/ui/tables/constants/index.ts
Normal file
1
front/src/modules/ui/tables/constants/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export const TABLE_MIN_COLUMN_NUMBER_BECAUSE_OF_CHECKBOX_COLUMN = 1;
|
||||
@ -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]);
|
||||
}
|
||||
@ -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,
|
||||
},
|
||||
);
|
||||
}
|
||||
161
front/src/modules/ui/tables/hooks/useMoveSoftFocus.ts
Normal file
161
front/src/modules/ui/tables/hooks/useMoveSoftFocus.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
21
front/src/modules/ui/tables/hooks/useSetSoftFocusPosition.ts
Normal file
21
front/src/modules/ui/tables/hooks/useSetSoftFocusPosition.ts
Normal 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);
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
3
front/src/modules/ui/tables/states/CellContext.ts
Normal file
3
front/src/modules/ui/tables/states/CellContext.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
export const CellContext = createContext<string | null>(null);
|
||||
3
front/src/modules/ui/tables/states/RowContext.ts
Normal file
3
front/src/modules/ui/tables/states/RowContext.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
export const RowContext = createContext<string | null>(null);
|
||||
@ -0,0 +1,6 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
export const currentColumnNumberScopedState = atomFamily<number, string>({
|
||||
key: 'currentColumnNumberScopedState',
|
||||
default: 0,
|
||||
});
|
||||
@ -0,0 +1,6 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
export const currentRowNumberScopedState = atomFamily<number, string>({
|
||||
key: 'currentRowNumberScopedState',
|
||||
default: 0,
|
||||
});
|
||||
@ -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,
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,8 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
import { TablePosition } from '../types/TablePosition';
|
||||
|
||||
export const isSoftFocusOnCellFamilyState = atomFamily<boolean, TablePosition>({
|
||||
key: 'isSoftFocusOnCellFamilyState',
|
||||
default: false,
|
||||
});
|
||||
@ -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;
|
||||
},
|
||||
});
|
||||
@ -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;
|
||||
},
|
||||
});
|
||||
11
front/src/modules/ui/tables/states/softFocusPositionState.ts
Normal file
11
front/src/modules/ui/tables/states/softFocusPositionState.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
4
front/src/modules/ui/tables/types/TableDimensions.ts
Normal file
4
front/src/modules/ui/tables/types/TableDimensions.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export type TablePosition = {
|
||||
numberOfRows: number;
|
||||
numberOfColumns: number;
|
||||
};
|
||||
4
front/src/modules/ui/tables/types/TablePosition.ts
Normal file
4
front/src/modules/ui/tables/types/TablePosition.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export type TablePosition = {
|
||||
row: number;
|
||||
column: number;
|
||||
};
|
||||
@ -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'
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user