diff --git a/packages/twenty-front/src/modules/action-menu/components/RecordShowRightDrawerOpenRecordButton.tsx b/packages/twenty-front/src/modules/action-menu/components/RecordShowRightDrawerOpenRecordButton.tsx
index e72f4118d..5b1120a75 100644
--- a/packages/twenty-front/src/modules/action-menu/components/RecordShowRightDrawerOpenRecordButton.tsx
+++ b/packages/twenty-front/src/modules/action-menu/components/RecordShowRightDrawerOpenRecordButton.tsx
@@ -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;
`;
diff --git a/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx b/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx
index 775a4b4f7..d99712590 100644
--- a/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx
+++ b/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx
@@ -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) {
diff --git a/packages/twenty-front/src/modules/object-record/components/RecordChip.tsx b/packages/twenty-front/src/modules/object-record/components/RecordChip.tsx
index f67b3db68..fa47d4928 100644
--- a/packages/twenty-front/src/modules/object-record/components/RecordChip.tsx
+++ b/packages/twenty-front/src/modules/object-record/components/RecordChip.tsx
@@ -68,6 +68,7 @@ export const RecordChip = ({
const isSidePanelViewOpenRecordInType =
recordIndexOpenRecordIn === ViewOpenRecordInType.SIDE_PANEL;
+
const onClick = isSidePanelViewOpenRecordInType
? () =>
openRecordInCommandMenu({
diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx
index 57edee8de..510694369 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx
@@ -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 && }
+ {isRecordTableCellFocusActive && }
+
+ {isRecordTableRowFocusActive && }
{recordTableIsEmpty && !hasRecordGroups ? (
{
- const isAtLeastOneRecordSelected = useRecoilComponentValueV2(
- isAtLeastOneTableRowSelectedSelector,
- );
-
- const isRecordTableFocusActive = useRecoilComponentValueV2(
- isRecordTableFocusActiveComponentState,
+ const isRecordTableRowFocusActive = useRecoilComponentValueV2(
+ isRecordTableRowFocusActiveComponentState,
);
return (
@@ -31,9 +28,11 @@ export const RecordTableBodyEffectsWrapper = ({
) : (
)}
- {isAtLeastOneRecordSelected && }
- {isRecordTableFocusActive && }
+
+
+ {isRecordTableRowFocusActive && }
+
>
);
};
diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableDeactivateRecordTableRowEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableDeactivateRecordTableRowEffect.tsx
new file mode 100644
index 000000000..4eabda7f7
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableDeactivateRecordTableRowEffect.tsx
@@ -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;
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableNoRecordGroupBodyContextProvider.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableNoRecordGroupBodyContextProvider.tsx
index 4278883a8..339215d6c 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableNoRecordGroupBodyContextProvider.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableNoRecordGroupBodyContextProvider.tsx
@@ -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);
diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRecordGroupBodyContextProvider.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRecordGroupBodyContextProvider.tsx
index 8151628f5..68a89348d 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRecordGroupBodyContextProvider.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRecordGroupBodyContextProvider.tsx
@@ -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();
diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableScrollToFocusedElementEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableScrollToFocusedCellEffect.tsx
similarity index 77%
rename from packages/twenty-front/src/modules/object-record/record-table/components/RecordTableScrollToFocusedElementEffect.tsx
rename to packages/twenty-front/src/modules/object-record/record-table/components/RecordTableScrollToFocusedCellEffect.tsx
index b018f500f..0834c04b4 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableScrollToFocusedElementEffect.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableScrollToFocusedCellEffect.tsx
@@ -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]);
diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableScrollToFocusedRowEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableScrollToFocusedRowEffect.tsx
new file mode 100644
index 000000000..23643452d
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableScrollToFocusedRowEffect.tsx
@@ -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;
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useLeaveTableFocus.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useLeaveTableFocus.ts
index 8f3ad47a8..20283baf3 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useLeaveTableFocus.ts
+++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useLeaveTableFocus.ts
@@ -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);
};
};
diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useActiveRecordTableRow.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useActiveRecordTableRow.ts
new file mode 100644
index 000000000..9a586a69c
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useActiveRecordTableRow.ts
@@ -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,
+ };
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useFocusedRecordTableRow.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useFocusedRecordTableRow.ts
new file mode 100644
index 000000000..c05921d24
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useFocusedRecordTableRow.ts
@@ -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,
+ };
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTable.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTable.ts
index 5d307d279..3d7fdb9ec 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTable.ts
+++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTable.ts
@@ -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,
diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTableMove.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTableMove.ts
new file mode 100644
index 000000000..3c66d6033
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTableMove.ts
@@ -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,
+ };
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTableMoveFocus.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTableMoveFocusedCell.ts
similarity index 98%
rename from packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTableMoveFocus.ts
rename to packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTableMoveFocusedCell.ts
index 09cada125..43f8062f2 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTableMoveFocus.ts
+++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTableMoveFocusedCell.ts
@@ -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(
diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTableMoveFocusedRow.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTableMoveFocusedRow.ts
new file mode 100644
index 000000000..cab82d746
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTableMoveFocusedRow.ts
@@ -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,
+ };
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBody.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBody.tsx
index e1a1f6f03..1363ff88c 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBody.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBody.tsx
@@ -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: '';
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyEscapeHotkeyEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyEscapeHotkeyEffect.tsx
index 54272d089..0fa9d56e3 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyEscapeHotkeyEffect.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyEscapeHotkeyEffect.tsx
@@ -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 <>>;
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyFocusKeyboardEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyFocusKeyboardEffect.tsx
index 98e7b90ab..08424ac9c 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyFocusKeyboardEffect.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyFocusKeyboardEffect.tsx
@@ -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 <>>;
};
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyRowFocusKeyboardEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyRowFocusKeyboardEffect.tsx
new file mode 100644
index 000000000..61afee3cc
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyRowFocusKeyboardEffect.tsx
@@ -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 <>>;
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellCheckbox.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellCheckbox.tsx
index c4f7a4eca..20c488cbf 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellCheckbox.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellCheckbox.tsx
@@ -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 (
-
+
-
+
);
};
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellHoveredPortal.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellHoveredPortal.tsx
index 50b5273fb..f649f0a5c 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellHoveredPortal.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellHoveredPortal.tsx
@@ -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 (
-
+
{isFieldInputOnly ? (
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellPortals.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellPortals.tsx
index f48c05abb..48097b0e7 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellPortals.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellPortals.tsx
@@ -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,
);
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableLastEmptyCell.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableLastEmptyCell.tsx
index 2243a41e6..3dc2cac90 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableLastEmptyCell.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableLastEmptyCell.tsx
@@ -4,10 +4,5 @@ import { RecordTableTd } from '@/object-record/record-table/record-table-cell/co
export const RecordTableLastEmptyCell = () => {
const { isSelected } = useRecordTableRowContextOrThrow();
- return (
- <>
-
-
- >
- );
+ return ;
};
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableTd.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableTd.tsx
index 881ef648c..355b63efa 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableTd.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableTd.tsx
@@ -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;
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/__tests__/useSetIsRecordTableFocusActive.test.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/__tests__/useSetIsRecordTableFocusActive.test.tsx
index 4f7769ddb..329049d7d 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/__tests__/useSetIsRecordTableFocusActive.test.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/__tests__/useSetIsRecordTableFocusActive.test.tsx
@@ -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 }) => (
{
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 };
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellFromCell.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellFromCell.ts
index f90bf23de..6015c3024 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellFromCell.ts
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellFromCell.ts
@@ -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 {
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2.ts
index db5f3bee7..3bddfb32a 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2.ts
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2.ts
@@ -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,
],
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useSetIsRecordTableFocusActive.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useSetIsRecordTableFocusActive.ts
index 6461e3f1d..8cf482847 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useSetIsRecordTableFocusActive.ts
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useSetIsRecordTableFocusActive.ts
@@ -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,
);
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx
index 994b24269..f195ec216 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx
@@ -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 (
setIconVisibility(true)}
onMouseLeave={() => setIconVisibility(false)}
+ isFirstRowActiveOrFocused={isFirstRowActiveOrFocused}
>
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCheckboxColumn.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCheckboxColumn.tsx
index 5cdd9ce4e..b7d2a95e3 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCheckboxColumn.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCheckboxColumn.tsx
@@ -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 (
-
+
`
- ${({ 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 (
- <>
-
+
+
{
scope: HIDDEN_TABLE_COLUMN_DROPDOWN_HOTKEY_SCOPE_ID,
}}
/>
-
-
- >
+
+
);
};
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableRow.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableRow.tsx
index b5acdedf9..32c2007e5 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableRow.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableRow.tsx
@@ -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 (
+ {isRowFocusActive && isFocused && }
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableRowHotkeyEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableRowHotkeyEffect.tsx
new file mode 100644
index 000000000..54056f15b
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableRowHotkeyEffect.tsx
@@ -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;
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableTr.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableTr.tsx
index a7aa53d9b..301580143 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableTr.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableTr.tsx
@@ -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;
+} & Omit<
+ React.ComponentProps,
+ '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 (
diff --git a/packages/twenty-front/src/modules/object-record/record-table/states/activeRecordTableRowIndexComponentState.ts b/packages/twenty-front/src/modules/object-record/record-table/states/activeRecordTableRowIndexComponentState.ts
new file mode 100644
index 000000000..4b5dd9ebc
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-table/states/activeRecordTableRowIndexComponentState.ts
@@ -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,
+});
diff --git a/packages/twenty-front/src/modules/object-record/record-table/states/focusedRecordTableRowIndexComponentState.ts b/packages/twenty-front/src/modules/object-record/record-table/states/focusedRecordTableRowIndexComponentState.ts
new file mode 100644
index 000000000..2f9b4a862
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-table/states/focusedRecordTableRowIndexComponentState.ts
@@ -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,
+});
diff --git a/packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableCellFocusActiveComponentState.ts b/packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableCellFocusActiveComponentState.ts
new file mode 100644
index 000000000..f332feb6d
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableCellFocusActiveComponentState.ts
@@ -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({
+ key: 'isRecordTableCellFocusActiveComponentState',
+ defaultValue: false,
+ componentInstanceContext: RecordTableComponentInstanceContext,
+ });
diff --git a/packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState.ts b/packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState.ts
new file mode 100644
index 000000000..21e9288d5
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState.ts
@@ -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({
+ key: 'isRecordTableRowActiveComponentFamilyState',
+ defaultValue: false,
+ componentInstanceContext: RecordTableComponentInstanceContext,
+ });
diff --git a/packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableFocusActiveComponentState.ts b/packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableRowFocusActiveComponentState.ts
similarity index 77%
rename from packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableFocusActiveComponentState.ts
rename to packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableRowFocusActiveComponentState.ts
index 36466471d..5752e9c26 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableFocusActiveComponentState.ts
+++ b/packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableRowFocusActiveComponentState.ts
@@ -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({
- key: 'isRecordTableFocusActiveComponentState',
+ key: 'isRecordTableRowFocusActiveComponentState',
defaultValue: false,
componentInstanceContext: RecordTableComponentInstanceContext,
});
diff --git a/packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState.ts b/packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState.ts
new file mode 100644
index 000000000..aad8da42b
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState.ts
@@ -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({
+ key: 'isRecordTableRowFocusedComponentFamilyState',
+ defaultValue: false,
+ componentInstanceContext: RecordTableComponentInstanceContext,
+ });