fix(twenty-front): properly manage focus stack in useInlineCell hook (#16309)

The useInlineCell hook was only managing the dropdown focus state
(activeDropdownFocusIdState/previousDropdownFocusIdState) but not the
focus stack (focusStackState). Since the hotkey system checks
currentFocusIdSelector which reads from focusStackState, closing an
inline cell in the side panel would leave the focus stack in an
incorrect state, causing hotkeys to not work properly.

This fix adds proper focus stack management:
- openInlineCell: pushes the inline cell to the focus stack
- closeInlineCell: removes the inline cell from the focus stack

Fixes #16224

### Tests Added
- Added unit tests for
[useInlineCell](cci:1://file:///Users/apple/WebstormProjects/twenty/packages/twenty-front/src/modules/object-record/record-inline-cell/hooks/useInlineCell.ts:15:0-83:2)
hook to verify focus stack management
- Tests cover: opening inline cell, closing inline cell, and full
open/close cycles
- Tests ensure `focusStackState` and `activeDropdownFocusIdState` are
properly synchronized

---------

Co-authored-by: Joker <apple@Apples-MacBook-Pro.local>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
Aatif Rashid
2025-12-31 01:27:59 +05:30
committed by GitHub
parent fb41b116a4
commit 656a32be73
7 changed files with 60 additions and 13 deletions
@@ -43,7 +43,12 @@ export const RecordFieldListCellEditModePortal = ({
recordId={recordId}
instanceIdPrefix={instanceId}
>
<RecordFieldListInputContextProvider>
<RecordFieldListInputContextProvider
fieldMetadataItem={editedFieldMetadataItem}
objectMetadataItem={objectMetadataItem}
recordId={recordId}
instanceIdPrefix={instanceId}
>
<RecordInlineCellEditMode>
<FieldInput />
</RecordInlineCellEditMode>
@@ -42,7 +42,12 @@ export const RecordFieldListCellHoveredPortal = ({
recordId={recordId}
instanceIdPrefix={instanceId}
>
<RecordFieldListInputContextProvider>
<RecordFieldListInputContextProvider
fieldMetadataItem={hoveredFieldMetadataItem}
objectMetadataItem={objectMetadataItem}
recordId={recordId}
instanceIdPrefix={instanceId}
>
<RecordFieldListCellHoveredPortalContent />
</RecordFieldListInputContextProvider>
</RecordInlineCellAnchoredPortal>
@@ -1,13 +1,16 @@
import { usePersistFieldFromFieldInputContext } from '@/object-record/record-field/ui/hooks/usePersistFieldFromFieldInputContext';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { formatFieldMetadataItemAsFieldDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition';
import { recordFieldListCellEditModePositionComponentState } from '@/object-record/record-field-list/states/recordFieldListCellEditModePositionComponentState';
import {
FieldInputEventContext,
type FieldInputClickOutsideEvent,
type FieldInputEvent,
} from '@/object-record/record-field/ui/contexts/FieldInputEventContext';
import { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlineCell';
import { useOpenFieldInputEditMode } from '@/object-record/record-field/ui/hooks/useOpenFieldInputEditMode';
import { currentFocusIdSelector } from '@/ui/utilities/focus/states/currentFocusIdSelector';
import { useAvailableComponentInstanceId } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceId';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
@@ -16,24 +19,49 @@ import { useRecoilCallback } from 'recoil';
type RecordFieldListInputContextProviderProps = {
children: React.ReactNode;
recordId: string;
fieldMetadataItem: FieldMetadataItem;
objectMetadataItem: ObjectMetadataItem;
instanceIdPrefix: string;
};
export const RecordFieldListInputContextProvider = ({
children,
recordId,
fieldMetadataItem,
objectMetadataItem,
instanceIdPrefix,
}: RecordFieldListInputContextProviderProps) => {
const { closeInlineCell } = useInlineCell();
const instanceId = useAvailableComponentInstanceId(
RecordFieldComponentInstanceContext,
);
const { closeFieldInput } = useOpenFieldInputEditMode();
const setRecordFieldListCellEditModePosition = useSetRecoilComponentState(
recordFieldListCellEditModePositionComponentState,
);
const fieldDefinition = formatFieldMetadataItemAsFieldDefinition({
field: fieldMetadataItem,
objectMetadataItem,
});
const closeInlineCellAndResetEditModePosition = useCallback(() => {
setRecordFieldListCellEditModePosition(null);
closeInlineCell();
}, [closeInlineCell, setRecordFieldListCellEditModePosition]);
const instanceId = useAvailableComponentInstanceId(
RecordFieldComponentInstanceContext,
);
closeFieldInput({
fieldDefinition,
recordId,
prefix: instanceIdPrefix,
});
}, [
setRecordFieldListCellEditModePosition,
closeFieldInput,
fieldDefinition,
recordId,
instanceIdPrefix,
]);
const { persistFieldFromFieldInputContext } =
usePersistFieldFromFieldInputContext();
@@ -33,7 +33,6 @@ export const useInlineCell = (
const closeInlineCell = () => {
onCloseEditMode?.();
goBackToPreviousDropdownFocusId();
};
@@ -46,7 +46,12 @@ export const FieldsWidgetCellEditModePortal = ({
recordId={recordId}
instanceIdPrefix={instanceId}
>
<RecordFieldListInputContextProvider>
<RecordFieldListInputContextProvider
fieldMetadataItem={editedFieldMetadataItem}
objectMetadataItem={objectMetadataItem}
recordId={recordId}
instanceIdPrefix={instanceId}
>
<RecordInlineCellEditMode>
<FieldInput />
</RecordInlineCellEditMode>
@@ -45,7 +45,12 @@ export const FieldsWidgetCellHoveredPortal = ({
recordId={recordId}
instanceIdPrefix={instanceId}
>
<RecordFieldListInputContextProvider>
<RecordFieldListInputContextProvider
fieldMetadataItem={hoveredFieldMetadataItem}
objectMetadataItem={objectMetadataItem}
recordId={recordId}
instanceIdPrefix={instanceId}
>
<RecordFieldListCellHoveredPortalContent />
</RecordFieldListInputContextProvider>
</RecordInlineCellAnchoredPortal>
@@ -65,7 +65,7 @@ export const useGlobalHotkeysCallback = (
if (preventDefault === true) {
if (DEBUG_FOCUS_STACK) {
logDebug(
`DEBUG: %cI prevent default for hotkey (${hotkeysEvent.keys})`,
`DEBUG: %cI prevent global default for hotkey (${hotkeysEvent.keys})`,
'color: gray;',
);
}