Record Table Row Navigation (#11879)

# Record Table Row Navigation

This PR improves the table accessibility by adding a row navigation and
new shortcuts to the table. Closes #896.

# Introduce focused active row states on the table

This PR implements the focused and active row states feature for the
record table, allowing users to navigate through the table with keyboard
arrows and providing visual feedback for selection.

## Implementation details:

- Added new component states to track focused and active row positions
and states.

- Implemented dedicated hooks for row state management

- Updated UI styling for active and focused rows:
  - Applied blue border (Adaptive Colors Blue 3)
  - Added highlight background (Accent Quaternary)
  - Added styling for focused rows to clearly indicate selection state

- Added row state cleanup:
- `RecordTableDeactivateRecordTableRowEffect` component to reset states
  - Added row state reset logic upon navigation

## Bug fixes
- Fixed record table unselection in the page change effect
- Fixed a hack introduced by
https://github.com/twentyhq/twenty/pull/8489 which duplicated the last
table column

# Shortcuts

## Arrow keys and J+K navigation


https://github.com/user-attachments/assets/6b46f6b5-cd98-4053-aaef-f8bf2b9584b5


## Record selection with X


https://github.com/user-attachments/assets/44ab7397-e00c-4dfe-8dd1-b3ffc53b3e5f


## Enter allows for cell navigation, Escape goes back to row navigation


https://github.com/user-attachments/assets/890d7e25-2d81-47e3-972f-043a1879b8cc


## Command + Enter opens the record



https://github.com/user-attachments/assets/cf8cdbd5-7cf0-4d78-909f-dc6be88b9e25
This commit is contained in:
Raphaël Bosi
2025-05-06 14:52:05 +02:00
committed by GitHub
parent f1d658bcb6
commit b3f5a3f75f
42 changed files with 964 additions and 130 deletions

View File

@ -21,11 +21,10 @@ import { useCallback } from 'react';
import { Link } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { useNavigateApp } from '~/hooks/useNavigateApp';
import { Button } from 'twenty-ui/input';
import { IconBrowserMaximize } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { getOsControlSymbol } from 'twenty-ui/utilities';
import { useNavigateApp } from '~/hooks/useNavigateApp';
const StyledLink = styled(Link)`
text-decoration: none;
`;

View File

@ -15,21 +15,26 @@ import { useExecuteTasksOnAnyLocationChange } from '@/app/hooks/useExecuteTasksO
import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken';
import { isCaptchaScriptLoadedState } from '@/captcha/states/isCaptchaScriptLoadedState';
import { isCaptchaRequiredForPath } from '@/captcha/utils/isCaptchaRequiredForPath';
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
import { useActiveRecordTableRow } from '@/object-record/record-table/hooks/useActiveRecordTableRow';
import { useFocusedRecordTableRow } from '@/object-record/record-table/hooks/useFocusedRecordTableRow';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId';
import { AppBasePath } from '@/types/AppBasePath';
import { AppPath } from '@/types/AppPath';
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
import { SettingsPath } from '@/types/SettingsPath';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { isDefined } from 'twenty-shared/utils';
import { AnalyticsType } from '~/generated/graphql';
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
import { usePageChangeEffectNavigateLocation } from '~/hooks/usePageChangeEffectNavigateLocation';
import { useInitializeQueryParamState } from '~/modules/app/hooks/useInitializeQueryParamState';
import { AnalyticsType } from '~/generated/graphql';
import { getPageTitleFromPath } from '~/utils/title-utils';
// TODO: break down into smaller functions and / or hooks
// - moved usePageChangeEffectNavigateLocation into dedicated hook
export const PageChangeEffect = () => {
@ -54,7 +59,19 @@ export const PageChangeEffect = () => {
const objectNamePlural =
useParams().objectNamePlural ?? CoreObjectNamePlural.Person;
const resetTableSelections = useResetTableRowSelection(objectNamePlural);
const contextStoreCurrentViewId = useRecoilComponentValueV2(
contextStoreCurrentViewIdComponentState,
MAIN_CONTEXT_STORE_INSTANCE_ID,
);
const recordIndexId = getRecordIndexIdFromObjectNamePluralAndViewId(
objectNamePlural,
contextStoreCurrentViewId || '',
);
const resetTableSelections = useResetTableRowSelection(recordIndexId);
const { unfocusRecordTableRow } = useFocusedRecordTableRow(recordIndexId);
const { deactivateRecordTableRow } = useActiveRecordTableRow(recordIndexId);
const { executeTasksOnAnyLocationChange } =
useExecuteTasksOnAnyLocationChange();
@ -84,8 +101,16 @@ export const PageChangeEffect = () => {
if (isLeavingRecordIndexPage) {
resetTableSelections();
unfocusRecordTableRow();
deactivateRecordTableRow();
}
}, [isMatchingLocation, previousLocation, resetTableSelections]);
}, [
isMatchingLocation,
previousLocation,
resetTableSelections,
unfocusRecordTableRow,
deactivateRecordTableRow,
]);
useEffect(() => {
switch (true) {

View File

@ -68,6 +68,7 @@ export const RecordChip = ({
const isSidePanelViewOpenRecordInType =
recordIndexOpenRecordIn === ViewOpenRecordInType.SIDE_PANEL;
const onClick = isSidePanelViewOpenRecordInType
? () =>
openRecordInCommandMenu({

View File

@ -6,12 +6,14 @@ import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record
import { RecordTableBodyEffectsWrapper } from '@/object-record/record-table/components/RecordTableBodyEffectsWrapper';
import { RecordTableContent } from '@/object-record/record-table/components/RecordTableContent';
import { RecordTableEmpty } from '@/object-record/record-table/components/RecordTableEmpty';
import { RecordTableScrollToFocusedElementEffect } from '@/object-record/record-table/components/RecordTableScrollToFocusedElementEffect';
import { RecordTableScrollToFocusedCellEffect } from '@/object-record/record-table/components/RecordTableScrollToFocusedCellEffect';
import { RecordTableScrollToFocusedRowEffect } from '@/object-record/record-table/components/RecordTableScrollToFocusedRowEffect';
import { RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/RecordTableClickOutsideListenerId';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { isRecordTableFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableFocusActiveComponentState';
import { isRecordTableCellFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableCellFocusActiveComponentState';
import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState';
import { isRecordTableRowFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableRowFocusActiveComponentState';
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
@ -46,8 +48,13 @@ export const RecordTable = () => {
const recordTableIsEmpty =
!isRecordTableInitialLoading && allRecordIds.length === 0;
const isRecordTableFocusActive = useRecoilComponentValueV2(
isRecordTableFocusActiveComponentState,
const isRecordTableCellFocusActive = useRecoilComponentValueV2(
isRecordTableCellFocusActiveComponentState,
recordTableId,
);
const isRecordTableRowFocusActive = useRecoilComponentValueV2(
isRecordTableRowFocusActiveComponentState,
recordTableId,
);
@ -71,7 +78,9 @@ export const RecordTable = () => {
tableBodyRef={tableBodyRef}
/>
{isRecordTableFocusActive && <RecordTableScrollToFocusedElementEffect />}
{isRecordTableCellFocusActive && <RecordTableScrollToFocusedCellEffect />}
{isRecordTableRowFocusActive && <RecordTableScrollToFocusedRowEffect />}
{recordTableIsEmpty && !hasRecordGroups ? (
<RecordTableEmpty

View File

@ -1,10 +1,11 @@
import { RecordTableDeactivateRecordTableRowEffect } from '@/object-record/record-table/components/RecordTableDeactivateRecordTableRowEffect';
import { RecordTableBodyEscapeHotkeyEffect } from '@/object-record/record-table/record-table-body/components/RecordTableBodyEscapeHotkeyEffect';
import { RecordTableBodyFocusClickOutsideEffect } from '@/object-record/record-table/record-table-body/components/RecordTableBodyFocusClickOutsideEffect';
import { RecordTableBodyFocusKeyboardEffect } from '@/object-record/record-table/record-table-body/components/RecordTableBodyFocusKeyboardEffect';
import { RecordTableBodyRowFocusKeyboardEffect } from '@/object-record/record-table/record-table-body/components/RecordTableBodyRowFocusKeyboardEffect';
import { RecordTableNoRecordGroupBodyEffect } from '@/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBodyEffect';
import { RecordTableRecordGroupBodyEffects } from '@/object-record/record-table/record-table-body/components/RecordTableRecordGroupBodyEffects';
import { isAtLeastOneTableRowSelectedSelector } from '@/object-record/record-table/record-table-row/states/isAtLeastOneTableRowSelectedSelector';
import { isRecordTableFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableFocusActiveComponentState';
import { isRecordTableRowFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableRowFocusActiveComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
export interface RecordTableBodyEffectsWrapperProps {
@ -16,12 +17,8 @@ export const RecordTableBodyEffectsWrapper = ({
hasRecordGroups,
tableBodyRef,
}: RecordTableBodyEffectsWrapperProps) => {
const isAtLeastOneRecordSelected = useRecoilComponentValueV2(
isAtLeastOneTableRowSelectedSelector,
);
const isRecordTableFocusActive = useRecoilComponentValueV2(
isRecordTableFocusActiveComponentState,
const isRecordTableRowFocusActive = useRecoilComponentValueV2(
isRecordTableRowFocusActiveComponentState,
);
return (
@ -31,9 +28,11 @@ export const RecordTableBodyEffectsWrapper = ({
) : (
<RecordTableNoRecordGroupBodyEffect />
)}
{isAtLeastOneRecordSelected && <RecordTableBodyEscapeHotkeyEffect />}
{isRecordTableFocusActive && <RecordTableBodyFocusKeyboardEffect />}
<RecordTableBodyEscapeHotkeyEffect />
<RecordTableBodyFocusKeyboardEffect />
{isRecordTableRowFocusActive && <RecordTableBodyRowFocusKeyboardEffect />}
<RecordTableBodyFocusClickOutsideEffect tableBodyRef={tableBodyRef} />
<RecordTableDeactivateRecordTableRowEffect />
</>
);
};

View File

@ -0,0 +1,12 @@
import { useActiveRecordTableRow } from '@/object-record/record-table/hooks/useActiveRecordTableRow';
import { useListenRightDrawerClose } from '@/ui/layout/right-drawer/hooks/useListenRightDrawerClose';
export const RecordTableDeactivateRecordTableRowEffect = () => {
const { deactivateRecordTableRow } = useActiveRecordTableRow();
useListenRightDrawerClose(() => {
deactivateRecordTableRow();
});
return null;
};

View File

@ -1,7 +1,7 @@
import { RecordTableBodyContextProvider } from '@/object-record/record-table/contexts/RecordTableBodyContext';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useHandleContainerMouseEnter } from '@/object-record/record-table/hooks/internal/useHandleContainerMouseEnter';
import { useRecordTableMoveFocus } from '@/object-record/record-table/hooks/useRecordTableMoveFocus';
import { useRecordTableMoveFocusedCell } from '@/object-record/record-table/hooks/useRecordTableMoveFocusedCell';
import { useCloseRecordTableCellNoGroup } from '@/object-record/record-table/record-table-cell/hooks/internal/useCloseRecordTableCellNoGroup';
import { useMoveHoverToCurrentCell } from '@/object-record/record-table/record-table-cell/hooks/useMoveHoverToCurrentCell';
import {
@ -28,7 +28,7 @@ export const RecordTableNoRecordGroupBodyContextProvider = ({
openTableCell(args);
};
const { moveFocus } = useRecordTableMoveFocus(recordTableId);
const { moveFocus } = useRecordTableMoveFocusedCell(recordTableId);
const handleMoveFocus = (direction: MoveFocusDirection) => {
moveFocus(direction);

View File

@ -1,7 +1,7 @@
import { RecordTableBodyContextProvider } from '@/object-record/record-table/contexts/RecordTableBodyContext';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useHandleContainerMouseEnter } from '@/object-record/record-table/hooks/internal/useHandleContainerMouseEnter';
import { useRecordTableMoveFocus } from '@/object-record/record-table/hooks/useRecordTableMoveFocus';
import { useRecordTableMove } from '@/object-record/record-table/hooks/useRecordTableMove';
import { useCloseRecordTableCellInGroup } from '@/object-record/record-table/record-table-cell/hooks/internal/useCloseRecordTableCellInGroup';
import { useMoveHoverToCurrentCell } from '@/object-record/record-table/record-table-cell/hooks/useMoveHoverToCurrentCell';
import {
@ -29,10 +29,10 @@ export const RecordTableRecordGroupBodyContextProvider = ({
openTableCell(args);
};
const { moveFocus } = useRecordTableMoveFocus(recordTableId);
const { move } = useRecordTableMove(recordTableId);
const handleMoveFocus = (direction: MoveFocusDirection) => {
moveFocus(direction);
move(direction);
};
const { closeTableCellInGroup } = useCloseRecordTableCellInGroup();

View File

@ -1,13 +1,18 @@
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { recordTableFocusPositionComponentState } from '@/object-record/record-table/states/recordTableFocusPositionComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useEffect } from 'react';
import { isDefined } from 'twenty-shared/utils';
export const RecordTableScrollToFocusedElementEffect = () => {
export const RecordTableScrollToFocusedCellEffect = () => {
const { recordTableId } = useRecordTableContextOrThrow();
const focusPosition = useRecoilComponentValueV2(
recordTableFocusPositionComponentState,
recordTableId,
);
// Handle cell focus
useEffect(() => {
if (!focusPosition) {
return;
@ -36,11 +41,15 @@ export const RecordTableScrollToFocusedElementEffect = () => {
}
}
focusElement.style.scrollMarginTop = '32px';
focusElement.style.scrollMarginBottom = '32px';
focusElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
return () => {
if (isDefined(focusElement)) {
focusElement.style.scrollMarginLeft = '';
focusElement.style.scrollMarginBottom = '';
}
};
}, [focusPosition]);

View File

@ -0,0 +1,63 @@
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { focusedRecordTableRowIndexComponentState } from '@/object-record/record-table/states/focusedRecordTableRowIndexComponentState';
import { isRecordTableRowFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableRowFocusActiveComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useEffect } from 'react';
import { isDefined } from 'twenty-shared/utils';
export const RecordTableScrollToFocusedRowEffect = () => {
const { recordTableId } = useRecordTableContextOrThrow();
const focusedRowIndex = useRecoilComponentValueV2(
focusedRecordTableRowIndexComponentState,
recordTableId,
);
const isRowFocusActive = useRecoilComponentValueV2(
isRecordTableRowFocusActiveComponentState,
recordTableId,
);
const allRecordIds = useRecoilComponentValueV2(
recordIndexAllRecordIdsComponentSelector,
recordTableId,
);
useEffect(() => {
if (
!isRowFocusActive ||
!isDefined(focusedRowIndex) ||
!allRecordIds?.length
) {
return;
}
const recordId = allRecordIds[focusedRowIndex];
if (!recordId) {
return;
}
const focusElement = document.getElementById(
`record-table-cell-0-${focusedRowIndex}`,
);
if (!focusElement) {
return;
}
focusElement.style.scrollMarginBottom = '32px';
focusElement.style.scrollMarginTop = '32px';
focusElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
return () => {
if (isDefined(focusElement)) {
focusElement.style.scrollMarginBottom = '';
}
};
}, [focusedRowIndex, isRowFocusActive, allRecordIds]);
return null;
};

View File

@ -1,7 +1,11 @@
import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
import { useActiveRecordTableRow } from '@/object-record/record-table/hooks/useActiveRecordTableRow';
import { useFocusedRecordTableRow } from '@/object-record/record-table/hooks/useFocusedRecordTableRow';
import { useSetIsRecordTableFocusActive } from '@/object-record/record-table/record-table-cell/hooks/useSetIsRecordTableFocusActive';
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { recordTableHoverPositionComponentState } from '@/object-record/record-table/states/recordTableHoverPositionComponentState';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
@ -24,11 +28,27 @@ export const useLeaveTableFocus = (recordTableId?: string) => {
recordTableIdFromContext,
);
const { unfocusRecordTableRow } = useFocusedRecordTableRow(
recordTableIdFromContext,
);
const { deactivateRecordTableRow } = useActiveRecordTableRow(
recordTableIdFromContext,
);
const setHotkeyScope = useSetHotkeyScope();
return () => {
resetTableRowSelection();
setIsFocusActiveForCurrentPosition(false);
unfocusRecordTableRow();
deactivateRecordTableRow();
setRecordTableHoverPosition(null);
setHotkeyScope(TableHotkeyScope.Table);
};
};

View File

@ -0,0 +1,62 @@
import { activeRecordTableRowIndexComponentState } from '@/object-record/record-table/states/activeRecordTableRowIndexComponentState';
import { isRecordTableRowActiveComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
export const useActiveRecordTableRow = (recordTableId?: string) => {
const isRowActiveState = useRecoilComponentCallbackStateV2(
isRecordTableRowActiveComponentFamilyState,
recordTableId,
);
const activeRowIndexState = useRecoilComponentCallbackStateV2(
activeRecordTableRowIndexComponentState,
recordTableId,
);
const deactivateRecordTableRow = useRecoilCallback(
({ set, snapshot }) =>
() => {
const activeRowIndex = snapshot
.getLoadable(activeRowIndexState)
.getValue();
if (!isDefined(activeRowIndex)) {
return;
}
set(activeRowIndexState, null);
set(isRowActiveState(activeRowIndex), false);
},
[activeRowIndexState, isRowActiveState],
);
const activateRecordTableRow = useRecoilCallback(
({ set, snapshot }) =>
(rowIndex: number) => {
const activeRowIndex = snapshot
.getLoadable(activeRowIndexState)
.getValue();
if (activeRowIndex === rowIndex) {
return;
}
if (isDefined(activeRowIndex)) {
set(isRowActiveState(activeRowIndex), false);
}
set(activeRowIndexState, rowIndex);
set(isRowActiveState(rowIndex), true);
},
[activeRowIndexState, isRowActiveState],
);
return {
activateRecordTableRow,
deactivateRecordTableRow,
};
};

View File

@ -0,0 +1,110 @@
import { focusedRecordTableRowIndexComponentState } from '@/object-record/record-table/states/focusedRecordTableRowIndexComponentState';
import { isRecordTableCellFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableCellFocusActiveComponentState';
import { isRecordTableRowFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableRowFocusActiveComponentState';
import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState';
import { recordTableFocusPositionComponentState } from '@/object-record/record-table/states/recordTableFocusPositionComponentState';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
export const useFocusedRecordTableRow = (recordTableId?: string) => {
const isRowFocusedState = useRecoilComponentCallbackStateV2(
isRecordTableRowFocusedComponentFamilyState,
recordTableId,
);
const focusedRowIndexState = useRecoilComponentCallbackStateV2(
focusedRecordTableRowIndexComponentState,
recordTableId,
);
const isRowFocusActiveState = useRecoilComponentCallbackStateV2(
isRecordTableRowFocusActiveComponentState,
recordTableId,
);
const focusedCellPositionState = useRecoilComponentCallbackStateV2(
recordTableFocusPositionComponentState,
recordTableId,
);
const isRecordTableCellFocusActiveState = useRecoilComponentCallbackStateV2(
isRecordTableCellFocusActiveComponentState,
recordTableId,
);
const unfocusRecordTableRow = useRecoilCallback(
({ set, snapshot }) =>
() => {
const focusedRowIndex = snapshot
.getLoadable(focusedRowIndexState)
.getValue();
if (!isDefined(focusedRowIndex)) {
return;
}
set(focusedRowIndexState, null);
set(isRowFocusedState(focusedRowIndex), false);
set(isRowFocusActiveState, false);
},
[focusedRowIndexState, isRowFocusedState, isRowFocusActiveState],
);
const focusRecordTableRow = useRecoilCallback(
({ set, snapshot }) =>
(rowIndex: number) => {
const focusedRowIndex = snapshot
.getLoadable(focusedRowIndexState)
.getValue();
if (isDefined(focusedRowIndex) && focusedRowIndex !== rowIndex) {
set(isRowFocusedState(focusedRowIndex), false);
}
set(focusedRowIndexState, rowIndex);
set(isRowFocusedState(rowIndex), true);
set(isRowFocusActiveState, true);
},
[focusedRowIndexState, isRowFocusedState, isRowFocusActiveState],
);
const restoreRecordTableRowFocusFromCellPosition = useRecoilCallback(
({ snapshot }) =>
() => {
const focusedRowIndex = snapshot
.getLoadable(focusedRowIndexState)
.getValue();
const focusedCellPosition = snapshot
.getLoadable(focusedCellPositionState)
.getValue();
const isRecordTableCellFocusActive = snapshot
.getLoadable(isRecordTableCellFocusActiveState)
.getValue();
if (
!isDefined(focusedRowIndex) ||
!isDefined(focusedCellPosition) ||
!isRecordTableCellFocusActive
) {
return;
}
focusRecordTableRow(focusedCellPosition.row);
},
[
focusedRowIndexState,
focusedCellPositionState,
isRecordTableCellFocusActiveState,
focusRecordTableRow,
],
);
return {
focusRecordTableRow,
unfocusRecordTableRow,
restoreRecordTableRowFocusFromCellPosition,
};
};

View File

@ -3,9 +3,7 @@ import { Key } from 'ts-key-enum';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { useSetHasUserSelectedAllRows } from '@/object-record/record-table/hooks/internal/useSetAllRowSelectedState';
import { useRecordTableMoveFocus } from '@/object-record/record-table/hooks/useRecordTableMoveFocus';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
@ -13,15 +11,16 @@ import { useUpsertRecordFromState } from '../../hooks/useUpsertRecordFromState';
import { ColumnDefinition } from '../types/ColumnDefinition';
import { TableHotkeyScope } from '../types/TableHotkeyScope';
import { useSetIsRecordTableFocusActive } from '@/object-record/record-table/record-table-cell/hooks/useSetIsRecordTableFocusActive';
import { availableTableColumnsComponentState } from '@/object-record/record-table/states/availableTableColumnsComponentState';
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState';
import { onColumnsChangeComponentState } from '@/object-record/record-table/states/onColumnsChangeComponentState';
import { onEntityCountChangeComponentState } from '@/object-record/record-table/states/onEntityCountChangeComponentState';
import { useRecordTableMove } from '@/object-record/record-table/hooks/useRecordTableMove';
import { onToggleColumnSortComponentState } from '@/object-record/record-table/states/onToggleColumnSortComponentState';
import { tableLastRowVisibleComponentState } from '@/object-record/record-table/states/tableLastRowVisibleComponentState';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
@ -31,7 +30,6 @@ import { useSelectAllRows } from './internal/useSelectAllRows';
import { useSetRecordTableData } from './internal/useSetRecordTableData';
import { useSetRecordTableFocusPosition } from './internal/useSetRecordTableFocusPosition';
import { useSetRowSelectedState } from './internal/useSetRowSelectedState';
type useRecordTableProps = {
recordTableId?: string;
};
@ -141,62 +139,65 @@ export const useRecordTable = (props?: useRecordTableProps) => {
const setFocusPosition = useSetRecordTableFocusPosition(recordTableId);
const { setIsFocusActiveForCurrentPosition } =
useSetIsRecordTableFocusActive(recordTableId);
const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope();
const { moveDown, moveLeft, moveRight, moveUp } =
useRecordTableMoveFocus(recordTableId);
const { move } = useRecordTableMove(recordTableId);
const useMapKeyboardToFocus = () => {
const setHotkeyScope = useSetHotkeyScope();
useScopedHotkeys(
[Key.ArrowUp, `${Key.Shift}+${Key.Enter}`],
() => {
moveUp();
move('up');
},
TableHotkeyScope.TableFocus,
[moveUp],
[move],
);
useScopedHotkeys(
Key.ArrowDown,
() => {
moveDown();
move('down');
},
TableHotkeyScope.TableFocus,
[moveDown],
[move],
);
useScopedHotkeys(
[Key.ArrowUp, 'k'],
() => {
setHotkeyScopeAndMemorizePreviousScope(TableHotkeyScope.TableFocus);
move('up');
},
TableHotkeyScope.Table,
[move],
);
useScopedHotkeys(
[Key.ArrowDown, 'j'],
() => {
setHotkeyScopeAndMemorizePreviousScope(TableHotkeyScope.TableFocus);
move('down');
},
TableHotkeyScope.Table,
[move],
);
useScopedHotkeys(
[Key.ArrowLeft, `${Key.Shift}+${Key.Tab}`],
() => {
moveLeft();
move('left');
},
TableHotkeyScope.TableFocus,
[moveLeft],
[move],
);
useScopedHotkeys(
[Key.ArrowRight, Key.Tab],
() => {
moveRight();
move('right');
},
TableHotkeyScope.TableFocus,
[moveRight],
);
useScopedHotkeys(
[Key.Escape],
() => {
setHotkeyScope(TableHotkeyScope.Table, {
goto: true,
keyboardShortcutMenu: true,
});
setIsFocusActiveForCurrentPosition(false);
},
TableHotkeyScope.TableFocus,
[setIsFocusActiveForCurrentPosition],
[move],
);
};
@ -211,10 +212,7 @@ export const useRecordTable = (props?: useRecordTableProps) => {
setRowSelected,
resetTableRowSelection,
upsertRecordTableItem,
moveDown,
moveLeft,
moveRight,
moveUp,
move,
useMapKeyboardToFocus,
selectAllRows,
setOnColumnsChange,

View File

@ -0,0 +1,39 @@
import { useRecordTableMoveFocusedCell } from '@/object-record/record-table/hooks/useRecordTableMoveFocusedCell';
import { useRecordTableMoveFocusedRow } from '@/object-record/record-table/hooks/useRecordTableMoveFocusedRow';
import { isRecordTableCellFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableCellFocusActiveComponentState';
import { MoveFocusDirection } from '@/object-record/record-table/types/MoveFocusDirection';
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { useRecoilCallback } from 'recoil';
export const useRecordTableMove = (recordTableId?: string) => {
const { moveFocusedRow } = useRecordTableMoveFocusedRow(recordTableId);
const { moveFocus } = useRecordTableMoveFocusedCell(recordTableId);
const isRecordTableFocusActiveState = useRecoilComponentCallbackStateV2(
isRecordTableCellFocusActiveComponentState,
recordTableId,
);
const move = useRecoilCallback(
({ snapshot }) =>
(direction: MoveFocusDirection) => {
const isRecordTableFocusActive = getSnapshotValue(
snapshot,
isRecordTableFocusActiveState,
);
if (isRecordTableFocusActive) {
moveFocus(direction);
} else {
moveFocusedRow(direction);
}
},
[isRecordTableFocusActiveState, moveFocusedRow, moveFocus],
);
return {
move,
};
};

View File

@ -9,7 +9,7 @@ import { recordTableFocusPositionComponentState } from '@/object-record/record-t
import { numberOfTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/numberOfTableColumnsComponentSelector';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
export const useRecordTableMoveFocus = (recordTableId?: string) => {
export const useRecordTableMoveFocusedCell = (recordTableId?: string) => {
const setFocusPosition = useSetRecordTableFocusPosition(recordTableId);
const focusPositionState = useRecoilComponentCallbackStateV2(

View File

@ -0,0 +1,93 @@
import { useRecoilCallback } from 'recoil';
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
import { useFocusedRecordTableRow } from '@/object-record/record-table/hooks/useFocusedRecordTableRow';
import { focusedRecordTableRowIndexComponentState } from '@/object-record/record-table/states/focusedRecordTableRowIndexComponentState';
import { MoveFocusDirection } from '@/object-record/record-table/types/MoveFocusDirection';
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { isDefined } from 'twenty-shared/utils';
export const useRecordTableMoveFocusedRow = (recordTableId?: string) => {
const { focusRecordTableRow } = useFocusedRecordTableRow(recordTableId);
const focusedRowIndexState = useRecoilComponentCallbackStateV2(
focusedRecordTableRowIndexComponentState,
recordTableId,
);
const recordIndexAllRecordIdsSelector = useRecoilComponentCallbackStateV2(
recordIndexAllRecordIdsComponentSelector,
recordTableId,
);
const moveFocusedRowUp = useRecoilCallback(
({ snapshot }) =>
() => {
const focusedRowIndex = getSnapshotValue(
snapshot,
focusedRowIndexState,
);
if (!isDefined(focusedRowIndex)) {
focusRecordTableRow(0);
return;
}
let newRowIndex = focusedRowIndex - 1;
if (newRowIndex < 0) {
newRowIndex = 0;
}
focusRecordTableRow(newRowIndex);
},
[focusedRowIndexState, focusRecordTableRow],
);
const moveFocusedRowDown = useRecoilCallback(
({ snapshot }) =>
() => {
const allRecordIds = getSnapshotValue(
snapshot,
recordIndexAllRecordIdsSelector,
);
const focusedRowIndex = getSnapshotValue(
snapshot,
focusedRowIndexState,
);
if (!isDefined(focusedRowIndex)) {
focusRecordTableRow(0);
return;
}
let newRowIndex = focusedRowIndex + 1;
if (newRowIndex >= allRecordIds.length) {
newRowIndex = allRecordIds.length - 1;
}
focusRecordTableRow(newRowIndex);
},
[
recordIndexAllRecordIdsSelector,
focusedRowIndexState,
focusRecordTableRow,
],
);
const moveFocusedRow = (direction: MoveFocusDirection) => {
if (direction === 'up') {
moveFocusedRowUp();
} else if (direction === 'down') {
moveFocusedRowDown();
}
};
return {
moveFocusedRowUp,
moveFocusedRowDown,
moveFocusedRow,
};
};

View File

@ -6,20 +6,20 @@ const StyledTbody = styled.tbody`
position: sticky;
left: 0;
z-index: 6;
transition: 0.3s ease;
transition: transform 0.3s ease;
}
td:nth-of-type(2) {
position: sticky;
left: 11px;
z-index: 6;
transition: 0.3s ease;
transition: transform 0.3s ease;
}
tr:not(:last-child) td:nth-of-type(3) {
// Last row is aggregate footer
position: sticky;
left: 43px;
z-index: 6;
transition: 0.3s ease;
transition: transform 0.3s ease;
&:not(.disable-shadow)::after {
content: '';

View File

@ -1,10 +1,12 @@
import { Key } from 'ts-key-enum';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useFocusedRecordTableRow } from '@/object-record/record-table/hooks/useFocusedRecordTableRow';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { isAtLeastOneTableRowSelectedSelector } from '@/object-record/record-table/record-table-row/states/isAtLeastOneTableRowSelectedSelector';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
export const RecordTableBodyEscapeHotkeyEffect = () => {
const { recordTableId } = useRecordTableContextOrThrow();
@ -12,12 +14,22 @@ export const RecordTableBodyEscapeHotkeyEffect = () => {
recordTableId,
});
const { unfocusRecordTableRow } = useFocusedRecordTableRow(recordTableId);
const isAtLeastOneRecordSelected = useRecoilComponentValueV2(
isAtLeastOneTableRowSelectedSelector,
);
useScopedHotkeys(
[Key.Escape],
() => {
resetTableRowSelection();
unfocusRecordTableRow();
if (isAtLeastOneRecordSelected) {
resetTableRowSelection();
}
},
TableHotkeyScope.Table,
[isAtLeastOneRecordSelected, resetTableRowSelection, unfocusRecordTableRow],
);
return <></>;

View File

@ -1,5 +1,13 @@
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useFocusedRecordTableRow } from '@/object-record/record-table/hooks/useFocusedRecordTableRow';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { useSetIsRecordTableFocusActive } from '@/object-record/record-table/record-table-cell/hooks/useSetIsRecordTableFocusActive';
import { isRecordTableCellFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableCellFocusActiveComponentState';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { Key } from 'ts-key-enum';
export const RecordTableBodyFocusKeyboardEffect = () => {
const { recordTableId } = useRecordTableContextOrThrow();
@ -8,7 +16,41 @@ export const RecordTableBodyFocusKeyboardEffect = () => {
recordTableId,
});
const setHotkeyScope = useSetHotkeyScope();
const { restoreRecordTableRowFocusFromCellPosition } =
useFocusedRecordTableRow(recordTableId);
const { setIsFocusActiveForCurrentPosition } =
useSetIsRecordTableFocusActive(recordTableId);
const isRecordTableFocusActive = useRecoilComponentValueV2(
isRecordTableCellFocusActiveComponentState,
);
useMapKeyboardToFocus();
useScopedHotkeys(
[Key.Escape],
() => {
if (isRecordTableFocusActive) {
restoreRecordTableRowFocusFromCellPosition();
setIsFocusActiveForCurrentPosition(false);
} else {
setHotkeyScope(TableHotkeyScope.Table, {
goto: true,
keyboardShortcutMenu: true,
});
}
},
TableHotkeyScope.TableFocus,
[
setIsFocusActiveForCurrentPosition,
restoreRecordTableRowFocusFromCellPosition,
setHotkeyScope,
isRecordTableFocusActive,
],
);
return <></>;
};

View File

@ -0,0 +1,31 @@
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useRecordTableMoveFocusedRow } from '@/object-record/record-table/hooks/useRecordTableMoveFocusedRow';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { Key } from 'ts-key-enum';
export const RecordTableBodyRowFocusKeyboardEffect = () => {
const { recordTableId } = useRecordTableContextOrThrow();
const { moveFocusedRow } = useRecordTableMoveFocusedRow(recordTableId);
useScopedHotkeys(
[Key.ArrowUp, 'k'],
() => {
moveFocusedRow('up');
},
TableHotkeyScope.TableFocus,
[moveFocusedRow],
);
useScopedHotkeys(
[Key.ArrowDown, 'j'],
() => {
moveFocusedRow('down');
},
TableHotkeyScope.TableFocus,
[moveFocusedRow],
);
return <></>;
};

View File

@ -17,6 +17,10 @@ const StyledContainer = styled.div`
min-width: ${TABLE_CELL_CHECKBOX_MIN_WIDTH};
`;
const StyledRecordTableTd = styled(RecordTableTd)`
border-left: 1px solid transparent;
`;
export const RecordTableCellCheckbox = () => {
const { isSelected } = useRecordTableRowContextOrThrow();
@ -27,10 +31,10 @@ export const RecordTableCellCheckbox = () => {
}, [isSelected, setCurrentRowSelected]);
return (
<RecordTableTd isSelected={isSelected} hasRightBorder={false}>
<StyledRecordTableTd isSelected={isSelected} hasRightBorder={false}>
<StyledContainer onClick={handleClick}>
<Checkbox hoverable checked={isSelected} />
</StyledContainer>
</RecordTableTd>
</StyledRecordTableTd>
);
};

View File

@ -6,20 +6,25 @@ import styled from '@emotion/styled';
import { FieldDisplay } from '@/object-record/record-field/components/FieldDisplay';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useIsFieldInputOnly } from '@/object-record/record-field/hooks/useIsFieldInputOnly';
import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { RecordTableCellDisplayMode } from '@/object-record/record-table/record-table-cell/components/RecordTableCellDisplayMode';
import { RecordTableCellEditButton } from '@/object-record/record-table/record-table-cell/components/RecordTableCellEditButton';
import { RecordTableCellEditMode } from '@/object-record/record-table/record-table-cell/components/RecordTableCellEditMode';
import { RecordTableCellFieldInput } from '@/object-record/record-table/record-table-cell/components/RecordTableCellFieldInput';
import { isRecordTableRowActiveComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState';
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
import { useContext } from 'react';
import { BORDER_COMMON } from 'twenty-ui/theme';
import { useIsMobile } from 'twenty-ui/utilities';
const StyledRecordTableCellHoveredPortalContent = styled.div<{
isReadOnly: boolean;
isRowActive: boolean;
}>`
align-items: center;
background: ${({ theme }) => theme.background.transparent.secondary};
background-color: ${({ theme }) => theme.background.primary};
background-color: ${({ theme, isRowActive }) =>
isRowActive ? theme.accent.quaternary : theme.background.primary};
border-radius: ${({ isReadOnly }) =>
!isReadOnly ? BORDER_COMMON.radius.sm : 'none'};
box-sizing: border-box;
@ -28,10 +33,12 @@ const StyledRecordTableCellHoveredPortalContent = styled.div<{
height: 32px;
outline: ${({ theme, isReadOnly }) =>
isReadOnly
? `1px solid ${theme.border.color.medium}`
: `1px solid ${theme.font.color.extraLight}`};
outline: ${({ theme, isReadOnly, isRowActive }) =>
isRowActive
? 'none'
: isReadOnly
? `1px solid ${theme.border.color.medium}`
: `1px solid ${theme.font.color.extraLight}`};
position: relative;
user-select: none;
@ -53,8 +60,18 @@ const RecordTableCellHoveredPortalContent = () => {
const showButton =
!isFieldInputOnly && !isReadOnly && !(isMobile && isFirstColumn);
const { rowIndex } = useRecordTableRowContextOrThrow();
const isRowActive = useRecoilComponentFamilyValueV2(
isRecordTableRowActiveComponentFamilyState,
rowIndex,
);
return (
<StyledRecordTableCellHoveredPortalContent isReadOnly={isReadOnly}>
<StyledRecordTableCellHoveredPortalContent
isReadOnly={isReadOnly}
isRowActive={isRowActive}
>
{isFieldInputOnly ? (
<RecordTableCellEditMode>
<RecordTableCellFieldInput />

View File

@ -1,14 +1,14 @@
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableCellEditModePortal } from '@/object-record/record-table/record-table-cell/components/RecordTableCellEditModePortal';
import { RecordTableCellHoveredPortal } from '@/object-record/record-table/record-table-cell/components/RecordTableCellHoveredPortal';
import { isRecordTableFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableFocusActiveComponentState';
import { isRecordTableCellFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableCellFocusActiveComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
export const RecordTableCellPortals = () => {
const { recordTableId } = useRecordTableContextOrThrow();
const isRecordTableFocusActive = useRecoilComponentValueV2(
isRecordTableFocusActiveComponentState,
isRecordTableCellFocusActiveComponentState,
recordTableId,
);

View File

@ -4,10 +4,5 @@ import { RecordTableTd } from '@/object-record/record-table/record-table-cell/co
export const RecordTableLastEmptyCell = () => {
const { isSelected } = useRecordTableRowContextOrThrow();
return (
<>
<RecordTableTd isSelected={isSelected} hasRightBorder={false} />
<RecordTableTd isSelected={isSelected} hasRightBorder={false} />
</>
);
return <RecordTableTd isSelected={isSelected} hasRightBorder={false} />;
};

View File

@ -26,7 +26,7 @@ const StyledTd = styled.td<{
hasRightBorder && !isDragging ? `1px solid ${borderColor}` : 'none'};
padding: 0;
transition: 0.3s ease;
transition: transform 0.3s ease;
text-align: left;

View File

@ -4,7 +4,7 @@ import { RecoilRoot, useRecoilValue } from 'recoil';
import { RecordTableComponentInstance } from '@/object-record/record-table/components/RecordTableComponentInstance';
import { useSetIsRecordTableFocusActive } from '@/object-record/record-table/record-table-cell/hooks/useSetIsRecordTableFocusActive';
import { isRecordTableFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableFocusActiveComponentState';
import { isRecordTableCellFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableCellFocusActiveComponentState';
import { recordTableFocusPositionComponentState } from '@/object-record/record-table/states/recordTableFocusPositionComponentState';
import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition';
@ -19,7 +19,7 @@ const Wrapper = ({ children }: { children: React.ReactNode }) => (
<RecoilRoot
initializeState={({ set }) => {
set(
isRecordTableFocusActiveComponentState.atomFamily({
isRecordTableCellFocusActiveComponentState.atomFamily({
instanceId: 'test-table-id',
}),
false,
@ -50,7 +50,7 @@ const renderHooks = () => {
const { setIsFocusActive, setIsFocusActiveForCurrentPosition } =
useSetIsRecordTableFocusActive('test-table-id');
const isRecordTableFocusActive = useRecoilValue(
isRecordTableFocusActiveComponentState.atomFamily({
isRecordTableCellFocusActiveComponentState.atomFamily({
instanceId: 'test-table-id',
}),
);
@ -99,7 +99,7 @@ describe('useSetIsRecordTableFocusActive', () => {
expect(result.current.focusPosition).toEqual(cellPosition);
});
it('should remove focus-active class when focus is deactivated and update isRecordTableFocusActiveComponentState', () => {
it('should remove focus-active class when focus is deactivated and update isRecordTableCellFocusActiveComponentState', () => {
const { result } = renderHooks();
const cellPosition: TableCellPosition = { column: 1, row: 0 };

View File

@ -9,7 +9,6 @@ import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { useRecordTableBodyContextOrThrow } from '@/object-record/record-table/contexts/RecordTableBodyContext';
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
import { useSetRecordTableFocusPosition } from '@/object-record/record-table/hooks/internal/useSetRecordTableFocusPosition';
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
export const DEFAULT_CELL_SCOPE: HotkeyScope = {
@ -36,8 +35,6 @@ export const useOpenRecordTableCellFromCell = () => {
const { cellPosition } = useContext(RecordTableCellContext);
const setFocusPosition = useSetRecordTableFocusPosition();
const openTableCell = (
initialValue?: string,
isActionButtonClick = false,
@ -54,8 +51,6 @@ export const useOpenRecordTableCellFromCell = () => {
isActionButtonClick,
isNavigating,
});
setFocusPosition(cellPosition);
};
return {

View File

@ -25,6 +25,10 @@ import { getDropdownFocusIdForRecordField } from '@/object-record/utils/getDropd
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious';
import { useSetRecordTableFocusPosition } from '@/object-record/record-table/hooks/internal/useSetRecordTableFocusPosition';
import { useActiveRecordTableRow } from '@/object-record/record-table/hooks/useActiveRecordTableRow';
import { useFocusedRecordTableRow } from '@/object-record/record-table/hooks/useFocusedRecordTableRow';
import { isRecordTableRowFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableRowFocusActiveComponentState';
import { clickOutsideListenerIsActivatedComponentState } from '@/ui/utilities/pointer-event/states/clickOutsideListenerIsActivatedComponentState';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
@ -82,6 +86,18 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
const { openFieldInput } = useOpenFieldInputEditMode();
const { activateRecordTableRow, deactivateRecordTableRow } =
useActiveRecordTableRow(tableScopeId);
const { unfocusRecordTableRow } = useFocusedRecordTableRow(tableScopeId);
const setIsRowFocusActive = useSetRecoilComponentStateV2(
isRecordTableRowFocusActiveComponentState,
tableScopeId,
);
const setFocusPosition = useSetRecordTableFocusPosition();
const openTableCell = useRecoilCallback(
({ snapshot, set }) =>
({
@ -134,6 +150,9 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
recordId,
objectNameSingular,
});
activateRecordTableRow(cellPosition.row);
unfocusRecordTableRow();
}
return;
@ -147,6 +166,12 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
return;
}
deactivateRecordTableRow();
setFocusPosition(cellPosition);
setIsRowFocusActive(false);
setDragSelectionStartEnabled(false);
openFieldInput({
@ -179,6 +204,9 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
},
[
clickOutsideListenerIsActivatedState,
deactivateRecordTableRow,
setFocusPosition,
setIsRowFocusActive,
setDragSelectionStartEnabled,
openFieldInput,
setCurrentTableCellInEditModePosition,
@ -189,6 +217,8 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
navigate,
indexIdentifierUrl,
openRecordInCommandMenu,
activateRecordTableRow,
unfocusRecordTableRow,
setViewableRecordId,
setViewableRecordNameSingular,
],

View File

@ -1,4 +1,4 @@
import { isRecordTableFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableFocusActiveComponentState';
import { isRecordTableCellFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableCellFocusActiveComponentState';
import { recordTableFocusPositionComponentState } from '@/object-record/record-table/states/recordTableFocusPositionComponentState';
import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
@ -6,7 +6,7 @@ import { useRecoilCallback } from 'recoil';
export const useSetIsRecordTableFocusActive = (recordTableId?: string) => {
const isRecordTableFocusActiveState = useRecoilComponentCallbackStateV2(
isRecordTableFocusActiveComponentState,
isRecordTableCellFocusActiveComponentState,
recordTableId,
);

View File

@ -8,6 +8,8 @@ import { useRecordTableContextOrThrow } from '@/object-record/record-table/conte
import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord';
import { useTableColumns } from '@/object-record/record-table/hooks/useTableColumns';
import { RecordTableColumnHeadWithDropdown } from '@/object-record/record-table/record-table-header/components/RecordTableColumnHeadWithDropdown';
import { isRecordTableRowActiveComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState';
import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState';
import { isRecordTableScrolledLeftComponentState } from '@/object-record/record-table/states/isRecordTableScrolledLeftComponentState';
import { resizeFieldOffsetComponentState } from '@/object-record/record-table/states/resizeFieldOffsetComponentState';
import { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState';
@ -17,23 +19,27 @@ import { useTrackPointer } from '@/ui/utilities/pointer-event/hooks/useTrackPoin
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { mapArrayToObject } from '~/utils/array/mapArrayToObject';
import { IconPlus } from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
import { mapArrayToObject } from '~/utils/array/mapArrayToObject';
const COLUMN_MIN_WIDTH = 104;
const StyledColumnHeaderCell = styled.th<{
columnWidth: number;
isResizing?: boolean;
isFirstRowActiveOrFocused: boolean;
}>`
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
border-bottom: ${({ isFirstRowActiveOrFocused, theme }) =>
isFirstRowActiveOrFocused
? 'none'
: `1px solid ${theme.border.color.light}`};
color: ${({ theme }) => theme.font.color.tertiary};
padding: 0;
text-align: left;
transition: 0.3s ease;
background-color: ${({ theme }) => theme.background.primary};
border-right: 1px solid ${({ theme }) => theme.border.color.light};
@ -215,6 +221,18 @@ export const RecordTableHeaderCell = ({
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
const isFirstRowActive = useRecoilComponentFamilyValueV2(
isRecordTableRowActiveComponentFamilyState,
0,
);
const isFirstRowFocused = useRecoilComponentFamilyValueV2(
isRecordTableRowFocusedComponentFamilyState,
0,
);
const isFirstRowActiveOrFocused = isFirstRowActive || isFirstRowFocused;
return (
<StyledColumnHeaderCell
key={column.fieldMetadataId}
@ -227,6 +245,7 @@ export const RecordTableHeaderCell = ({
)}
onMouseEnter={() => setIconVisibility(true)}
onMouseLeave={() => setIconVisibility(false)}
isFirstRowActiveOrFocused={isFirstRowActiveOrFocused}
>
<StyledColumnHeadContainer>
<RecordTableColumnHeadWithDropdown column={column} />

View File

@ -4,7 +4,10 @@ import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState';
import { isRecordTableRowActiveComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState';
import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState';
import { allRowsSelectedStatusComponentSelector } from '@/object-record/record-table/states/selectors/allRowsSelectedStatusComponentSelector';
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { Checkbox } from 'twenty-ui/input';
@ -16,9 +19,14 @@ const StyledContainer = styled.div`
background-color: ${({ theme }) => theme.background.primary};
`;
const StyledColumnHeaderCell = styled.th`
const StyledColumnHeaderCell = styled.th<{
isFirstRowActiveOrFocused: boolean;
}>`
background-color: ${({ theme }) => theme.background.primary};
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
border-bottom: ${({ isFirstRowActiveOrFocused, theme }) =>
isFirstRowActiveOrFocused
? 'none'
: `1px solid ${theme.border.color.light}`};
border-right: transparent;
width: 30px;
`;
@ -58,8 +66,22 @@ export const RecordTableHeaderCheckboxColumn = () => {
}
};
const isFirstRowActive = useRecoilComponentFamilyValueV2(
isRecordTableRowActiveComponentFamilyState,
0,
);
const isFirstRowFocused = useRecoilComponentFamilyValueV2(
isRecordTableRowFocusedComponentFamilyState,
0,
);
const isFirstRowActiveOrFocused = isFirstRowActive || isFirstRowFocused;
return (
<StyledColumnHeaderCell>
<StyledColumnHeaderCell
isFirstRowActiveOrFocused={isFirstRowActiveOrFocused}
>
<StyledContainer>
<Checkbox
hoverable

View File

@ -2,49 +2,50 @@ import styled from '@emotion/styled';
import { HIDDEN_TABLE_COLUMN_DROPDOWN_ID } from '@/object-record/record-table/constants/HiddenTableColumnDropdownId';
import { RecordTableHeaderPlusButtonContent } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderPlusButtonContent';
import { isRecordTableRowActiveComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState';
import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { useScrollWrapperElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperElement';
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
import { useTheme } from '@emotion/react';
import { IconPlus } from 'twenty-ui/display';
const StyledPlusIconHeaderCell = styled.th<{
isTableWiderThanScreen: boolean;
isFirstRowActiveOrFocused: boolean;
}>`
${({ theme }) => {
return `
&:hover {
background: ${theme.background.transparent.light};
};
`;
}};
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
border-bottom: ${({ isFirstRowActiveOrFocused, theme }) =>
isFirstRowActiveOrFocused
? 'none'
: `1px solid ${theme.border.color.light}`};
background-color: ${({ theme }) => theme.background.primary};
border-left: none !important;
color: ${({ theme }) => theme.font.color.tertiary};
border-right: none !important;
width: 32px;
cursor: default;
${({ isTableWiderThanScreen, theme }) =>
isTableWiderThanScreen
? `
background-color: ${theme.background.primary};
width: 32px;
`
: ''};
: 'width: 100%'};
z-index: 1;
`;
const StyledEmptyHeaderCell = styled.th`
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
background-color: ${({ theme }) => theme.background.primary};
width: 100%;
`;
const StyledPlusIconContainer = styled.div`
align-items: center;
display: flex;
height: 32px;
justify-content: center;
width: 100%;
`;
const StyledDropdownContainer = styled.div`
&:hover {
background: ${({ theme }) => theme.background.transparent.light};
}
cursor: pointer;
`;
const HIDDEN_TABLE_COLUMN_DROPDOWN_HOTKEY_SCOPE_ID =
@ -59,9 +60,24 @@ export const RecordTableHeaderLastColumn = () => {
(scrollWrapperHTMLElement?.clientWidth ?? 0) <
(scrollWrapperHTMLElement?.scrollWidth ?? 0);
const isFirstRowActive = useRecoilComponentFamilyValueV2(
isRecordTableRowActiveComponentFamilyState,
0,
);
const isFirstRowFocused = useRecoilComponentFamilyValueV2(
isRecordTableRowFocusedComponentFamilyState,
0,
);
const isFirstRowActiveOrFocused = isFirstRowActive || isFirstRowFocused;
return (
<>
<StyledPlusIconHeaderCell isTableWiderThanScreen={isTableWiderThanScreen}>
<StyledPlusIconHeaderCell
isTableWiderThanScreen={isTableWiderThanScreen}
isFirstRowActiveOrFocused={isFirstRowActiveOrFocused}
>
<StyledDropdownContainer>
<Dropdown
dropdownId={HIDDEN_TABLE_COLUMN_DROPDOWN_ID}
clickableComponent={
@ -75,8 +91,7 @@ export const RecordTableHeaderLastColumn = () => {
scope: HIDDEN_TABLE_COLUMN_DROPDOWN_HOTKEY_SCOPE_ID,
}}
/>
</StyledPlusIconHeaderCell>
<StyledEmptyHeaderCell />
</>
</StyledDropdownContainer>
</StyledPlusIconHeaderCell>
);
};

View File

@ -5,8 +5,13 @@ import { RecordTableCellGrip } from '@/object-record/record-table/record-table-c
import { RecordTableLastEmptyCell } from '@/object-record/record-table/record-table-cell/components/RecordTableLastEmptyCell';
import { RecordTableCells } from '@/object-record/record-table/record-table-row/components/RecordTableCells';
import { RecordTableDraggableTr } from '@/object-record/record-table/record-table-row/components/RecordTableDraggableTr';
import { RecordTableRowHotkeyEffect } from '@/object-record/record-table/record-table-row/components/RecordTableRowHotkeyEffect';
import { isRecordTableRowFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableRowFocusActiveComponentState';
import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState';
import { ListenRecordUpdatesEffect } from '@/subscription/components/ListenUpdatesEffect';
import { getDefaultRecordFieldsToListen } from '@/subscription/utils/getDefaultRecordFieldsToListen.util';
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
type RecordTableRowProps = {
recordId: string;
@ -23,6 +28,13 @@ export const RecordTableRow = ({
const listenedFields = getDefaultRecordFieldsToListen({
objectNameSingular,
});
const isFocused = useRecoilComponentFamilyValueV2(
isRecordTableRowFocusedComponentFamilyState,
rowIndexForFocus,
);
const isRowFocusActive = useRecoilComponentValueV2(
isRecordTableRowFocusActiveComponentState,
);
return (
<RecordTableDraggableTr
@ -30,6 +42,7 @@ export const RecordTableRow = ({
draggableIndex={rowIndexForDrag}
focusIndex={rowIndexForFocus}
>
{isRowFocusActive && isFocused && <RecordTableRowHotkeyEffect />}
<RecordTableCellGrip />
<RecordTableCellCheckbox />
<RecordTableCells />

View File

@ -0,0 +1,63 @@
import { useOpenRecordInCommandMenu } from '@/command-menu/hooks/useOpenRecordInCommandMenu';
import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { useSetRecordTableFocusPosition } from '@/object-record/record-table/hooks/internal/useSetRecordTableFocusPosition';
import { useActiveRecordTableRow } from '@/object-record/record-table/hooks/useActiveRecordTableRow';
import { useSetCurrentRowSelected } from '@/object-record/record-table/record-table-row/hooks/useSetCurrentRowSelected';
import { isRecordTableRowFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableRowFocusActiveComponentState';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { Key } from 'ts-key-enum';
export const RecordTableRowHotkeyEffect = () => {
const { isSelected, recordId, objectNameSingular, rowIndex } =
useRecordTableRowContextOrThrow();
const { setCurrentRowSelected } = useSetCurrentRowSelected();
const { openRecordInCommandMenu } = useOpenRecordInCommandMenu();
const { activateRecordTableRow } = useActiveRecordTableRow();
const setIsRowFocusActive = useSetRecoilComponentStateV2(
isRecordTableRowFocusActiveComponentState,
);
const setFocusPosition = useSetRecordTableFocusPosition();
useScopedHotkeys(
'x',
() => {
setCurrentRowSelected(!isSelected);
},
TableHotkeyScope.TableFocus,
);
useScopedHotkeys(
[`${Key.Control}+${Key.Enter}`, `${Key.Meta}+${Key.Enter}`],
() => {
openRecordInCommandMenu({
recordId: recordId,
objectNameSingular: objectNameSingular,
isNewRecord: false,
});
activateRecordTableRow(rowIndex);
},
TableHotkeyScope.TableFocus,
);
useScopedHotkeys(
Key.Enter,
() => {
setIsRowFocusActive(false);
setFocusPosition({
row: rowIndex,
column: 0,
});
},
TableHotkeyScope.TableFocus,
);
return null;
};

View File

@ -3,17 +3,70 @@ import { useRecordTableContextOrThrow } from '@/object-record/record-table/conte
import { RecordTableRowContextProvider } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { isRowSelectedComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowSelectedComponentFamilyState';
import { isRowVisibleComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowVisibleComponentFamilyState';
import { isRecordTableRowActiveComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState';
import { isRecordTableRowFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableRowFocusActiveComponentState';
import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState';
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import styled from '@emotion/styled';
import { ReactNode, forwardRef } from 'react';
const StyledTr = styled.tr<{ isDragging: boolean }>`
position: relative;
const StyledTr = styled.tr<{
isDragging: boolean;
}>`
border: ${({ isDragging, theme }) =>
isDragging
? `1px solid ${theme.border.color.medium}`
: '1px solid transparent'};
position: relative;
transition: border-left-color 0.2s ease-in-out;
&[data-next-row-active-or-focused='true'] {
td {
border-bottom: none;
}
}
&[data-focused='true'] {
td {
&:not(:first-of-type) {
border-bottom: 1px solid ${({ theme }) => theme.border.color.medium};
border-top: 1px solid ${({ theme }) => theme.border.color.medium};
border-color: ${({ theme }) => theme.border.color.medium};
background-color: ${({ theme }) => theme.background.tertiary};
}
&:nth-of-type(2) {
border-left: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: ${({ theme }) => theme.border.radius.sm} 0 0
${({ theme }) => theme.border.radius.sm};
}
&:last-of-type {
border-right: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: 0 ${({ theme }) => theme.border.radius.sm}
${({ theme }) => theme.border.radius.sm} 0;
}
}
}
&[data-active='true'] {
td {
&:not(:first-of-type) {
border-bottom: 1px solid ${({ theme }) => theme.adaptiveColors.blue3};
border-top: 1px solid ${({ theme }) => theme.adaptiveColors.blue3};
background-color: ${({ theme }) => theme.accent.quaternary};
}
&:nth-of-type(2) {
border-left: 1px solid ${({ theme }) => theme.adaptiveColors.blue3};
border-radius: ${({ theme }) => theme.border.radius.sm} 0 0
${({ theme }) => theme.border.radius.sm};
}
&:last-of-type {
border-right: 1px solid ${({ theme }) => theme.adaptiveColors.blue3};
border-radius: 0 ${({ theme }) => theme.border.radius.sm}
${({ theme }) => theme.border.radius.sm} 0;
}
}
}
`;
type RecordTableTrProps = {
@ -21,7 +74,10 @@ type RecordTableTrProps = {
recordId: string;
focusIndex: number;
isDragging?: boolean;
} & React.ComponentProps<typeof StyledTr>;
} & Omit<
React.ComponentProps<typeof StyledTr>,
'isActive' | 'isNextRowActiveOrFocused' | 'isFocused'
>;
export const RecordTableTr = forwardRef<
HTMLTableRowElement,
@ -38,6 +94,33 @@ export const RecordTableTr = forwardRef<
recordId,
);
const isActive = useRecoilComponentFamilyValueV2(
isRecordTableRowActiveComponentFamilyState,
focusIndex,
);
const isNextRowActive = useRecoilComponentFamilyValueV2(
isRecordTableRowActiveComponentFamilyState,
focusIndex + 1,
);
const isFocused = useRecoilComponentFamilyValueV2(
isRecordTableRowFocusedComponentFamilyState,
focusIndex,
);
const isRowFocusActive = useRecoilComponentValueV2(
isRecordTableRowFocusActiveComponentState,
);
const isNextRowFocused = useRecoilComponentFamilyValueV2(
isRecordTableRowFocusedComponentFamilyState,
focusIndex + 1,
);
const isNextRowActiveOrFocused =
(isRowFocusActive && isNextRowFocused) || isNextRowActive;
return (
<RecordTableRowContextProvider
value={{
@ -56,6 +139,13 @@ export const RecordTableTr = forwardRef<
data-virtualized-id={recordId}
isDragging={isDragging}
ref={ref}
data-active={isActive ? 'true' : 'false'}
data-focused={
isRowFocusActive && isFocused && !isActive ? 'true' : 'false'
}
data-next-row-active-or-focused={
isNextRowActiveOrFocused ? 'true' : 'false'
}
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
>

View File

@ -0,0 +1,10 @@
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
export const activeRecordTableRowIndexComponentState = createComponentStateV2<
number | null
>({
key: 'activeRecordTableRowIndexComponentState',
defaultValue: null,
componentInstanceContext: RecordTableComponentInstanceContext,
});

View File

@ -0,0 +1,10 @@
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
export const focusedRecordTableRowIndexComponentState = createComponentStateV2<
number | null
>({
key: 'focusedRecordTableRowIndexComponentState',
defaultValue: null,
componentInstanceContext: RecordTableComponentInstanceContext,
});

View File

@ -0,0 +1,9 @@
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
export const isRecordTableCellFocusActiveComponentState =
createComponentStateV2<boolean>({
key: 'isRecordTableCellFocusActiveComponentState',
defaultValue: false,
componentInstanceContext: RecordTableComponentInstanceContext,
});

View File

@ -0,0 +1,9 @@
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { createComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentFamilyStateV2';
export const isRecordTableRowActiveComponentFamilyState =
createComponentFamilyStateV2<boolean, number>({
key: 'isRecordTableRowActiveComponentFamilyState',
defaultValue: false,
componentInstanceContext: RecordTableComponentInstanceContext,
});

View File

@ -1,9 +1,9 @@
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
export const isRecordTableFocusActiveComponentState =
export const isRecordTableRowFocusActiveComponentState =
createComponentStateV2<boolean>({
key: 'isRecordTableFocusActiveComponentState',
key: 'isRecordTableRowFocusActiveComponentState',
defaultValue: false,
componentInstanceContext: RecordTableComponentInstanceContext,
});

View File

@ -0,0 +1,9 @@
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { createComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentFamilyStateV2';
export const isRecordTableRowFocusedComponentFamilyState =
createComponentFamilyStateV2<boolean, number>({
key: 'isRecordTableRowFocusedComponentFamilyState',
defaultValue: false,
componentInstanceContext: RecordTableComponentInstanceContext,
});