Introduce focus stack to handle hotkeys (#12166)
# Introduce focus stack to handle hotkeys This PR introduces a focus stack to track the order in which the elements are focused: - Each focused element has a unique focus id - When an element is focused, it is pushed on top of the stack - When an element loses focus, we remove it from the stack This focus stack is then used to determine which hotkeys are available. The previous implementation lead to many regressions because of race conditions, of wrong order of open and close operations and by overwriting previous states. This implementation should be way more robust than the previous one. The new api can be incrementally implemented since it preserves backwards compatibility by writing to the old hotkey scopes states. For now, it has been implemented on the modal components. To test this PR, verify that the shortcuts still work correctly, especially for the modal components.
This commit is contained in:
@ -0,0 +1,90 @@
|
||||
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
|
||||
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 pushFocusItem = usePushFocusItemToFocusStack();
|
||||
const focusStack = useRecoilValue(focusStackState);
|
||||
const currentFocusId = useRecoilValue(currentFocusIdSelector);
|
||||
|
||||
return {
|
||||
pushFocusItem,
|
||||
focusStack,
|
||||
currentFocusId,
|
||||
};
|
||||
},
|
||||
{
|
||||
wrapper: RecoilRoot,
|
||||
},
|
||||
);
|
||||
|
||||
return { result };
|
||||
};
|
||||
|
||||
describe('usePushFocusItemToFocusStack', () => {
|
||||
it('should push focus item to the stack', async () => {
|
||||
const { result } = renderHooks();
|
||||
|
||||
expect(result.current.focusStack).toEqual([]);
|
||||
|
||||
const focusItem = {
|
||||
focusId: 'test-focus-id',
|
||||
componentInstance: {
|
||||
componentType: FocusComponentType.MODAL,
|
||||
componentInstanceId: 'test-instance-id',
|
||||
},
|
||||
globalHotkeysConfig: {
|
||||
enableGlobalHotkeysWithModifiers: true,
|
||||
enableGlobalHotkeysConflictingWithKeyboard: true,
|
||||
},
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
result.current.pushFocusItem({
|
||||
focusId: focusItem.focusId,
|
||||
component: {
|
||||
type: focusItem.componentInstance.componentType,
|
||||
instanceId: focusItem.componentInstance.componentInstanceId,
|
||||
},
|
||||
hotkeyScope: { scope: 'test-scope' },
|
||||
memoizeKey: 'global',
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current.focusStack).toEqual([focusItem]);
|
||||
expect(result.current.currentFocusId).toEqual(focusItem.focusId);
|
||||
|
||||
const anotherFocusItem = {
|
||||
focusId: 'another-focus-id',
|
||||
componentInstance: {
|
||||
componentType: FocusComponentType.MODAL,
|
||||
componentInstanceId: 'another-instance-id',
|
||||
},
|
||||
globalHotkeysConfig: {
|
||||
enableGlobalHotkeysWithModifiers: true,
|
||||
enableGlobalHotkeysConflictingWithKeyboard: true,
|
||||
},
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
result.current.pushFocusItem({
|
||||
focusId: anotherFocusItem.focusId,
|
||||
component: {
|
||||
type: anotherFocusItem.componentInstance.componentType,
|
||||
instanceId: anotherFocusItem.componentInstance.componentInstanceId,
|
||||
},
|
||||
hotkeyScope: { scope: 'test-scope' },
|
||||
memoizeKey: 'global',
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current.focusStack).toEqual([focusItem, anotherFocusItem]);
|
||||
expect(result.current.currentFocusId).toEqual(anotherFocusItem.focusId);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,101 @@
|
||||
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
|
||||
import { useRemoveFocusIdFromFocusStack } from '@/ui/utilities/focus/hooks/useRemoveFocusIdFromFocusStack';
|
||||
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 pushFocusItem = usePushFocusItemToFocusStack();
|
||||
const removeFocusId = useRemoveFocusIdFromFocusStack();
|
||||
const focusStack = useRecoilValue(focusStackState);
|
||||
const currentFocusId = useRecoilValue(currentFocusIdSelector);
|
||||
|
||||
return {
|
||||
pushFocusItem,
|
||||
removeFocusId,
|
||||
focusStack,
|
||||
currentFocusId,
|
||||
};
|
||||
},
|
||||
{
|
||||
wrapper: RecoilRoot,
|
||||
},
|
||||
);
|
||||
|
||||
return { result };
|
||||
};
|
||||
|
||||
describe('useRemoveFocusIdFromFocusStack', () => {
|
||||
it('should remove focus id 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.pushFocusItem({
|
||||
focusId: firstFocusItem.focusId,
|
||||
component: {
|
||||
type: firstFocusItem.componentInstance.componentType,
|
||||
instanceId: firstFocusItem.componentInstance.componentInstanceId,
|
||||
},
|
||||
hotkeyScope: { scope: 'test-scope' },
|
||||
memoizeKey: 'global',
|
||||
});
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
result.current.pushFocusItem({
|
||||
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.removeFocusId({
|
||||
focusId: firstFocusItem.focusId,
|
||||
memoizeKey: 'global',
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current.focusStack).toEqual([secondFocusItem]);
|
||||
expect(result.current.currentFocusId).toEqual(secondFocusItem.focusId);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,71 @@
|
||||
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
|
||||
import { useResetFocusStack } from '@/ui/utilities/focus/hooks/useResetFocusStack';
|
||||
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 pushFocusItem = usePushFocusItemToFocusStack();
|
||||
const resetFocusStack = useResetFocusStack();
|
||||
const focusStack = useRecoilValue(focusStackState);
|
||||
const currentFocusId = useRecoilValue(currentFocusIdSelector);
|
||||
|
||||
return {
|
||||
pushFocusItem,
|
||||
resetFocusStack,
|
||||
focusStack,
|
||||
currentFocusId,
|
||||
};
|
||||
},
|
||||
{
|
||||
wrapper: RecoilRoot,
|
||||
},
|
||||
);
|
||||
|
||||
return { result };
|
||||
};
|
||||
|
||||
describe('useResetFocusStack', () => {
|
||||
it('should reset the focus stack', async () => {
|
||||
const { result } = renderHooks();
|
||||
|
||||
const focusItem = {
|
||||
focusId: 'test-focus-id',
|
||||
componentInstance: {
|
||||
componentType: FocusComponentType.MODAL,
|
||||
componentInstanceId: 'test-instance-id',
|
||||
},
|
||||
globalHotkeysConfig: {
|
||||
enableGlobalHotkeysWithModifiers: true,
|
||||
enableGlobalHotkeysConflictingWithKeyboard: true,
|
||||
},
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
result.current.pushFocusItem({
|
||||
focusId: focusItem.focusId,
|
||||
component: {
|
||||
type: focusItem.componentInstance.componentType,
|
||||
instanceId: focusItem.componentInstance.componentInstanceId,
|
||||
},
|
||||
hotkeyScope: { scope: 'test-scope' },
|
||||
memoizeKey: 'global',
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current.focusStack).toEqual([focusItem]);
|
||||
expect(result.current.currentFocusId).toEqual(focusItem.focusId);
|
||||
|
||||
await act(async () => {
|
||||
result.current.resetFocusStack();
|
||||
});
|
||||
|
||||
expect(result.current.focusStack).toEqual([]);
|
||||
expect(result.current.currentFocusId).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,102 @@
|
||||
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
|
||||
import { useResetFocusStackToFocusItem } from '@/ui/utilities/focus/hooks/useResetFocusStackToFocusItem';
|
||||
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 pushFocusItem = usePushFocusItemToFocusStack();
|
||||
const resetFocusStackToFocusItem = useResetFocusStackToFocusItem();
|
||||
const focusStack = useRecoilValue(focusStackState);
|
||||
const currentFocusId = useRecoilValue(currentFocusIdSelector);
|
||||
|
||||
return {
|
||||
pushFocusItem,
|
||||
resetFocusStackToFocusItem,
|
||||
focusStack,
|
||||
currentFocusId,
|
||||
};
|
||||
},
|
||||
{
|
||||
wrapper: RecoilRoot,
|
||||
},
|
||||
);
|
||||
|
||||
return { result };
|
||||
};
|
||||
|
||||
describe('useResetFocusStackToFocusItem', () => {
|
||||
it('should reset the focus stack to a specific focus item', 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.pushFocusItem({
|
||||
focusId: firstFocusItem.focusId,
|
||||
component: {
|
||||
type: firstFocusItem.componentInstance.componentType,
|
||||
instanceId: firstFocusItem.componentInstance.componentInstanceId,
|
||||
},
|
||||
hotkeyScope: { scope: 'test-scope' },
|
||||
memoizeKey: 'global',
|
||||
});
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
result.current.pushFocusItem({
|
||||
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.resetFocusStackToFocusItem({
|
||||
focusStackItem: firstFocusItem,
|
||||
hotkeyScope: { scope: 'test-scope' },
|
||||
memoizeKey: 'global',
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current.focusStack).toEqual([firstFocusItem]);
|
||||
expect(result.current.currentFocusId).toEqual(firstFocusItem.focusId);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,71 @@
|
||||
import { focusStackState } from '@/ui/utilities/focus/states/focusStackState';
|
||||
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
|
||||
import { FocusStackItem } from '@/ui/utilities/focus/types/FocusStackItem';
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
import { GlobalHotkeysConfig } from '@/ui/utilities/hotkey/types/GlobalHotkeysConfig';
|
||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
export const usePushFocusItemToFocusStack = () => {
|
||||
const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope();
|
||||
|
||||
const addOrMoveItemToTheTopOfTheStack = useRecoilCallback(
|
||||
({ set }) =>
|
||||
(focusStackItem: FocusStackItem) => {
|
||||
set(focusStackState, (currentFocusStack) => [
|
||||
...currentFocusStack.filter(
|
||||
(currentFocusStackItem) =>
|
||||
currentFocusStackItem.focusId !== focusStackItem.focusId,
|
||||
),
|
||||
focusStackItem,
|
||||
]);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return useRecoilCallback(
|
||||
() =>
|
||||
({
|
||||
focusId,
|
||||
component,
|
||||
hotkeyScope,
|
||||
memoizeKey = 'global',
|
||||
globalHotkeysConfig,
|
||||
}: {
|
||||
focusId: string;
|
||||
component: {
|
||||
type: FocusComponentType;
|
||||
instanceId: string;
|
||||
};
|
||||
globalHotkeysConfig?: Partial<GlobalHotkeysConfig>;
|
||||
// TODO: Remove this once we've migrated hotkey scopes to the new api
|
||||
hotkeyScope: HotkeyScope;
|
||||
memoizeKey: string;
|
||||
}) => {
|
||||
const focusStackItem: FocusStackItem = {
|
||||
focusId,
|
||||
componentInstance: {
|
||||
componentType: component.type,
|
||||
componentInstanceId: component.instanceId,
|
||||
},
|
||||
globalHotkeysConfig: {
|
||||
enableGlobalHotkeysWithModifiers:
|
||||
globalHotkeysConfig?.enableGlobalHotkeysWithModifiers ?? true,
|
||||
enableGlobalHotkeysConflictingWithKeyboard:
|
||||
globalHotkeysConfig?.enableGlobalHotkeysConflictingWithKeyboard ??
|
||||
true,
|
||||
},
|
||||
};
|
||||
|
||||
addOrMoveItemToTheTopOfTheStack(focusStackItem);
|
||||
|
||||
// TODO: Remove this once we've migrated hotkey scopes to the new api
|
||||
setHotkeyScopeAndMemorizePreviousScope({
|
||||
scope: hotkeyScope.scope,
|
||||
customScopes: hotkeyScope.customScopes,
|
||||
memoizeKey,
|
||||
});
|
||||
},
|
||||
[setHotkeyScopeAndMemorizePreviousScope, addOrMoveItemToTheTopOfTheStack],
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,22 @@
|
||||
import { focusStackState } from '@/ui/utilities/focus/states/focusStackState';
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
export const useRemoveFocusIdFromFocusStack = () => {
|
||||
const { goBackToPreviousHotkeyScope } = usePreviousHotkeyScope();
|
||||
|
||||
return useRecoilCallback(
|
||||
({ set }) =>
|
||||
({ focusId, memoizeKey }: { focusId: string; memoizeKey: string }) => {
|
||||
set(focusStackState, (previousFocusStack) =>
|
||||
previousFocusStack.filter(
|
||||
(focusStackItem) => focusStackItem.focusId !== focusId,
|
||||
),
|
||||
);
|
||||
|
||||
// TODO: Remove this once we've migrated hotkey scopes to the new api
|
||||
goBackToPreviousHotkeyScope(memoizeKey);
|
||||
},
|
||||
[goBackToPreviousHotkeyScope],
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,18 @@
|
||||
import { focusStackState } from '@/ui/utilities/focus/states/focusStackState';
|
||||
import { currentHotkeyScopeState } from '@/ui/utilities/hotkey/states/internal/currentHotkeyScopeState';
|
||||
import { previousHotkeyScopeFamilyState } from '@/ui/utilities/hotkey/states/internal/previousHotkeyScopeFamilyState';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
export const useResetFocusStack = () => {
|
||||
return useRecoilCallback(
|
||||
({ reset }) =>
|
||||
(memoizeKey = 'global') => {
|
||||
reset(focusStackState);
|
||||
|
||||
// TODO: Remove this once we've migrated hotkey scopes to the new api
|
||||
reset(previousHotkeyScopeFamilyState(memoizeKey as string));
|
||||
reset(currentHotkeyScopeState);
|
||||
},
|
||||
[],
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,28 @@
|
||||
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 { previousHotkeyScopeFamilyState } from '@/ui/utilities/hotkey/states/internal/previousHotkeyScopeFamilyState';
|
||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
export const useResetFocusStackToFocusItem = () => {
|
||||
return useRecoilCallback(
|
||||
({ set }) =>
|
||||
({
|
||||
focusStackItem,
|
||||
hotkeyScope,
|
||||
memoizeKey,
|
||||
}: {
|
||||
focusStackItem: FocusStackItem;
|
||||
hotkeyScope: HotkeyScope;
|
||||
memoizeKey: string;
|
||||
}) => {
|
||||
set(focusStackState, [focusStackItem]);
|
||||
|
||||
// TODO: Remove this once we've migrated hotkey scopes to the new api
|
||||
set(previousHotkeyScopeFamilyState(memoizeKey), null);
|
||||
set(currentHotkeyScopeState, hotkeyScope);
|
||||
},
|
||||
[],
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,11 @@
|
||||
import { selector } from 'recoil';
|
||||
import { focusStackState } from './focusStackState';
|
||||
|
||||
export const currentFocusIdSelector = selector<string | undefined>({
|
||||
key: 'currentFocusIdSelector',
|
||||
get: ({ get }) => {
|
||||
const focusStack = get(focusStackState);
|
||||
|
||||
return focusStack.at(-1)?.focusId;
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,21 @@
|
||||
import { focusStackState } from '@/ui/utilities/focus/states/focusStackState';
|
||||
import { DEFAULT_GLOBAL_HOTKEYS_CONFIG } from '@/ui/utilities/hotkey/constants/DefaultGlobalHotkeysConfig';
|
||||
import { GlobalHotkeysConfig } from '@/ui/utilities/hotkey/types/GlobalHotkeysConfig';
|
||||
import { selector } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const currentGlobalHotkeysConfigSelector = selector<GlobalHotkeysConfig>(
|
||||
{
|
||||
key: 'currentGlobalHotkeysConfigSelector',
|
||||
get: ({ get }) => {
|
||||
const focusStack = get(focusStackState);
|
||||
const lastFocusStackItem = focusStack.at(-1);
|
||||
|
||||
if (!isDefined(lastFocusStackItem)) {
|
||||
return DEFAULT_GLOBAL_HOTKEYS_CONFIG;
|
||||
}
|
||||
|
||||
return lastFocusStackItem.globalHotkeysConfig;
|
||||
},
|
||||
},
|
||||
);
|
||||
@ -0,0 +1,7 @@
|
||||
import { FocusStackItem } from '@/ui/utilities/focus/types/FocusStackItem';
|
||||
import { createState } from 'twenty-ui/utilities';
|
||||
|
||||
export const focusStackState = createState<FocusStackItem[]>({
|
||||
key: 'focusStackState',
|
||||
defaultValue: [],
|
||||
});
|
||||
@ -0,0 +1,6 @@
|
||||
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
|
||||
|
||||
export type FocusComponentInstance = {
|
||||
componentType: FocusComponentType;
|
||||
componentInstanceId: string;
|
||||
};
|
||||
@ -0,0 +1,3 @@
|
||||
export enum FocusComponentType {
|
||||
MODAL = 'modal',
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
import { FocusComponentInstance } from '@/ui/utilities/focus/types/FocusComponentInstance';
|
||||
import { GlobalHotkeysConfig } from '@/ui/utilities/hotkey/types/GlobalHotkeysConfig';
|
||||
|
||||
export type FocusStackItem = {
|
||||
focusId: string;
|
||||
componentInstance: FocusComponentInstance;
|
||||
globalHotkeysConfig: GlobalHotkeysConfig;
|
||||
};
|
||||
@ -0,0 +1 @@
|
||||
export const DEBUG_HOTKEY_SCOPE = false;
|
||||
@ -0,0 +1,6 @@
|
||||
import { GlobalHotkeysConfig } from '@/ui/utilities/hotkey/types/GlobalHotkeysConfig';
|
||||
|
||||
export const DEFAULT_GLOBAL_HOTKEYS_CONFIG: GlobalHotkeysConfig = {
|
||||
enableGlobalHotkeysWithModifiers: true,
|
||||
enableGlobalHotkeysConflictingWithKeyboard: true,
|
||||
};
|
||||
@ -0,0 +1,74 @@
|
||||
import { useGlobalHotkeysCallback } from '@/ui/utilities/hotkey/hooks/useGlobalHotkeysCallback';
|
||||
import { pendingHotkeyState } from '@/ui/utilities/hotkey/states/internal/pendingHotkeysState';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { HotkeyCallback, Keys, Options } from 'react-hotkeys-hook/dist/types';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type UseHotkeysOptionsWithoutBuggyOptions = Omit<Options, 'enabled'>;
|
||||
|
||||
export const useGlobalHotkeys = (
|
||||
keys: Keys,
|
||||
callback: HotkeyCallback,
|
||||
containsModifier: boolean,
|
||||
// TODO: Remove this once we've migrated hotkey scopes to the new api
|
||||
scope: string,
|
||||
dependencies?: unknown[],
|
||||
options?: UseHotkeysOptionsWithoutBuggyOptions,
|
||||
) => {
|
||||
const callGlobalHotkeysCallback = useGlobalHotkeysCallback(dependencies);
|
||||
|
||||
const enableOnContentEditable = isDefined(options?.enableOnContentEditable)
|
||||
? options.enableOnContentEditable
|
||||
: true;
|
||||
|
||||
const enableOnFormTags = isDefined(options?.enableOnFormTags)
|
||||
? options.enableOnFormTags
|
||||
: true;
|
||||
|
||||
const preventDefault = isDefined(options?.preventDefault)
|
||||
? options.preventDefault === true
|
||||
: true;
|
||||
|
||||
const ignoreModifiers = isDefined(options?.ignoreModifiers)
|
||||
? options.ignoreModifiers === true
|
||||
: false;
|
||||
|
||||
const handleCallback = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
async (keyboardEvent: KeyboardEvent, hotkeysEvent: any) => {
|
||||
const pendingHotkey = snapshot
|
||||
.getLoadable(pendingHotkeyState)
|
||||
.getValue();
|
||||
|
||||
if (!pendingHotkey) {
|
||||
callback(keyboardEvent, hotkeysEvent);
|
||||
}
|
||||
|
||||
set(pendingHotkeyState, null);
|
||||
},
|
||||
[callback],
|
||||
);
|
||||
|
||||
return useHotkeys(
|
||||
keys,
|
||||
(keyboardEvent, hotkeysEvent) => {
|
||||
callGlobalHotkeysCallback({
|
||||
keyboardEvent,
|
||||
hotkeysEvent,
|
||||
callback: () => {
|
||||
handleCallback(keyboardEvent, hotkeysEvent);
|
||||
},
|
||||
scope,
|
||||
preventDefault,
|
||||
containsModifier,
|
||||
});
|
||||
},
|
||||
{
|
||||
enableOnContentEditable,
|
||||
enableOnFormTags,
|
||||
ignoreModifiers,
|
||||
},
|
||||
dependencies,
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,119 @@
|
||||
import { currentGlobalHotkeysConfigSelector } from '@/ui/utilities/focus/states/currentGlobalHotkeysConfigSelector';
|
||||
import { internalHotkeysEnabledScopesState } from '@/ui/utilities/hotkey/states/internal/internalHotkeysEnabledScopesState';
|
||||
import {
|
||||
Hotkey,
|
||||
OptionsOrDependencyArray,
|
||||
} from 'react-hotkeys-hook/dist/types';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { logDebug } from '~/utils/logDebug';
|
||||
import { DEBUG_HOTKEY_SCOPE } from '../constants/DebugHotkeyScope';
|
||||
|
||||
export const useGlobalHotkeysCallback = (
|
||||
dependencies?: OptionsOrDependencyArray,
|
||||
) => {
|
||||
const dependencyArray = Array.isArray(dependencies) ? dependencies : [];
|
||||
|
||||
return useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
({
|
||||
callback,
|
||||
containsModifier,
|
||||
hotkeysEvent,
|
||||
keyboardEvent,
|
||||
preventDefault,
|
||||
scope,
|
||||
}: {
|
||||
keyboardEvent: KeyboardEvent;
|
||||
hotkeysEvent: Hotkey;
|
||||
containsModifier: boolean;
|
||||
callback: (keyboardEvent: KeyboardEvent, hotkeysEvent: Hotkey) => void;
|
||||
preventDefault?: boolean;
|
||||
scope: string;
|
||||
}) => {
|
||||
// TODO: Remove this once we've migrated hotkey scopes to the new api
|
||||
const currentHotkeyScopes = snapshot
|
||||
.getLoadable(internalHotkeysEnabledScopesState)
|
||||
.getValue();
|
||||
|
||||
const currentGlobalHotkeysConfig = snapshot
|
||||
.getLoadable(currentGlobalHotkeysConfigSelector)
|
||||
.getValue();
|
||||
|
||||
if (
|
||||
containsModifier &&
|
||||
!currentGlobalHotkeysConfig.enableGlobalHotkeysWithModifiers
|
||||
) {
|
||||
if (DEBUG_HOTKEY_SCOPE) {
|
||||
logDebug(
|
||||
`DEBUG: %cI can't call hotkey (${
|
||||
hotkeysEvent.keys
|
||||
}) because global hotkeys with modifiers are disabled`,
|
||||
'color: gray; ',
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!containsModifier &&
|
||||
!currentGlobalHotkeysConfig.enableGlobalHotkeysConflictingWithKeyboard
|
||||
) {
|
||||
if (DEBUG_HOTKEY_SCOPE) {
|
||||
logDebug(
|
||||
`DEBUG: %cI can't call hotkey (${
|
||||
hotkeysEvent.keys
|
||||
}) because global hotkeys conflicting with keyboard are disabled`,
|
||||
'color: gray; ',
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Remove this once we've migrated hotkey scopes to the new api
|
||||
if (!currentHotkeyScopes.includes(scope)) {
|
||||
if (DEBUG_HOTKEY_SCOPE) {
|
||||
logDebug(
|
||||
`DEBUG: %cI can't call hotkey (${
|
||||
hotkeysEvent.keys
|
||||
}) because I'm in scope [${scope}] and the active scopes are : [${currentHotkeyScopes.join(
|
||||
', ',
|
||||
)}]`,
|
||||
'color: gray; ',
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Remove this once we've migrated hotkey scopes to the new api
|
||||
if (DEBUG_HOTKEY_SCOPE) {
|
||||
logDebug(
|
||||
`DEBUG: %cI can call hotkey (${
|
||||
hotkeysEvent.keys
|
||||
}) because I'm in scope [${scope}] and the active scopes are : [${currentHotkeyScopes.join(
|
||||
', ',
|
||||
)}]`,
|
||||
'color: green;',
|
||||
);
|
||||
}
|
||||
|
||||
if (preventDefault === true) {
|
||||
if (DEBUG_HOTKEY_SCOPE) {
|
||||
logDebug(
|
||||
`DEBUG: %cI prevent default for hotkey (${hotkeysEvent.keys})`,
|
||||
'color: gray;',
|
||||
);
|
||||
}
|
||||
|
||||
keyboardEvent.stopPropagation();
|
||||
keyboardEvent.preventDefault();
|
||||
keyboardEvent.stopImmediatePropagation();
|
||||
}
|
||||
|
||||
return callback(keyboardEvent, hotkeysEvent);
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
dependencyArray,
|
||||
);
|
||||
};
|
||||
@ -4,10 +4,10 @@ import { useRecoilState } from 'recoil';
|
||||
|
||||
import { pendingHotkeyState } from '../states/internal/pendingHotkeysState';
|
||||
|
||||
import { useScopedHotkeyCallback } from './useScopedHotkeyCallback';
|
||||
import { useGlobalHotkeysCallback } from '@/ui/utilities/hotkey/hooks/useGlobalHotkeysCallback';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useSequenceHotkeys = (
|
||||
export const useGlobalHotkeysSequence = (
|
||||
firstKey: Keys,
|
||||
secondKey: Keys,
|
||||
sequenceCallback: () => void,
|
||||
@ -21,14 +21,15 @@ export const useSequenceHotkeys = (
|
||||
) => {
|
||||
const [pendingHotkey, setPendingHotkey] = useRecoilState(pendingHotkeyState);
|
||||
|
||||
const callScopedHotkeyCallback = useScopedHotkeyCallback();
|
||||
const callGlobalHotkeysCallback = useGlobalHotkeysCallback();
|
||||
|
||||
useHotkeys(
|
||||
firstKey,
|
||||
(keyboardEvent, hotkeysEvent) => {
|
||||
callScopedHotkeyCallback({
|
||||
callGlobalHotkeysCallback({
|
||||
keyboardEvent,
|
||||
hotkeysEvent,
|
||||
containsModifier: false,
|
||||
callback: () => {
|
||||
setPendingHotkey(firstKey);
|
||||
},
|
||||
@ -46,9 +47,10 @@ export const useSequenceHotkeys = (
|
||||
useHotkeys(
|
||||
secondKey,
|
||||
(keyboardEvent, hotkeysEvent) => {
|
||||
callScopedHotkeyCallback({
|
||||
callGlobalHotkeysCallback({
|
||||
keyboardEvent,
|
||||
hotkeysEvent,
|
||||
containsModifier: false,
|
||||
callback: () => {
|
||||
if (pendingHotkey !== firstKey) {
|
||||
return;
|
||||
@ -1,10 +1,9 @@
|
||||
import { Keys } from 'react-hotkeys-hook/dist/types';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useGlobalHotkeysSequence } from '@/ui/utilities/hotkey/hooks/useGlobalHotkeysSequence';
|
||||
import { AppHotkeyScope } from '../types/AppHotkeyScope';
|
||||
|
||||
import { useSequenceHotkeys } from './useSequenceScopedHotkeys';
|
||||
|
||||
type GoToHotkeysProps = {
|
||||
key: Keys;
|
||||
location: string;
|
||||
@ -18,7 +17,7 @@ export const useGoToHotkeys = ({
|
||||
}: GoToHotkeysProps) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
useSequenceHotkeys(
|
||||
useGlobalHotkeysSequence(
|
||||
'g',
|
||||
key,
|
||||
() => {
|
||||
|
||||
@ -0,0 +1,73 @@
|
||||
import { useHotkeysOnFocusedElementCallback } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElementCallback';
|
||||
import { pendingHotkeyState } from '@/ui/utilities/hotkey/states/internal/pendingHotkeysState';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { HotkeyCallback, Keys, Options } from 'react-hotkeys-hook/dist/types';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type UseHotkeysOptionsWithoutBuggyOptions = Omit<Options, 'enabled'>;
|
||||
|
||||
export const useHotkeysOnFocusedElement = ({
|
||||
keys,
|
||||
callback,
|
||||
focusId,
|
||||
// TODO: Remove this once we've migrated hotkey scopes to the new api
|
||||
scope,
|
||||
dependencies,
|
||||
options,
|
||||
}: {
|
||||
keys: Keys;
|
||||
callback: HotkeyCallback;
|
||||
focusId: string;
|
||||
// TODO: Remove this once we've migrated hotkey scopes to the new api
|
||||
scope: string;
|
||||
dependencies?: unknown[];
|
||||
options?: UseHotkeysOptionsWithoutBuggyOptions;
|
||||
}) => {
|
||||
const [pendingHotkey, setPendingHotkey] = useRecoilState(pendingHotkeyState);
|
||||
|
||||
const callScopedHotkeyCallback =
|
||||
useHotkeysOnFocusedElementCallback(dependencies);
|
||||
|
||||
const enableOnContentEditable = isDefined(options?.enableOnContentEditable)
|
||||
? options.enableOnContentEditable
|
||||
: true;
|
||||
|
||||
const enableOnFormTags = isDefined(options?.enableOnFormTags)
|
||||
? options.enableOnFormTags
|
||||
: true;
|
||||
|
||||
const preventDefault = isDefined(options?.preventDefault)
|
||||
? options.preventDefault === true
|
||||
: true;
|
||||
|
||||
const ignoreModifiers = isDefined(options?.ignoreModifiers)
|
||||
? options.ignoreModifiers === true
|
||||
: false;
|
||||
|
||||
return useHotkeys(
|
||||
keys,
|
||||
(keyboardEvent, hotkeysEvent) => {
|
||||
callScopedHotkeyCallback({
|
||||
keyboardEvent,
|
||||
hotkeysEvent,
|
||||
callback: () => {
|
||||
if (!pendingHotkey) {
|
||||
callback(keyboardEvent, hotkeysEvent);
|
||||
return;
|
||||
}
|
||||
setPendingHotkey(null);
|
||||
},
|
||||
focusId,
|
||||
scope,
|
||||
preventDefault,
|
||||
});
|
||||
},
|
||||
{
|
||||
enableOnContentEditable,
|
||||
enableOnFormTags,
|
||||
ignoreModifiers,
|
||||
},
|
||||
dependencies,
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,89 @@
|
||||
import { internalHotkeysEnabledScopesState } from '@/ui/utilities/hotkey/states/internal/internalHotkeysEnabledScopesState';
|
||||
import {
|
||||
Hotkey,
|
||||
OptionsOrDependencyArray,
|
||||
} from 'react-hotkeys-hook/dist/types';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { logDebug } from '~/utils/logDebug';
|
||||
import { currentFocusIdSelector } from '../../focus/states/currentFocusIdSelector';
|
||||
import { DEBUG_HOTKEY_SCOPE } from '../constants/DebugHotkeyScope';
|
||||
|
||||
export const useHotkeysOnFocusedElementCallback = (
|
||||
dependencies?: OptionsOrDependencyArray,
|
||||
) => {
|
||||
const dependencyArray = Array.isArray(dependencies) ? dependencies : [];
|
||||
|
||||
return useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
({
|
||||
callback,
|
||||
hotkeysEvent,
|
||||
keyboardEvent,
|
||||
focusId,
|
||||
scope,
|
||||
preventDefault,
|
||||
}: {
|
||||
keyboardEvent: KeyboardEvent;
|
||||
hotkeysEvent: Hotkey;
|
||||
callback: (keyboardEvent: KeyboardEvent, hotkeysEvent: Hotkey) => void;
|
||||
focusId: string;
|
||||
scope: string;
|
||||
preventDefault?: boolean;
|
||||
}) => {
|
||||
const currentFocusId = snapshot
|
||||
.getLoadable(currentFocusIdSelector)
|
||||
.getValue();
|
||||
|
||||
// TODO: Remove this once we've migrated hotkey scopes to the new api
|
||||
const currentHotkeyScopes = snapshot
|
||||
.getLoadable(internalHotkeysEnabledScopesState)
|
||||
.getValue();
|
||||
|
||||
if (
|
||||
currentFocusId !== focusId ||
|
||||
!currentHotkeyScopes.includes(scope)
|
||||
) {
|
||||
if (DEBUG_HOTKEY_SCOPE) {
|
||||
logDebug(
|
||||
`DEBUG: %cI can't call hotkey (${
|
||||
hotkeysEvent.keys
|
||||
}) because I'm in scope [${scope}] and the active scopes are : [${currentHotkeyScopes.join(
|
||||
', ',
|
||||
)}] and the current focus identifier is [${focusId}]`,
|
||||
'color: gray; ',
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (DEBUG_HOTKEY_SCOPE) {
|
||||
logDebug(
|
||||
`DEBUG: %cI can call hotkey (${
|
||||
hotkeysEvent.keys
|
||||
}) because I'm in scope [${scope}] and the active scopes are : [${currentHotkeyScopes.join(
|
||||
', ',
|
||||
)}], and the current focus identifier is [${focusId}]`,
|
||||
'color: green;',
|
||||
);
|
||||
}
|
||||
|
||||
if (preventDefault === true) {
|
||||
if (DEBUG_HOTKEY_SCOPE) {
|
||||
logDebug(
|
||||
`DEBUG: %cI prevent default for hotkey (${hotkeysEvent.keys})`,
|
||||
'color: gray;',
|
||||
);
|
||||
}
|
||||
|
||||
keyboardEvent.stopPropagation();
|
||||
keyboardEvent.preventDefault();
|
||||
keyboardEvent.stopImmediatePropagation();
|
||||
}
|
||||
|
||||
return callback(keyboardEvent, hotkeysEvent);
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
dependencyArray,
|
||||
);
|
||||
};
|
||||
@ -1,7 +1,7 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { DEBUG_HOTKEY_SCOPE } from '@/ui/utilities/hotkey/hooks/useScopedHotkeyCallback';
|
||||
import { logDebug } from '~/utils/logDebug';
|
||||
import { DEBUG_HOTKEY_SCOPE } from '../constants/DebugHotkeyScope';
|
||||
|
||||
import { currentHotkeyScopeState } from '../states/internal/currentHotkeyScopeState';
|
||||
import { previousHotkeyScopeFamilyState } from '../states/internal/previousHotkeyScopeFamilyState';
|
||||
@ -9,14 +9,14 @@ import { CustomHotkeyScopes } from '../types/CustomHotkeyScope';
|
||||
|
||||
import { useSetHotkeyScope } from './useSetHotkeyScope';
|
||||
|
||||
export const usePreviousHotkeyScope = (memoizeKey = 'global') => {
|
||||
export const usePreviousHotkeyScope = () => {
|
||||
const setHotkeyScope = useSetHotkeyScope();
|
||||
|
||||
const goBackToPreviousHotkeyScope = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
() => {
|
||||
(memoizeKey = 'global') => {
|
||||
const previousHotkeyScope = snapshot
|
||||
.getLoadable(previousHotkeyScopeFamilyState(memoizeKey))
|
||||
.getLoadable(previousHotkeyScopeFamilyState(memoizeKey as string))
|
||||
.getValue();
|
||||
|
||||
if (!previousHotkeyScope) {
|
||||
@ -39,14 +39,22 @@ export const usePreviousHotkeyScope = (memoizeKey = 'global') => {
|
||||
previousHotkeyScope.customScopes,
|
||||
);
|
||||
|
||||
set(previousHotkeyScopeFamilyState(memoizeKey), null);
|
||||
set(previousHotkeyScopeFamilyState(memoizeKey as string), null);
|
||||
},
|
||||
[setHotkeyScope, memoizeKey],
|
||||
[setHotkeyScope],
|
||||
);
|
||||
|
||||
const setHotkeyScopeAndMemorizePreviousScope = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
(scope: string, customScopes?: CustomHotkeyScopes) => {
|
||||
({
|
||||
scope,
|
||||
customScopes,
|
||||
memoizeKey = 'global',
|
||||
}: {
|
||||
scope: string;
|
||||
customScopes?: CustomHotkeyScopes;
|
||||
memoizeKey?: string;
|
||||
}) => {
|
||||
const currentHotkeyScope = snapshot
|
||||
.getLoadable(currentHotkeyScopeState)
|
||||
.getValue();
|
||||
@ -63,7 +71,7 @@ export const usePreviousHotkeyScope = (memoizeKey = 'global') => {
|
||||
|
||||
set(previousHotkeyScopeFamilyState(memoizeKey), currentHotkeyScope);
|
||||
},
|
||||
[setHotkeyScope, memoizeKey],
|
||||
[setHotkeyScope],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@ -6,10 +6,9 @@ import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { logDebug } from '~/utils/logDebug';
|
||||
|
||||
import { DEBUG_HOTKEY_SCOPE } from '../constants/DebugHotkeyScope';
|
||||
import { internalHotkeysEnabledScopesState } from '../states/internal/internalHotkeysEnabledScopesState';
|
||||
|
||||
export const DEBUG_HOTKEY_SCOPE = false;
|
||||
|
||||
export const useScopedHotkeyCallback = (
|
||||
dependencies?: OptionsOrDependencyArray,
|
||||
) => {
|
||||
|
||||
@ -2,9 +2,9 @@ import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { HotkeyCallback, Keys, Options } from 'react-hotkeys-hook/dist/types';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { pendingHotkeyState } from '../states/internal/pendingHotkeysState';
|
||||
import { useScopedHotkeyCallback } from './useScopedHotkeyCallback';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type UseHotkeysOptionsWithoutBuggyOptions = Omit<Options, 'enabled'>;
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { DEBUG_HOTKEY_SCOPE } from '@/ui/utilities/hotkey/hooks/useScopedHotkeyCallback';
|
||||
import { DEBUG_HOTKEY_SCOPE } from '../constants/DebugHotkeyScope';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { logDebug } from '~/utils/logDebug';
|
||||
@ -11,7 +11,7 @@ import { AppHotkeyScope } from '../types/AppHotkeyScope';
|
||||
import { CustomHotkeyScopes } from '../types/CustomHotkeyScope';
|
||||
import { HotkeyScope } from '../types/HotkeyScope';
|
||||
|
||||
const isCustomScopesEqual = (
|
||||
const areCustomScopesEqual = (
|
||||
customScopesA: CustomHotkeyScopes | undefined,
|
||||
customScopesB: CustomHotkeyScopes | undefined,
|
||||
) => {
|
||||
@ -34,7 +34,7 @@ export const useSetHotkeyScope = () =>
|
||||
if (currentHotkeyScope.scope === hotkeyScopeToSet) {
|
||||
if (!isDefined(customScopes)) {
|
||||
if (
|
||||
isCustomScopesEqual(
|
||||
areCustomScopesEqual(
|
||||
currentHotkeyScope?.customScopes,
|
||||
DEFAULT_HOTKEYS_SCOPE_CUSTOM_SCOPES,
|
||||
)
|
||||
@ -43,7 +43,7 @@ export const useSetHotkeyScope = () =>
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
isCustomScopesEqual(
|
||||
areCustomScopesEqual(
|
||||
currentHotkeyScope?.customScopes,
|
||||
customScopes,
|
||||
)
|
||||
|
||||
@ -0,0 +1,4 @@
|
||||
export type GlobalHotkeysConfig = {
|
||||
enableGlobalHotkeysWithModifiers: boolean;
|
||||
enableGlobalHotkeysConflictingWithKeyboard: boolean;
|
||||
};
|
||||
Reference in New Issue
Block a user