Replace hotkey scopes by focus stack (Part 2 - Record Table, Rows and Cells) (#12798)

# Replace hotkey scopes by focus stack (Part 2 - Record Table, Rows and
Cells)

This PR is the second part of a refactoring aiming to deprecate the
hotkey scopes api in favor of the new focus stack api which is more
robust.
Part 1: https://github.com/twentyhq/twenty/pull/12673

The record table shortcuts are no longer centralized in the record
table, they now split and the focused element is in charge of applying
the desired shortcuts. (For instance: The rows are in charge of the row
navigation and the cells of the cells navigation).

## Video QA:


https://github.com/user-attachments/assets/f0bb9eed-8a2a-4b6d-a82f-1998e929f122


## Bugfixes:

### Fix record table click outside not working after opening and closing
a cell

Introduced by https://github.com/twentyhq/twenty/pull/11644

#### Before


https://github.com/user-attachments/assets/d28deda8-15e9-4ffe-b60a-e8b54625f8e5


#### After


https://github.com/user-attachments/assets/3f7e1ffc-15d9-4336-aeb0-bebd8ae0cbe0


### Fix ObjectFilterDropdownFilterInput hotkeys

Introduced by https://github.com/twentyhq/twenty/pull/12673

#### Before


https://github.com/user-attachments/assets/ab2039bd-ebe1-49ba-8377-a6b300664469


#### After


https://github.com/user-attachments/assets/90597453-dab2-426b-a134-0a24b0de0a6b
This commit is contained in:
Raphaël Bosi
2025-06-25 15:18:51 +02:00
committed by GitHub
parent 6450e11f1e
commit 1ab51d41aa
80 changed files with 1246 additions and 847 deletions
@@ -395,6 +395,7 @@ const initializeModalState = ({ set }: { set: SetRecoilState }) => {
enableGlobalHotkeysWithModifiers: true,
enableGlobalHotkeysConflictingWithKeyboard: true,
},
memoizeKey: 'global',
},
]);
};
@@ -1,14 +1,15 @@
import { useCloseDropdownFromOutside } from '@/ui/layout/dropdown/hooks/useCloseDropdownFromOutside';
import { activeDropdownFocusIdState } from '@/ui/layout/dropdown/states/activeDropdownFocusIdState';
import { previousDropdownFocusIdState } from '@/ui/layout/dropdown/states/previousDropdownFocusIdState';
import { useRemoveFocusItemFromFocusStack } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStack';
import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
export const useCloseAnyOpenDropdown = () => {
const { closeDropdownFromOutside } = useCloseDropdownFromOutside();
const { removeFocusItemFromFocusStack } = useRemoveFocusItemFromFocusStack();
const { removeFocusItemFromFocusStackById } =
useRemoveFocusItemFromFocusStackById();
const closeAnyOpenDropdown = useRecoilCallback(
({ snapshot, set }) =>
@@ -33,24 +34,22 @@ export const useCloseAnyOpenDropdown = () => {
if (isDefined(activeDropdownFocusId)) {
closeDropdownFromOutside(activeDropdownFocusId);
removeFocusItemFromFocusStack({
removeFocusItemFromFocusStackById({
focusId: activeDropdownFocusId,
memoizeKey: 'global',
});
}
if (thereIsOneNestedDropdownOpen) {
closeDropdownFromOutside(previousDropdownFocusId);
removeFocusItemFromFocusStack({
removeFocusItemFromFocusStackById({
focusId: previousDropdownFocusId,
memoizeKey: 'global',
});
}
set(previousDropdownFocusIdState, null);
set(activeDropdownFocusIdState, null);
},
[closeDropdownFromOutside, removeFocusItemFromFocusStack],
[closeDropdownFromOutside, removeFocusItemFromFocusStackById],
);
return { closeAnyOpenDropdown };
@@ -4,7 +4,7 @@ import { useDropdownStates } from '@/ui/layout/dropdown/hooks/internal/useDropdo
import { useGoBackToPreviousDropdownFocusId } from '@/ui/layout/dropdown/hooks/useGoBackToPreviousDropdownFocusId';
import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious';
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { useRemoveFocusItemFromFocusStack } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStack';
import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { GlobalHotkeysConfig } from '@/ui/utilities/hotkey/types/GlobalHotkeysConfig';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
@@ -12,7 +12,8 @@ import { useCallback } from 'react';
export const useDropdown = (dropdownId?: string) => {
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
const { removeFocusItemFromFocusStack } = useRemoveFocusItemFromFocusStack();
const { removeFocusItemFromFocusStackById } =
useRemoveFocusItemFromFocusStackById();
const { scopeId, isDropdownOpenState, dropdownPlacementState } =
useDropdownStates({ dropdownScopeId: dropdownId });
@@ -34,16 +35,15 @@ export const useDropdown = (dropdownId?: string) => {
if (isDropdownOpen) {
setIsDropdownOpen(false);
goBackToPreviousDropdownFocusId();
removeFocusItemFromFocusStack({
removeFocusItemFromFocusStackById({
focusId: dropdownId ?? scopeId,
memoizeKey: 'global',
});
}
}, [
isDropdownOpen,
setIsDropdownOpen,
goBackToPreviousDropdownFocusId,
removeFocusItemFromFocusStack,
removeFocusItemFromFocusStackById,
dropdownId,
scopeId,
]);
@@ -63,7 +63,6 @@ export const useDropdown = (dropdownId?: string) => {
globalHotkeysConfig,
// TODO: Remove this once we've fully migrated away from hotkey scopes
hotkeyScope: { scope: 'dropdown' } as HotkeyScope,
memoizeKey: 'global',
});
}
},
@@ -4,7 +4,7 @@ import { useGoBackToPreviousDropdownFocusId } from '@/ui/layout/dropdown/hooks/u
import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious';
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { useRemoveFocusItemFromFocusStack } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStack';
import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { GlobalHotkeysConfig } from '@/ui/utilities/hotkey/types/GlobalHotkeysConfig';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
@@ -15,7 +15,8 @@ export const useDropdownV2 = () => {
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
const { removeFocusItemFromFocusStack } = useRemoveFocusItemFromFocusStack();
const { removeFocusItemFromFocusStackById } =
useRemoveFocusItemFromFocusStackById();
const { setActiveDropdownFocusIdAndMemorizePrevious } =
useSetActiveDropdownFocusIdAndMemorizePrevious();
@@ -30,9 +31,8 @@ export const useDropdownV2 = () => {
.getValue();
if (isDropdownOpen) {
removeFocusItemFromFocusStack({
removeFocusItemFromFocusStackById({
focusId: scopeId,
memoizeKey: 'global',
});
goBackToPreviousDropdownFocusId();
set(
@@ -43,7 +43,7 @@ export const useDropdownV2 = () => {
);
}
},
[removeFocusItemFromFocusStack, goBackToPreviousDropdownFocusId],
[removeFocusItemFromFocusStackById, goBackToPreviousDropdownFocusId],
);
const openDropdown = useRecoilCallback(
@@ -1,36 +0,0 @@
import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious';
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
import { useRecoilCallback } from 'recoil';
export const useOpenDropdownFromOutside = () => {
const { setActiveDropdownFocusIdAndMemorizePrevious } =
useSetActiveDropdownFocusIdAndMemorizePrevious();
const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope();
const openDropdownFromOutside = useRecoilCallback(
({ set }) => {
return (dropdownId: string) => {
const dropdownOpenState = extractComponentState(
isDropdownOpenComponentState,
dropdownId,
);
setActiveDropdownFocusIdAndMemorizePrevious(dropdownId);
setHotkeyScopeAndMemorizePreviousScope({
scope: dropdownId,
});
set(dropdownOpenState, true);
};
},
[
setActiveDropdownFocusIdAndMemorizePrevious,
setHotkeyScopeAndMemorizePreviousScope,
],
);
return { openDropdownFromOutside };
};
@@ -44,6 +44,7 @@ const initializeState = ({ set }: { set: SetRecoilState }) => {
enableGlobalHotkeysWithModifiers: true,
enableGlobalHotkeysConflictingWithKeyboard: true,
},
memoizeKey: 'global',
},
]);
};
@@ -44,6 +44,7 @@ const initializeState = ({ set }: { set: SetRecoilState }) => {
enableGlobalHotkeysWithModifiers: true,
enableGlobalHotkeysConflictingWithKeyboard: true,
},
memoizeKey: 'global',
},
]);
};
@@ -1,13 +1,14 @@
import { ModalHotkeyScope } from '@/ui/layout/modal/components/types/ModalHotkeyScope';
import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpenedComponentState';
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { useRemoveFocusItemFromFocusStack } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStack';
import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { useRecoilCallback } from 'recoil';
export const useModal = () => {
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
const { removeFocusItemFromFocusStack } = useRemoveFocusItemFromFocusStack();
const { removeFocusItemFromFocusStackById } =
useRemoveFocusItemFromFocusStackById();
const closeModal = useRecoilCallback(
({ set, snapshot }) =>
@@ -22,9 +23,8 @@ export const useModal = () => {
return;
}
removeFocusItemFromFocusStack({
removeFocusItemFromFocusStackById({
focusId: modalId,
memoizeKey: modalId,
});
set(
@@ -32,7 +32,7 @@ export const useModal = () => {
false,
);
},
[removeFocusItemFromFocusStack],
[removeFocusItemFromFocusStackById],
);
const openModal = useRecoilCallback(
@@ -1,12 +0,0 @@
import { RIGHT_DRAWER_CLOSE_EVENT_NAME } from '@/ui/layout/right-drawer/utils/emitRightDrawerCloseEvent';
import { useEffect } from 'react';
export const useListenRightDrawerClose = (callback: () => void) => {
useEffect(() => {
window.addEventListener(RIGHT_DRAWER_CLOSE_EVENT_NAME, callback);
return () => {
window.removeEventListener(RIGHT_DRAWER_CLOSE_EVENT_NAME, callback);
};
}, [callback]);
};
@@ -0,0 +1,12 @@
import { SIDE_PANEL_CLOSE_EVENT_NAME } from '@/ui/layout/right-drawer/utils/emitSidePanelCloseEvent';
import { useEffect } from 'react';
export const useListenToSidePanelClosing = (callback: () => void) => {
useEffect(() => {
window.addEventListener(SIDE_PANEL_CLOSE_EVENT_NAME, callback);
return () => {
window.removeEventListener(SIDE_PANEL_CLOSE_EVENT_NAME, callback);
};
}, [callback]);
};
@@ -0,0 +1,12 @@
import { SIDE_PANEL_OPEN_EVENT_NAME } from '@/ui/layout/right-drawer/utils/emitSidePanelOpenEvent';
import { useEffect } from 'react';
export const useListenToSidePanelOpening = (callback: () => void) => {
useEffect(() => {
window.addEventListener(SIDE_PANEL_OPEN_EVENT_NAME, callback);
return () => {
window.removeEventListener(SIDE_PANEL_OPEN_EVENT_NAME, callback);
};
}, [callback]);
};
@@ -1,5 +0,0 @@
export const RIGHT_DRAWER_CLOSE_EVENT_NAME = 'right-drawer-close';
export const emitRightDrawerCloseEvent = () => {
window.dispatchEvent(new CustomEvent(RIGHT_DRAWER_CLOSE_EVENT_NAME));
};
@@ -0,0 +1,5 @@
export const SIDE_PANEL_CLOSE_EVENT_NAME = 'side-panel-close';
export const emitSidePanelCloseEvent = () => {
window.dispatchEvent(new CustomEvent(SIDE_PANEL_CLOSE_EVENT_NAME));
};
@@ -0,0 +1,5 @@
export const SIDE_PANEL_OPEN_EVENT_NAME = 'side-panel-open';
export const emitSidePanelOpenEvent = () => {
window.dispatchEvent(new CustomEvent(SIDE_PANEL_OPEN_EVENT_NAME));
};
@@ -43,6 +43,7 @@ describe('usePushFocusItemToFocusStack', () => {
enableGlobalHotkeysWithModifiers: true,
enableGlobalHotkeysConflictingWithKeyboard: true,
},
memoizeKey: 'global',
};
await act(async () => {
@@ -70,6 +71,7 @@ describe('usePushFocusItemToFocusStack', () => {
enableGlobalHotkeysWithModifiers: true,
enableGlobalHotkeysConflictingWithKeyboard: true,
},
memoizeKey: 'global',
};
await act(async () => {
@@ -1,102 +0,0 @@
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { useRemoveFocusItemFromFocusStack } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStack';
import { currentFocusIdSelector } from '@/ui/utilities/focus/states/currentFocusIdSelector';
import { focusStackState } from '@/ui/utilities/focus/states/focusStackState';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { renderHook } from '@testing-library/react';
import { act } from 'react';
import { RecoilRoot, useRecoilValue } from 'recoil';
const renderHooks = () => {
const { result } = renderHook(
() => {
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
const { removeFocusItemFromFocusStack } =
useRemoveFocusItemFromFocusStack();
const focusStack = useRecoilValue(focusStackState);
const currentFocusId = useRecoilValue(currentFocusIdSelector);
return {
pushFocusItemToFocusStack,
removeFocusItemFromFocusStack,
focusStack,
currentFocusId,
};
},
{
wrapper: RecoilRoot,
},
);
return { result };
};
describe('useRemoveFocusItemFromFocusStack', () => {
it('should remove focus item from the stack', async () => {
const { result } = renderHooks();
const firstFocusItem = {
focusId: 'first-focus-id',
componentInstance: {
componentType: FocusComponentType.MODAL,
componentInstanceId: 'first-instance-id',
},
globalHotkeysConfig: {
enableGlobalHotkeysWithModifiers: true,
enableGlobalHotkeysConflictingWithKeyboard: true,
},
};
const secondFocusItem = {
focusId: 'second-focus-id',
componentInstance: {
componentType: FocusComponentType.MODAL,
componentInstanceId: 'second-instance-id',
},
globalHotkeysConfig: {
enableGlobalHotkeysWithModifiers: true,
enableGlobalHotkeysConflictingWithKeyboard: true,
},
};
await act(async () => {
result.current.pushFocusItemToFocusStack({
focusId: firstFocusItem.focusId,
component: {
type: firstFocusItem.componentInstance.componentType,
instanceId: firstFocusItem.componentInstance.componentInstanceId,
},
hotkeyScope: { scope: 'test-scope' },
memoizeKey: 'global',
});
});
await act(async () => {
result.current.pushFocusItemToFocusStack({
focusId: secondFocusItem.focusId,
component: {
type: secondFocusItem.componentInstance.componentType,
instanceId: secondFocusItem.componentInstance.componentInstanceId,
},
hotkeyScope: { scope: 'test-scope' },
memoizeKey: 'global',
});
});
expect(result.current.focusStack).toEqual([
firstFocusItem,
secondFocusItem,
]);
expect(result.current.currentFocusId).toEqual(secondFocusItem.focusId);
await act(async () => {
result.current.removeFocusItemFromFocusStack({
focusId: firstFocusItem.focusId,
memoizeKey: 'global',
});
});
expect(result.current.focusStack).toEqual([secondFocusItem]);
expect(result.current.currentFocusId).toEqual(secondFocusItem.focusId);
});
});
@@ -0,0 +1,150 @@
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById';
import { currentFocusIdSelector } from '@/ui/utilities/focus/states/currentFocusIdSelector';
import { focusStackState } from '@/ui/utilities/focus/states/focusStackState';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { renderHook } from '@testing-library/react';
import { act } from 'react';
import { RecoilRoot, useRecoilValue } from 'recoil';
const renderHooks = () => {
const { result } = renderHook(
() => {
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
const { removeFocusItemFromFocusStackById } =
useRemoveFocusItemFromFocusStackById();
const focusStack = useRecoilValue(focusStackState);
const currentFocusId = useRecoilValue(currentFocusIdSelector);
return {
pushFocusItemToFocusStack,
removeFocusItemFromFocusStackById,
focusStack,
currentFocusId,
};
},
{
wrapper: RecoilRoot,
},
);
return { result };
};
const firstFocusItem = {
focusId: 'first-focus-id',
componentInstance: {
componentType: FocusComponentType.MODAL,
componentInstanceId: 'first-instance-id',
},
globalHotkeysConfig: {
enableGlobalHotkeysWithModifiers: true,
enableGlobalHotkeysConflictingWithKeyboard: true,
},
memoizeKey: 'global',
};
const secondFocusItem = {
focusId: 'second-focus-id',
componentInstance: {
componentType: FocusComponentType.DROPDOWN,
componentInstanceId: 'second-instance-id',
},
globalHotkeysConfig: {
enableGlobalHotkeysWithModifiers: true,
enableGlobalHotkeysConflictingWithKeyboard: true,
},
memoizeKey: 'global',
};
describe('useRemoveFocusItemFromFocusStackById', () => {
it('should remove focus item from the stack', async () => {
const { result } = renderHooks();
await act(async () => {
result.current.pushFocusItemToFocusStack({
focusId: firstFocusItem.focusId,
component: {
type: firstFocusItem.componentInstance.componentType,
instanceId: firstFocusItem.componentInstance.componentInstanceId,
},
hotkeyScope: { scope: 'test-scope' },
memoizeKey: firstFocusItem.memoizeKey,
});
});
await act(async () => {
result.current.pushFocusItemToFocusStack({
focusId: secondFocusItem.focusId,
component: {
type: secondFocusItem.componentInstance.componentType,
instanceId: secondFocusItem.componentInstance.componentInstanceId,
},
hotkeyScope: { scope: 'test-scope' },
memoizeKey: secondFocusItem.memoizeKey,
});
});
expect(result.current.focusStack).toEqual([
firstFocusItem,
secondFocusItem,
]);
expect(result.current.currentFocusId).toEqual(secondFocusItem.focusId);
await act(async () => {
result.current.removeFocusItemFromFocusStackById({
focusId: firstFocusItem.focusId,
});
});
expect(result.current.focusStack).toEqual([secondFocusItem]);
expect(result.current.currentFocusId).toEqual(secondFocusItem.focusId);
});
it('should handle invalid focusId gracefully without errors', async () => {
const { result } = renderHooks();
await act(async () => {
result.current.pushFocusItemToFocusStack({
focusId: firstFocusItem.focusId,
component: {
type: firstFocusItem.componentInstance.componentType,
instanceId: firstFocusItem.componentInstance.componentInstanceId,
},
hotkeyScope: { scope: 'test-scope' },
memoizeKey: 'global',
});
});
await act(async () => {
result.current.pushFocusItemToFocusStack({
focusId: secondFocusItem.focusId,
component: {
type: secondFocusItem.componentInstance.componentType,
instanceId: secondFocusItem.componentInstance.componentInstanceId,
},
hotkeyScope: { scope: 'test-scope' },
memoizeKey: 'global',
});
});
const originalFocusStack = result.current.focusStack;
const originalCurrentFocusId = result.current.currentFocusId;
await act(async () => {
expect(() => {
result.current.removeFocusItemFromFocusStackById({
focusId: 'invalid-focus-id',
});
}).not.toThrow();
});
expect(result.current.focusStack).toEqual(originalFocusStack);
expect(result.current.currentFocusId).toEqual(originalCurrentFocusId);
expect(result.current.focusStack).toEqual([
firstFocusItem,
secondFocusItem,
]);
expect(result.current.currentFocusId).toEqual(secondFocusItem.focusId);
});
});
@@ -44,6 +44,7 @@ describe('useResetFocusStack', () => {
enableGlobalHotkeysWithModifiers: true,
enableGlobalHotkeysConflictingWithKeyboard: true,
},
memoizeKey: 'global',
};
await act(async () => {
@@ -44,6 +44,7 @@ describe('useResetFocusStackToFocusItem', () => {
enableGlobalHotkeysWithModifiers: true,
enableGlobalHotkeysConflictingWithKeyboard: true,
},
memoizeKey: 'global',
};
const secondFocusItem = {
@@ -56,6 +57,7 @@ describe('useResetFocusStackToFocusItem', () => {
enableGlobalHotkeysWithModifiers: true,
enableGlobalHotkeysConflictingWithKeyboard: true,
},
memoizeKey: 'global',
};
await act(async () => {
@@ -66,7 +68,7 @@ describe('useResetFocusStackToFocusItem', () => {
instanceId: firstFocusItem.componentInstance.componentInstanceId,
},
hotkeyScope: { scope: 'test-scope' },
memoizeKey: 'global',
memoizeKey: firstFocusItem.memoizeKey,
});
});
@@ -78,7 +80,7 @@ describe('useResetFocusStackToFocusItem', () => {
instanceId: secondFocusItem.componentInstance.componentInstanceId,
},
hotkeyScope: { scope: 'test-scope' },
memoizeKey: 'global',
memoizeKey: secondFocusItem.memoizeKey,
});
});
@@ -92,7 +94,7 @@ describe('useResetFocusStackToFocusItem', () => {
result.current.resetFocusStackToFocusItem({
focusStackItem: firstFocusItem,
hotkeyScope: { scope: 'test-scope' },
memoizeKey: 'global',
memoizeKey: firstFocusItem.memoizeKey,
});
});
@@ -42,7 +42,7 @@ export const usePushFocusItemToFocusStack = () => {
globalHotkeysConfig?: Partial<GlobalHotkeysConfig>;
// TODO: Remove this once we've migrated hotkey scopes to the new api
hotkeyScope: HotkeyScope;
memoizeKey: string;
memoizeKey?: string;
}) => {
const focusStackItem: FocusStackItem = {
focusId,
@@ -57,6 +57,8 @@ export const usePushFocusItemToFocusStack = () => {
globalHotkeysConfig?.enableGlobalHotkeysConflictingWithKeyboard ??
true,
},
// TODO: Remove this once we've migrated hotkey scopes to the new api
memoizeKey,
};
const currentFocusStack = snapshot
@@ -0,0 +1,55 @@
import { DEBUG_FOCUS_STACK } from '@/ui/utilities/focus/constants/DebugFocusStack';
import { focusStackState } from '@/ui/utilities/focus/states/focusStackState';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { useRecoilCallback } from 'recoil';
import { logDebug } from '~/utils/logDebug';
export const useRemoveLastFocusItemFromFocusStackByComponentType = () => {
const { goBackToPreviousHotkeyScope } = usePreviousHotkeyScope();
const removeLastFocusItemFromFocusStackByComponentType = useRecoilCallback(
({ snapshot, set }) =>
({ componentType }: { componentType: FocusComponentType }) => {
const focusStack = snapshot.getLoadable(focusStackState).getValue();
const lastMatchingIndex = focusStack.findLastIndex(
(focusStackItem) =>
focusStackItem.componentInstance.componentType === componentType,
);
if (lastMatchingIndex === -1) {
if (DEBUG_FOCUS_STACK) {
logDebug(
`DEBUG: removeFocusItemFromFocusStackByComponentType - no item found for type ${componentType}`,
{ focusStack },
);
}
return;
}
const removedFocusItem = focusStack[lastMatchingIndex];
const newFocusStack = focusStack.filter(
(_, index) => index !== lastMatchingIndex,
);
set(focusStackState, newFocusStack);
if (DEBUG_FOCUS_STACK) {
logDebug(
`DEBUG: removeFocusItemFromFocusStackByComponentType ${componentType}`,
{
removedFocusItem,
newFocusStack,
},
);
}
// TODO: Remove this once we've migrated hotkey scopes to the new api
goBackToPreviousHotkeyScope(removedFocusItem.memoizeKey);
},
[goBackToPreviousHotkeyScope],
);
return { removeLastFocusItemFromFocusStackByComponentType };
};
@@ -4,14 +4,22 @@ import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousH
import { useRecoilCallback } from 'recoil';
import { logDebug } from '~/utils/logDebug';
export const useRemoveFocusItemFromFocusStack = () => {
export const useRemoveFocusItemFromFocusStackById = () => {
const { goBackToPreviousHotkeyScope } = usePreviousHotkeyScope();
const removeFocusItemFromFocusStack = useRecoilCallback(
const removeFocusItemFromFocusStackById = useRecoilCallback(
({ snapshot, set }) =>
({ focusId, memoizeKey }: { focusId: string; memoizeKey: string }) => {
({ focusId }: { focusId: string }) => {
const focusStack = snapshot.getLoadable(focusStackState).getValue();
const removedFocusItem = focusStack.find(
(focusStackItem) => focusStackItem.focusId === focusId,
);
if (!removedFocusItem) {
return;
}
const newFocusStack = focusStack.filter(
(focusStackItem) => focusStackItem.focusId !== focusId,
);
@@ -25,10 +33,10 @@ export const useRemoveFocusItemFromFocusStack = () => {
}
// TODO: Remove this once we've migrated hotkey scopes to the new api
goBackToPreviousHotkeyScope(memoizeKey);
goBackToPreviousHotkeyScope(removedFocusItem.memoizeKey);
},
[goBackToPreviousHotkeyScope],
);
return { removeFocusItemFromFocusStack };
return { removeFocusItemFromFocusStackById };
};
@@ -1,23 +1,25 @@
import { DEBUG_FOCUS_STACK } from '@/ui/utilities/focus/constants/DebugFocusStack';
import { focusStackState } from '@/ui/utilities/focus/states/focusStackState';
import { FocusStackItem } from '@/ui/utilities/focus/types/FocusStackItem';
import { currentHotkeyScopeState } from '@/ui/utilities/hotkey/states/internal/currentHotkeyScopeState';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { previousHotkeyScopeFamilyState } from '@/ui/utilities/hotkey/states/internal/previousHotkeyScopeFamilyState';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { useRecoilCallback } from 'recoil';
import { logDebug } from '~/utils/logDebug';
export const useResetFocusStackToFocusItem = () => {
const setHotkeyScope = useSetHotkeyScope();
const resetFocusStackToFocusItem = useRecoilCallback(
({ set }) =>
({
focusStackItem,
hotkeyScope,
memoizeKey,
memoizeKey = 'global',
}: {
focusStackItem: FocusStackItem;
hotkeyScope: HotkeyScope;
memoizeKey: string;
memoizeKey?: string;
}) => {
set(focusStackState, [focusStackItem]);
@@ -29,9 +31,9 @@ export const useResetFocusStackToFocusItem = () => {
// TODO: Remove this once we've migrated hotkey scopes to the new api
set(previousHotkeyScopeFamilyState(memoizeKey), null);
set(currentHotkeyScopeState, hotkeyScope);
setHotkeyScope(hotkeyScope.scope, hotkeyScope.customScopes);
},
[],
[setHotkeyScope],
);
return { resetFocusStackToFocusItem };
@@ -3,4 +3,8 @@ export enum FocusComponentType {
DROPDOWN = 'dropdown',
SIDE_PANEL = 'side-panel',
OPEN_FIELD_INPUT = 'open-field-input',
PAGE = 'page',
RECORD_TABLE = 'record-table',
RECORD_TABLE_ROW = 'record-table-row',
RECORD_TABLE_CELL = 'record-table-cell',
}
@@ -5,4 +5,5 @@ export type FocusStackItem = {
focusId: string;
componentInstance: FocusComponentInstance;
globalHotkeysConfig: GlobalHotkeysConfig;
memoizeKey: string;
};