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:
+1
@@ -395,6 +395,7 @@ const initializeModalState = ({ set }: { set: SetRecoilState }) => {
|
||||
enableGlobalHotkeysWithModifiers: true,
|
||||
enableGlobalHotkeysConflictingWithKeyboard: true,
|
||||
},
|
||||
memoizeKey: 'global',
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
+6
-7
@@ -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(
|
||||
|
||||
-36
@@ -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 };
|
||||
};
|
||||
+1
@@ -44,6 +44,7 @@ const initializeState = ({ set }: { set: SetRecoilState }) => {
|
||||
enableGlobalHotkeysWithModifiers: true,
|
||||
enableGlobalHotkeysConflictingWithKeyboard: true,
|
||||
},
|
||||
memoizeKey: 'global',
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
+1
@@ -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(
|
||||
|
||||
-12
@@ -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]);
|
||||
};
|
||||
+12
@@ -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]);
|
||||
};
|
||||
+12
@@ -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]);
|
||||
};
|
||||
-5
@@ -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));
|
||||
};
|
||||
+5
@@ -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));
|
||||
};
|
||||
+5
@@ -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));
|
||||
};
|
||||
+2
@@ -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 () => {
|
||||
|
||||
-102
@@ -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);
|
||||
});
|
||||
});
|
||||
+150
@@ -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);
|
||||
});
|
||||
});
|
||||
+1
@@ -44,6 +44,7 @@ describe('useResetFocusStack', () => {
|
||||
enableGlobalHotkeysWithModifiers: true,
|
||||
enableGlobalHotkeysConflictingWithKeyboard: true,
|
||||
},
|
||||
memoizeKey: 'global',
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
|
||||
+5
-3
@@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+3
-1
@@ -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
|
||||
|
||||
+55
@@ -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 };
|
||||
};
|
||||
+13
-5
@@ -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 };
|
||||
};
|
||||
+7
-5
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user