Feat/better hotkeys scope (#526)
* Working version * fix * Fixed console log * Fix lint * wip * Fix * Fix * consolelog --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
103
front/src/modules/hotkeys/hooks/internal/useHotkeysScope.ts
Normal file
103
front/src/modules/hotkeys/hooks/internal/useHotkeysScope.ts
Normal file
@ -0,0 +1,103 @@
|
||||
import { useHotkeysContext } from 'react-hotkeys-hook';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { internalHotkeysEnabledScopesState } from '@/hotkeys/states/internal/internalHotkeysEnabledScopesState';
|
||||
|
||||
export function useHotkeysScope() {
|
||||
const { disableScope, enableScope } = useHotkeysContext();
|
||||
|
||||
const disableAllHotkeysScopes = useRecoilCallback(
|
||||
({ set, snapshot }) => {
|
||||
return async () => {
|
||||
const enabledScopes = await snapshot.getPromise(
|
||||
internalHotkeysEnabledScopesState,
|
||||
);
|
||||
|
||||
for (const enabledScope of enabledScopes) {
|
||||
disableScope(enabledScope);
|
||||
}
|
||||
|
||||
set(internalHotkeysEnabledScopesState, []);
|
||||
};
|
||||
},
|
||||
[disableScope],
|
||||
);
|
||||
|
||||
const enableHotkeysScope = useRecoilCallback(
|
||||
({ set, snapshot }) => {
|
||||
return async (scopeToEnable: string) => {
|
||||
const enabledScopes = await snapshot.getPromise(
|
||||
internalHotkeysEnabledScopesState,
|
||||
);
|
||||
|
||||
if (!enabledScopes.includes(scopeToEnable)) {
|
||||
enableScope(scopeToEnable);
|
||||
set(internalHotkeysEnabledScopesState, [
|
||||
...enabledScopes,
|
||||
scopeToEnable,
|
||||
]);
|
||||
}
|
||||
};
|
||||
},
|
||||
[enableScope],
|
||||
);
|
||||
|
||||
const disableHotkeysScope = useRecoilCallback(
|
||||
({ set, snapshot }) => {
|
||||
return async (scopeToDisable: string) => {
|
||||
const enabledScopes = await snapshot.getPromise(
|
||||
internalHotkeysEnabledScopesState,
|
||||
);
|
||||
|
||||
const scopeToRemoveIndex = enabledScopes.findIndex(
|
||||
(scope) => scope === scopeToDisable,
|
||||
);
|
||||
|
||||
if (scopeToRemoveIndex > -1) {
|
||||
disableScope(scopeToDisable);
|
||||
|
||||
enabledScopes.splice(scopeToRemoveIndex);
|
||||
|
||||
set(internalHotkeysEnabledScopesState, enabledScopes);
|
||||
}
|
||||
};
|
||||
},
|
||||
[disableScope],
|
||||
);
|
||||
|
||||
const setHotkeysScopes = useRecoilCallback(
|
||||
({ set, snapshot }) => {
|
||||
return async (scopesToSet: string[]) => {
|
||||
const enabledScopes = await snapshot.getPromise(
|
||||
internalHotkeysEnabledScopesState,
|
||||
);
|
||||
|
||||
const scopesToDisable = enabledScopes.filter(
|
||||
(enabledScope) => !scopesToSet.includes(enabledScope),
|
||||
);
|
||||
|
||||
const scopesToEnable = scopesToSet.filter(
|
||||
(scopeToSet) => !enabledScopes.includes(scopeToSet),
|
||||
);
|
||||
|
||||
for (const scopeToDisable of scopesToDisable) {
|
||||
disableScope(scopeToDisable);
|
||||
}
|
||||
|
||||
for (const scopeToEnable of scopesToEnable) {
|
||||
enableScope(scopeToEnable);
|
||||
}
|
||||
|
||||
set(internalHotkeysEnabledScopesState, scopesToSet);
|
||||
};
|
||||
},
|
||||
[disableScope, enableScope],
|
||||
);
|
||||
|
||||
return {
|
||||
disableAllHotkeysScopes,
|
||||
enableHotkeysScope,
|
||||
disableHotkeysScope,
|
||||
setHotkeysScopes,
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { customHotkeysScopesState } from '@/hotkeys/states/internal/customHotkeysScopesState';
|
||||
import { hotkeysScopeStackState } from '@/hotkeys/states/internal/hotkeysScopeStackState';
|
||||
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
|
||||
|
||||
import { useHotkeysScope } from './useHotkeysScope';
|
||||
|
||||
export function useHotkeysScopeStackAutoSync() {
|
||||
const { setHotkeysScopes } = useHotkeysScope();
|
||||
|
||||
const hotkeysScopeStack = useRecoilValue(hotkeysScopeStackState);
|
||||
const customHotkeysScopes = useRecoilValue(customHotkeysScopesState);
|
||||
|
||||
useEffect(() => {
|
||||
if (hotkeysScopeStack.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scopesToSet: string[] = [];
|
||||
|
||||
const currentHotkeysScope = hotkeysScopeStack[hotkeysScopeStack.length - 1];
|
||||
|
||||
if (currentHotkeysScope.customScopes?.['command-menu']) {
|
||||
scopesToSet.push(InternalHotkeysScope.CommandMenu);
|
||||
}
|
||||
|
||||
if (currentHotkeysScope?.customScopes?.goto) {
|
||||
scopesToSet.push(InternalHotkeysScope.Goto);
|
||||
}
|
||||
|
||||
scopesToSet.push(currentHotkeysScope.scope);
|
||||
|
||||
setHotkeysScopes(scopesToSet);
|
||||
}, [setHotkeysScopes, customHotkeysScopes, hotkeysScopeStack]);
|
||||
}
|
||||
48
front/src/modules/hotkeys/hooks/useAddToHotkeysScopeStack.ts
Normal file
48
front/src/modules/hotkeys/hooks/useAddToHotkeysScopeStack.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { produce } from 'immer';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { hotkeysScopeStackState } from '../states/internal/hotkeysScopeStackState';
|
||||
import { HotkeysScopeStackItem } from '../types/internal/HotkeysScopeStackItems';
|
||||
|
||||
export function useAddToHotkeysScopeStack() {
|
||||
return useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
async ({
|
||||
scope,
|
||||
customScopes = {
|
||||
'command-menu': true,
|
||||
goto: false,
|
||||
},
|
||||
ancestorScope,
|
||||
}: HotkeysScopeStackItem) => {
|
||||
const hotkeysScopeStack = await snapshot.getPromise(
|
||||
hotkeysScopeStackState,
|
||||
);
|
||||
|
||||
const currentHotkeysScope =
|
||||
hotkeysScopeStack.length > 0
|
||||
? hotkeysScopeStack[hotkeysScopeStack.length - 1]
|
||||
: null;
|
||||
|
||||
const previousHotkeysScope =
|
||||
hotkeysScopeStack.length > 1
|
||||
? hotkeysScopeStack[hotkeysScopeStack.length - 2]
|
||||
: null;
|
||||
|
||||
if (
|
||||
scope === currentHotkeysScope?.scope ||
|
||||
scope === previousHotkeysScope?.scope
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
set(
|
||||
hotkeysScopeStackState,
|
||||
produce(hotkeysScopeStack, (draft) => {
|
||||
draft.push({ scope, customScopes, ancestorScope });
|
||||
}),
|
||||
);
|
||||
},
|
||||
[],
|
||||
);
|
||||
}
|
||||
16
front/src/modules/hotkeys/hooks/useCurrentHotkeysScope.ts
Normal file
16
front/src/modules/hotkeys/hooks/useCurrentHotkeysScope.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { hotkeysScopeStackState } from '../states/internal/hotkeysScopeStackState';
|
||||
|
||||
export function useCurrentHotkeysScope() {
|
||||
const hotkeysScopeStack = useRecoilValue(hotkeysScopeStackState);
|
||||
|
||||
return useMemo(() => {
|
||||
if (hotkeysScopeStack.length === 0) {
|
||||
return null;
|
||||
} else {
|
||||
return hotkeysScopeStack[hotkeysScopeStack.length - 1];
|
||||
}
|
||||
}, [hotkeysScopeStack]);
|
||||
}
|
||||
@ -1,11 +1,19 @@
|
||||
import { Keys } from 'react-hotkeys-hook/dist/types';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useSequenceHotkeys } from './useSequenceHotkeys';
|
||||
import { InternalHotkeysScope } from '../types/internal/InternalHotkeysScope';
|
||||
|
||||
export function useGoToHotkeys(key: string, location: string) {
|
||||
import { useSequenceHotkeys } from './useSequenceScopedHotkeys';
|
||||
|
||||
export function useGoToHotkeys(key: Keys, location: string) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
useSequenceHotkeys('g', key, () => {
|
||||
navigate(location);
|
||||
});
|
||||
useSequenceHotkeys(
|
||||
'g',
|
||||
key,
|
||||
() => {
|
||||
navigate(location);
|
||||
},
|
||||
InternalHotkeysScope.Goto,
|
||||
);
|
||||
}
|
||||
|
||||
@ -0,0 +1,27 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { HotkeysScopeStackItem } from '../types/internal/HotkeysScopeStackItems';
|
||||
|
||||
import { useAddToHotkeysScopeStack } from './useAddToHotkeysScopeStack';
|
||||
import { useRemoveFromHotkeysScopeStack } from './useRemoveFromHotkeysScopeStack';
|
||||
|
||||
export function useHotkeysScopeOnBooleanState(
|
||||
hotkeysScopeStackItem: HotkeysScopeStackItem,
|
||||
booleanState: boolean,
|
||||
) {
|
||||
const addToHotkeysScopeStack = useAddToHotkeysScopeStack();
|
||||
const removeFromHoteysScopeStack = useRemoveFromHotkeysScopeStack();
|
||||
|
||||
useEffect(() => {
|
||||
if (booleanState) {
|
||||
addToHotkeysScopeStack(hotkeysScopeStackItem);
|
||||
} else {
|
||||
removeFromHoteysScopeStack(hotkeysScopeStackItem.scope);
|
||||
}
|
||||
}, [
|
||||
hotkeysScopeStackItem,
|
||||
removeFromHoteysScopeStack,
|
||||
addToHotkeysScopeStack,
|
||||
booleanState,
|
||||
]);
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { hotkeysScopeStackState } from '../states/internal/hotkeysScopeStackState';
|
||||
import { HotkeysScopeStackItem } from '../types/internal/HotkeysScopeStackItems';
|
||||
|
||||
import { useAddToHotkeysScopeStack } from './useAddToHotkeysScopeStack';
|
||||
|
||||
export function useHotkeysScopeOnMountOnly(
|
||||
hotkeysScopeStackItem: HotkeysScopeStackItem,
|
||||
enabled = true,
|
||||
) {
|
||||
const addToHotkeysScopeStack = useAddToHotkeysScopeStack();
|
||||
|
||||
const [hotkeysScopeStack] = useRecoilState(hotkeysScopeStackState);
|
||||
|
||||
const hotkeysScopeAlreadyInStack = hotkeysScopeStack.some(
|
||||
(hotkeysScopeStackItemToFind) =>
|
||||
hotkeysScopeStackItemToFind.scope === hotkeysScopeStackItem.scope,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hotkeysScopeAlreadyInStack && enabled) {
|
||||
addToHotkeysScopeStack(hotkeysScopeStackItem);
|
||||
}
|
||||
}, [
|
||||
enabled,
|
||||
addToHotkeysScopeStack,
|
||||
hotkeysScopeStackItem,
|
||||
hotkeysScopeAlreadyInStack,
|
||||
]);
|
||||
}
|
||||
@ -0,0 +1,54 @@
|
||||
import { produce } from 'immer';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { DEFAULT_HOTKEYS_SCOPE_STACK_ITEM } from '../constants';
|
||||
import { hotkeysScopeStackState } from '../states/internal/hotkeysScopeStackState';
|
||||
import { InternalHotkeysScope } from '../types/internal/InternalHotkeysScope';
|
||||
|
||||
export function useRemoveFromHotkeysScopeStack() {
|
||||
return useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
async (hotkeysScopeToRemove: string) => {
|
||||
const hotkeysScopeStack = await snapshot.getPromise(
|
||||
hotkeysScopeStackState,
|
||||
);
|
||||
|
||||
if (hotkeysScopeStack.length < 1) {
|
||||
set(hotkeysScopeStackState, [DEFAULT_HOTKEYS_SCOPE_STACK_ITEM]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const currentHotkeysScope =
|
||||
hotkeysScopeStack[hotkeysScopeStack.length - 1];
|
||||
|
||||
if (hotkeysScopeStack.length === 1) {
|
||||
if (currentHotkeysScope?.scope !== InternalHotkeysScope.App) {
|
||||
set(hotkeysScopeStackState, [DEFAULT_HOTKEYS_SCOPE_STACK_ITEM]);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const previousHotkeysScope =
|
||||
hotkeysScopeStack[hotkeysScopeStack.length - 2];
|
||||
|
||||
if (
|
||||
previousHotkeysScope.scope === hotkeysScopeToRemove ||
|
||||
currentHotkeysScope.scope !== hotkeysScopeToRemove
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
set(
|
||||
hotkeysScopeStackState,
|
||||
produce(hotkeysScopeStack, (draft) => {
|
||||
return draft.filter(
|
||||
(hotkeysScope) => hotkeysScope.scope !== hotkeysScopeToRemove,
|
||||
);
|
||||
}),
|
||||
);
|
||||
},
|
||||
[],
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
import { produce } from 'immer';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { DEFAULT_HOTKEYS_SCOPE_STACK_ITEM } from '../constants';
|
||||
import { hotkeysScopeStackState } from '../states/internal/hotkeysScopeStackState';
|
||||
import { InternalHotkeysScope } from '../types/internal/InternalHotkeysScope';
|
||||
|
||||
export function useRemoveHighestHotkeysScopeStackItem() {
|
||||
return useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
async () => {
|
||||
const hotkeysScopeStack = await snapshot.getPromise(
|
||||
hotkeysScopeStackState,
|
||||
);
|
||||
|
||||
if (hotkeysScopeStack.length < 1) {
|
||||
set(hotkeysScopeStackState, [DEFAULT_HOTKEYS_SCOPE_STACK_ITEM]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const currentHotkeysScope =
|
||||
hotkeysScopeStack[hotkeysScopeStack.length - 1];
|
||||
|
||||
if (hotkeysScopeStack.length === 1) {
|
||||
if (currentHotkeysScope?.scope !== InternalHotkeysScope.App) {
|
||||
set(hotkeysScopeStackState, [DEFAULT_HOTKEYS_SCOPE_STACK_ITEM]);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
set(
|
||||
hotkeysScopeStackState,
|
||||
produce(hotkeysScopeStack, (draft) => {
|
||||
draft.pop();
|
||||
}),
|
||||
);
|
||||
},
|
||||
[],
|
||||
);
|
||||
}
|
||||
18
front/src/modules/hotkeys/hooks/useResetHotkeysScopeStack.ts
Normal file
18
front/src/modules/hotkeys/hooks/useResetHotkeysScopeStack.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { useResetRecoilState } from 'recoil';
|
||||
|
||||
import { hotkeysScopeStackState } from '../states/internal/hotkeysScopeStackState';
|
||||
|
||||
import { useAddToHotkeysScopeStack } from './useAddToHotkeysScopeStack';
|
||||
|
||||
export function useResetHotkeysScopeStack() {
|
||||
const resetHotkeysScopeStack = useResetRecoilState(hotkeysScopeStackState);
|
||||
const addHotkeysScopedStack = useAddToHotkeysScopeStack();
|
||||
|
||||
return function reset(toFirstScope?: string) {
|
||||
resetHotkeysScopeStack();
|
||||
|
||||
if (toFirstScope) {
|
||||
addHotkeysScopedStack({ scope: toFirstScope });
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -2,16 +2,24 @@ import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import {
|
||||
Hotkey,
|
||||
HotkeyCallback,
|
||||
Keys,
|
||||
Options,
|
||||
OptionsOrDependencyArray,
|
||||
} from 'react-hotkeys-hook/dist/types';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { pendingHotkeyState } from '../states/pendingHotkeysState';
|
||||
import { pendingHotkeyState } from '../states/internal/pendingHotkeysState';
|
||||
|
||||
export function useDirectHotkeys(
|
||||
keys: string,
|
||||
export function useScopedHotkeys(
|
||||
keys: Keys,
|
||||
callback: HotkeyCallback,
|
||||
scope: string,
|
||||
dependencies?: OptionsOrDependencyArray,
|
||||
options: Options = {
|
||||
enableOnContentEditable: true,
|
||||
enableOnFormTags: true,
|
||||
preventDefault: true,
|
||||
},
|
||||
) {
|
||||
const [pendingHotkey, setPendingHotkey] = useRecoilState(pendingHotkeyState);
|
||||
|
||||
@ -26,5 +34,10 @@ export function useDirectHotkeys(
|
||||
setPendingHotkey(null);
|
||||
}
|
||||
|
||||
useHotkeys(keys, callbackIfDirectKey, dependencies);
|
||||
return useHotkeys(
|
||||
keys,
|
||||
callbackIfDirectKey,
|
||||
{ ...options, scopes: [scope] },
|
||||
dependencies,
|
||||
);
|
||||
}
|
||||
@ -1,12 +1,19 @@
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { Options, useHotkeys } from 'react-hotkeys-hook';
|
||||
import { Keys } from 'react-hotkeys-hook/dist/types';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { pendingHotkeyState } from '../states/pendingHotkeysState';
|
||||
import { pendingHotkeyState } from '../states/internal/pendingHotkeysState';
|
||||
|
||||
export function useSequenceHotkeys(
|
||||
firstKey: string,
|
||||
secondKey: string,
|
||||
firstKey: Keys,
|
||||
secondKey: Keys,
|
||||
callback: () => void,
|
||||
scope: string,
|
||||
options: Options = {
|
||||
enableOnContentEditable: true,
|
||||
enableOnFormTags: true,
|
||||
preventDefault: true,
|
||||
},
|
||||
) {
|
||||
const [pendingHotkey, setPendingHotkey] = useRecoilState(pendingHotkeyState);
|
||||
|
||||
@ -15,6 +22,7 @@ export function useSequenceHotkeys(
|
||||
() => {
|
||||
setPendingHotkey(firstKey);
|
||||
},
|
||||
{ ...options, scopes: [scope] },
|
||||
[pendingHotkey],
|
||||
);
|
||||
|
||||
@ -27,6 +35,7 @@ export function useSequenceHotkeys(
|
||||
setPendingHotkey(null);
|
||||
callback();
|
||||
},
|
||||
{ ...options, scopes: [scope] },
|
||||
[pendingHotkey, setPendingHotkey],
|
||||
);
|
||||
}
|
||||
@ -1,31 +0,0 @@
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { HotkeyCallback } from 'react-hotkeys-hook/dist/types';
|
||||
import { OptionsOrDependencyArray } from 'react-hotkeys-hook/dist/types';
|
||||
|
||||
export function useUpDownHotkeys(
|
||||
upArrowCallBack: HotkeyCallback,
|
||||
downArrownCallback: HotkeyCallback,
|
||||
dependencies?: OptionsOrDependencyArray,
|
||||
) {
|
||||
useHotkeys(
|
||||
'up',
|
||||
upArrowCallBack,
|
||||
{
|
||||
enableOnContentEditable: true,
|
||||
enableOnFormTags: true,
|
||||
preventDefault: true,
|
||||
},
|
||||
dependencies,
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
'down',
|
||||
downArrownCallback,
|
||||
{
|
||||
enableOnContentEditable: true,
|
||||
enableOnFormTags: true,
|
||||
preventDefault: true,
|
||||
},
|
||||
dependencies,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user