diff --git a/front/package.json b/front/package.json
index cc44cbc8f..e4ed64cf4 100644
--- a/front/package.json
+++ b/front/package.json
@@ -39,6 +39,7 @@
"react-tooltip": "^5.13.1",
"recoil": "^0.7.7",
"scroll-into-view": "^1.16.2",
+ "ts-key-enum": "^2.0.12",
"uuid": "^9.0.0",
"web-vitals": "^2.1.4"
},
diff --git a/front/src/App.tsx b/front/src/App.tsx
index b6c052c8e..a652c5c51 100644
--- a/front/src/App.tsx
+++ b/front/src/App.tsx
@@ -1,11 +1,9 @@
import { Navigate, Route, Routes, useLocation } from 'react-router-dom';
import { AnimatePresence, LayoutGroup } from 'framer-motion';
-import { useTrackPageView } from '@/analytics/hooks/useTrackPageView';
import { RequireOnboarded } from '@/auth/components/RequireOnboarded';
import { RequireOnboarding } from '@/auth/components/RequireOnboarding';
import { AuthModal } from '@/auth/components/ui/Modal';
-import { useGoToHotkeys } from '@/hotkeys/hooks/useGoToHotkeys';
import { AuthLayout } from '@/ui/layout/AuthLayout';
import { DefaultLayout } from '@/ui/layout/DefaultLayout';
import { CreateProfile } from '~/pages/auth/CreateProfile';
@@ -18,6 +16,8 @@ import { Opportunities } from '~/pages/opportunities/Opportunities';
import { People } from '~/pages/people/People';
import { SettingsProfile } from '~/pages/settings/SettingsProfile';
+import { AppInternalHooks } from './AppInternalHooks';
+
/**
* AuthRoutes is used to allow transitions between auth pages with framer-motion.
*/
@@ -42,48 +42,44 @@ function AuthRoutes() {
}
export function App() {
- useGoToHotkeys('p', '/people');
- useGoToHotkeys('c', '/companies');
- useGoToHotkeys('o', '/opportunities');
- useGoToHotkeys('s', '/settings/profile');
-
- useTrackPageView();
-
return (
-
-
-
-
-
-
-
- }
- />
-
-
- } />
- } />
- } />
- } />
-
- } />
-
- }
- />
-
-
- }
- />
-
-
+ <>
+
+
+
+
+
+
+
+
+ }
+ />
+
+
+ } />
+ } />
+ } />
+ } />
+
+ } />
+
+ }
+ />
+
+
+ }
+ />
+
+
+ >
);
}
diff --git a/front/src/AppInternalHooks.tsx b/front/src/AppInternalHooks.tsx
new file mode 100644
index 000000000..5df7c173a
--- /dev/null
+++ b/front/src/AppInternalHooks.tsx
@@ -0,0 +1,13 @@
+import { AnalyticsHook } from './sync-hooks/AnalyticsHook';
+import { GotoHotkeysHooks } from './sync-hooks/GotoHotkeysHooks';
+import { HotkeysScopeStackAutoSyncHook } from './sync-hooks/HotkeysScopeStackAutoSyncHook';
+
+export function AppInternalHooks() {
+ return (
+ <>
+
+
+
+ >
+ );
+}
diff --git a/front/src/index.tsx b/front/src/index.tsx
index 6345cc7a2..639529492 100644
--- a/front/src/index.tsx
+++ b/front/src/index.tsx
@@ -1,8 +1,10 @@
-import React, { StrictMode } from 'react';
+import { StrictMode } from 'react';
import ReactDOM from 'react-dom/client';
+import { HotkeysProvider } from 'react-hotkeys-hook';
import { BrowserRouter } from 'react-router-dom';
import { RecoilRoot } from 'recoil';
+import { INITIAL_HOTKEYS_SCOPES } from '@/hotkeys/constants';
import { ThemeType } from '@/ui/themes/themes';
import '@emotion/react';
@@ -27,9 +29,11 @@ root.render(
-
-
-
+
+
+
+
+
diff --git a/front/src/modules/auth/components/RequireOnboarded.tsx b/front/src/modules/auth/components/RequireOnboarded.tsx
index 8b05ec357..d72ae84d1 100644
--- a/front/src/modules/auth/components/RequireOnboarded.tsx
+++ b/front/src/modules/auth/components/RequireOnboarded.tsx
@@ -2,9 +2,6 @@ import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { keyframes } from '@emotion/react';
import styled from '@emotion/styled';
-import { useRecoilState } from 'recoil';
-
-import { captureHotkeyTypeInFocusState } from '@/hotkeys/states/captureHotkeyTypeInFocusState';
import { useOnboardingStatus } from '../hooks/useOnboardingStatus';
import { OnboardingStatus } from '../utils/getOnboardingStatus';
@@ -38,9 +35,6 @@ export function RequireOnboarded({
}): JSX.Element {
const navigate = useNavigate();
- const [, setCaptureHotkeyTypeInFocus] = useRecoilState(
- captureHotkeyTypeInFocusState,
- );
const onboardingStatus = useOnboardingStatus();
useEffect(() => {
@@ -53,12 +47,6 @@ export function RequireOnboarded({
}
}, [onboardingStatus, navigate]);
- useEffect(() => {
- if (onboardingStatus === OnboardingStatus.Completed) {
- setCaptureHotkeyTypeInFocus(false);
- }
- }, [setCaptureHotkeyTypeInFocus, onboardingStatus]);
-
if (onboardingStatus !== OnboardingStatus.Completed) {
return (
diff --git a/front/src/modules/auth/components/ui/Modal.tsx b/front/src/modules/auth/components/ui/Modal.tsx
index 6e22ff218..284007bc5 100644
--- a/front/src/modules/auth/components/ui/Modal.tsx
+++ b/front/src/modules/auth/components/ui/Modal.tsx
@@ -1,6 +1,8 @@
import React from 'react';
import styled from '@emotion/styled';
+import { useHotkeysScopeOnMountOnly } from '@/hotkeys/hooks/useHotkeysScopeOnMountOnly';
+import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { Modal as UIModal } from '@/ui/components/modal/Modal';
type Props = React.ComponentProps<'div'>;
@@ -17,6 +19,11 @@ const StyledContainer = styled.div`
`;
export function AuthModal({ children, ...restProps }: Props) {
+ useHotkeysScopeOnMountOnly({
+ scope: InternalHotkeysScope.Modal,
+ customScopes: { 'command-menu': false, goto: false },
+ });
+
return (
{children}
diff --git a/front/src/modules/command-menu/components/CommandMenu.tsx b/front/src/modules/command-menu/components/CommandMenu.tsx
index c32265ccf..dcdb1bddd 100644
--- a/front/src/modules/command-menu/components/CommandMenu.tsx
+++ b/front/src/modules/command-menu/components/CommandMenu.tsx
@@ -1,7 +1,8 @@
-import React from 'react';
import { useRecoilState } from 'recoil';
-import { useDirectHotkeys } from '@/hotkeys/hooks/useDirectHotkeys';
+import { useHotkeysScopeOnBooleanState } from '@/hotkeys/hooks/useHotkeysScopeOnBooleanState';
+import { useScopedHotkeys } from '@/hotkeys/hooks/useScopedHotkeys';
+import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { isCommandMenuOpenedState } from '../states/isCommandMenuOpened';
@@ -18,14 +19,20 @@ import {
export function CommandMenu() {
const [open, setOpen] = useRecoilState(isCommandMenuOpenedState);
- useDirectHotkeys(
+ useScopedHotkeys(
'ctrl+k,meta+k',
() => {
setOpen((prevOpen) => !prevOpen);
},
+ InternalHotkeysScope.CommandMenu,
[setOpen],
);
+ useHotkeysScopeOnBooleanState(
+ { scope: InternalHotkeysScope.CommandMenu },
+ open,
+ );
+
/*
TODO: Allow performing actions on page through CommandBar
diff --git a/front/src/modules/comments/components/CommentThreadRelationPicker.tsx b/front/src/modules/comments/components/CommentThreadRelationPicker.tsx
index 66ad701e9..97aeedab8 100644
--- a/front/src/modules/comments/components/CommentThreadRelationPicker.tsx
+++ b/front/src/modules/comments/components/CommentThreadRelationPicker.tsx
@@ -1,5 +1,4 @@
import { useState } from 'react';
-import { useHotkeys } from 'react-hotkeys-hook';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import {
@@ -13,7 +12,11 @@ import { IconArrowUpRight } from '@tabler/icons-react';
import { CommentThreadForDrawer } from '@/comments/types/CommentThreadForDrawer';
import CompanyChip from '@/companies/components/CompanyChip';
+import { useHotkeysScopeOnBooleanState } from '@/hotkeys/hooks/useHotkeysScopeOnBooleanState';
+import { useScopedHotkeys } from '@/hotkeys/hooks/useScopedHotkeys';
+import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { PersonChip } from '@/people/components/PersonChip';
+import { RecoilScope } from '@/recoil-scope/components/RecoilScope';
import { useFilteredSearchEntityQuery } from '@/relation-picker/hooks/useFilteredSearchEntityQuery';
import { useListenClickOutsideArrayOfRef } from '@/ui/hooks/useListenClickOutsideArrayOfRef';
import { flatMapAndSortEntityForSelectArrayOfArrayByName } from '@/ui/utils/flatMapAndSortEntityForSelectArrayByName';
@@ -95,6 +98,11 @@ export function CommentThreadRelationPicker({ commentThread }: OwnProps) {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [searchFilter, setSearchFilter] = useState('');
+ useHotkeysScopeOnBooleanState(
+ { scope: InternalHotkeysScope.RelationPicker },
+ isMenuOpen,
+ );
+
const theme = useTheme();
const peopleIds =
@@ -156,15 +164,12 @@ export function CommentThreadRelationPicker({ commentThread }: OwnProps) {
setSearchFilter('');
}
- useHotkeys(
+ useScopedHotkeys(
['esc', 'enter'],
() => {
exitEditMode();
},
- {
- enableOnContentEditable: true,
- enableOnFormTags: true,
- },
+ InternalHotkeysScope.RelationPicker,
[exitEditMode],
);
@@ -225,19 +230,21 @@ export function CommentThreadRelationPicker({ commentThread }: OwnProps) {
)}
{isMenuOpen && (
-
-
-
+
+
+
+
+
)}
);
diff --git a/front/src/modules/hotkeys/constants/index.ts b/front/src/modules/hotkeys/constants/index.ts
new file mode 100644
index 000000000..ecf2d20cc
--- /dev/null
+++ b/front/src/modules/hotkeys/constants/index.ts
@@ -0,0 +1,17 @@
+import { HotkeysScopeStackItem } from '../types/internal/HotkeysScopeStackItems';
+import { InternalHotkeysScope } from '../types/internal/InternalHotkeysScope';
+
+export const INITIAL_HOTKEYS_SCOPES: string[] = [InternalHotkeysScope.App];
+
+export const ALWAYS_ON_HOTKEYS_SCOPES: string[] = [
+ InternalHotkeysScope.CommandMenu,
+ InternalHotkeysScope.App,
+];
+
+export const DEFAULT_HOTKEYS_SCOPE_STACK_ITEM: HotkeysScopeStackItem = {
+ scope: InternalHotkeysScope.App,
+ customScopes: {
+ 'command-menu': true,
+ goto: true,
+ },
+};
diff --git a/front/src/modules/hotkeys/hooks/internal/useHotkeysScope.ts b/front/src/modules/hotkeys/hooks/internal/useHotkeysScope.ts
new file mode 100644
index 000000000..8129c5f79
--- /dev/null
+++ b/front/src/modules/hotkeys/hooks/internal/useHotkeysScope.ts
@@ -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,
+ };
+}
diff --git a/front/src/modules/hotkeys/hooks/internal/useHotkeysScopeStackAutoSync.ts b/front/src/modules/hotkeys/hooks/internal/useHotkeysScopeStackAutoSync.ts
new file mode 100644
index 000000000..3f5c362bf
--- /dev/null
+++ b/front/src/modules/hotkeys/hooks/internal/useHotkeysScopeStackAutoSync.ts
@@ -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]);
+}
diff --git a/front/src/modules/hotkeys/hooks/useAddToHotkeysScopeStack.ts b/front/src/modules/hotkeys/hooks/useAddToHotkeysScopeStack.ts
new file mode 100644
index 000000000..e18c6ed3e
--- /dev/null
+++ b/front/src/modules/hotkeys/hooks/useAddToHotkeysScopeStack.ts
@@ -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 });
+ }),
+ );
+ },
+ [],
+ );
+}
diff --git a/front/src/modules/hotkeys/hooks/useCurrentHotkeysScope.ts b/front/src/modules/hotkeys/hooks/useCurrentHotkeysScope.ts
new file mode 100644
index 000000000..5d3b14197
--- /dev/null
+++ b/front/src/modules/hotkeys/hooks/useCurrentHotkeysScope.ts
@@ -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]);
+}
diff --git a/front/src/modules/hotkeys/hooks/useGoToHotkeys.ts b/front/src/modules/hotkeys/hooks/useGoToHotkeys.ts
index df67341dc..f097ecfb9 100644
--- a/front/src/modules/hotkeys/hooks/useGoToHotkeys.ts
+++ b/front/src/modules/hotkeys/hooks/useGoToHotkeys.ts
@@ -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,
+ );
}
diff --git a/front/src/modules/hotkeys/hooks/useHotkeysScopeOnBooleanState.ts b/front/src/modules/hotkeys/hooks/useHotkeysScopeOnBooleanState.ts
new file mode 100644
index 000000000..2b82da138
--- /dev/null
+++ b/front/src/modules/hotkeys/hooks/useHotkeysScopeOnBooleanState.ts
@@ -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,
+ ]);
+}
diff --git a/front/src/modules/hotkeys/hooks/useHotkeysScopeOnMountOnly.ts b/front/src/modules/hotkeys/hooks/useHotkeysScopeOnMountOnly.ts
new file mode 100644
index 000000000..aa5f39dbe
--- /dev/null
+++ b/front/src/modules/hotkeys/hooks/useHotkeysScopeOnMountOnly.ts
@@ -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,
+ ]);
+}
diff --git a/front/src/modules/hotkeys/hooks/useRemoveFromHotkeysScopeStack.ts b/front/src/modules/hotkeys/hooks/useRemoveFromHotkeysScopeStack.ts
new file mode 100644
index 000000000..bc8d9344e
--- /dev/null
+++ b/front/src/modules/hotkeys/hooks/useRemoveFromHotkeysScopeStack.ts
@@ -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,
+ );
+ }),
+ );
+ },
+ [],
+ );
+}
diff --git a/front/src/modules/hotkeys/hooks/useRemoveHighestHotkeysScopeStackItem.ts b/front/src/modules/hotkeys/hooks/useRemoveHighestHotkeysScopeStackItem.ts
new file mode 100644
index 000000000..84aca2de1
--- /dev/null
+++ b/front/src/modules/hotkeys/hooks/useRemoveHighestHotkeysScopeStackItem.ts
@@ -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();
+ }),
+ );
+ },
+ [],
+ );
+}
diff --git a/front/src/modules/hotkeys/hooks/useResetHotkeysScopeStack.ts b/front/src/modules/hotkeys/hooks/useResetHotkeysScopeStack.ts
new file mode 100644
index 000000000..424fb48c2
--- /dev/null
+++ b/front/src/modules/hotkeys/hooks/useResetHotkeysScopeStack.ts
@@ -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 });
+ }
+ };
+}
diff --git a/front/src/modules/hotkeys/hooks/useDirectHotkeys.ts b/front/src/modules/hotkeys/hooks/useScopedHotkeys.ts
similarity index 59%
rename from front/src/modules/hotkeys/hooks/useDirectHotkeys.ts
rename to front/src/modules/hotkeys/hooks/useScopedHotkeys.ts
index c2b76e8db..c238cb2d6 100644
--- a/front/src/modules/hotkeys/hooks/useDirectHotkeys.ts
+++ b/front/src/modules/hotkeys/hooks/useScopedHotkeys.ts
@@ -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,
+ );
}
diff --git a/front/src/modules/hotkeys/hooks/useSequenceHotkeys.ts b/front/src/modules/hotkeys/hooks/useSequenceScopedHotkeys.ts
similarity index 53%
rename from front/src/modules/hotkeys/hooks/useSequenceHotkeys.ts
rename to front/src/modules/hotkeys/hooks/useSequenceScopedHotkeys.ts
index 3ec2544bf..b89f8b61f 100644
--- a/front/src/modules/hotkeys/hooks/useSequenceHotkeys.ts
+++ b/front/src/modules/hotkeys/hooks/useSequenceScopedHotkeys.ts
@@ -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],
);
}
diff --git a/front/src/modules/hotkeys/hooks/useUpDownHotkeys.ts b/front/src/modules/hotkeys/hooks/useUpDownHotkeys.ts
deleted file mode 100644
index f5149e37d..000000000
--- a/front/src/modules/hotkeys/hooks/useUpDownHotkeys.ts
+++ /dev/null
@@ -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,
- );
-}
diff --git a/front/src/modules/hotkeys/states/captureHotkeyTypeInFocusState.ts b/front/src/modules/hotkeys/states/captureHotkeyTypeInFocusState.ts
deleted file mode 100644
index 8d239b631..000000000
--- a/front/src/modules/hotkeys/states/captureHotkeyTypeInFocusState.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { atom } from 'recoil';
-
-export const captureHotkeyTypeInFocusState = atom({
- key: 'captureHotkeyTypeInFocusState',
- default: false,
-});
diff --git a/front/src/modules/hotkeys/states/internal/customHotkeysScopesState.ts b/front/src/modules/hotkeys/states/internal/customHotkeysScopesState.ts
new file mode 100644
index 000000000..8b67d1c9b
--- /dev/null
+++ b/front/src/modules/hotkeys/states/internal/customHotkeysScopesState.ts
@@ -0,0 +1,16 @@
+import { atom } from 'recoil';
+
+import { InternalHotkeysScope } from '../../types/internal/InternalHotkeysScope';
+
+export type CustomHotkeysScopes = {
+ [InternalHotkeysScope.Goto]: boolean;
+ [InternalHotkeysScope.CommandMenu]: boolean;
+};
+
+export const customHotkeysScopesState = atom({
+ key: 'customHotkeysScopesState',
+ default: {
+ 'command-menu': true,
+ goto: false,
+ },
+});
diff --git a/front/src/modules/hotkeys/states/internal/hotkeysScopeStackState.ts b/front/src/modules/hotkeys/states/internal/hotkeysScopeStackState.ts
new file mode 100644
index 000000000..dec50f6cc
--- /dev/null
+++ b/front/src/modules/hotkeys/states/internal/hotkeysScopeStackState.ts
@@ -0,0 +1,9 @@
+import { atom } from 'recoil';
+
+import { DEFAULT_HOTKEYS_SCOPE_STACK_ITEM } from '@/hotkeys/constants';
+import { HotkeysScopeStackItem } from '@/hotkeys/types/internal/HotkeysScopeStackItems';
+
+export const hotkeysScopeStackState = atom({
+ key: 'hotkeysScopeStackState',
+ default: [DEFAULT_HOTKEYS_SCOPE_STACK_ITEM],
+});
diff --git a/front/src/modules/hotkeys/states/internal/internalHotkeysEnabledScopesState.ts b/front/src/modules/hotkeys/states/internal/internalHotkeysEnabledScopesState.ts
new file mode 100644
index 000000000..5bc666110
--- /dev/null
+++ b/front/src/modules/hotkeys/states/internal/internalHotkeysEnabledScopesState.ts
@@ -0,0 +1,6 @@
+import { atom } from 'recoil';
+
+export const internalHotkeysEnabledScopesState = atom({
+ key: 'internalHotkeysEnabledScopesState',
+ default: [],
+});
diff --git a/front/src/modules/hotkeys/states/internal/pendingHotkeysState.ts b/front/src/modules/hotkeys/states/internal/pendingHotkeysState.ts
new file mode 100644
index 000000000..da2dbf173
--- /dev/null
+++ b/front/src/modules/hotkeys/states/internal/pendingHotkeysState.ts
@@ -0,0 +1,7 @@
+import { Keys } from 'react-hotkeys-hook/dist/types';
+import { atom } from 'recoil';
+
+export const pendingHotkeyState = atom({
+ key: 'pendingHotkeyState',
+ default: null,
+});
diff --git a/front/src/modules/hotkeys/states/pendingHotkeysState.ts b/front/src/modules/hotkeys/states/pendingHotkeysState.ts
deleted file mode 100644
index da724a5fd..000000000
--- a/front/src/modules/hotkeys/states/pendingHotkeysState.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { atom } from 'recoil';
-
-export const pendingHotkeyState = atom({
- key: 'pendingHotkeyState',
- default: null,
-});
diff --git a/front/src/modules/hotkeys/types/HotkeysScope.ts b/front/src/modules/hotkeys/types/HotkeysScope.ts
new file mode 100644
index 000000000..b5d4606ac
--- /dev/null
+++ b/front/src/modules/hotkeys/types/HotkeysScope.ts
@@ -0,0 +1,3 @@
+export enum HotkeysScope {
+ CompanyPage = 'company-page',
+}
diff --git a/front/src/modules/hotkeys/types/internal/HotkeysScopeStackItems.ts b/front/src/modules/hotkeys/types/internal/HotkeysScopeStackItems.ts
new file mode 100644
index 000000000..2caaaf735
--- /dev/null
+++ b/front/src/modules/hotkeys/types/internal/HotkeysScopeStackItems.ts
@@ -0,0 +1,7 @@
+import { CustomHotkeysScopes } from '@/hotkeys/states/internal/customHotkeysScopesState';
+
+export type HotkeysScopeStackItem = {
+ scope: string;
+ customScopes?: CustomHotkeysScopes;
+ ancestorScope?: string | null;
+};
diff --git a/front/src/modules/hotkeys/types/internal/InternalHotkeysScope.ts b/front/src/modules/hotkeys/types/internal/InternalHotkeysScope.ts
new file mode 100644
index 000000000..aa54d6084
--- /dev/null
+++ b/front/src/modules/hotkeys/types/internal/InternalHotkeysScope.ts
@@ -0,0 +1,14 @@
+export enum InternalHotkeysScope {
+ App = 'app',
+ Goto = 'goto',
+ CommandMenu = 'command-menu',
+ Table = 'table',
+ TableSoftFocus = 'table-soft-focus',
+ CellEditMode = 'cell-edit-mode',
+ RightDrawer = 'right-drawer',
+ TableHeaderDropdownButton = 'table-header-dropdown-button',
+ CreateProfile = 'create-profile',
+ RelationPicker = 'relation-picker',
+ CellDoubleTextInput = 'cell-double-text-input',
+ Modal = 'modal',
+}
diff --git a/front/src/modules/people/components/PeopleCompanyCell.tsx b/front/src/modules/people/components/PeopleCompanyCell.tsx
index 6b34efba7..eb2c2cd63 100644
--- a/front/src/modules/people/components/PeopleCompanyCell.tsx
+++ b/front/src/modules/people/components/PeopleCompanyCell.tsx
@@ -1,4 +1,5 @@
import CompanyChip from '@/companies/components/CompanyChip';
+import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
import { EditableCell } from '@/ui/components/editable-cell/EditableCell';
import { isCreateModeScopedState } from '@/ui/components/editable-cell/states/isCreateModeScopedState';
@@ -19,6 +20,9 @@ export function PeopleCompanyCell({ people }: OwnProps) {
return (
diff --git a/front/src/modules/people/components/PeopleCompanyPicker.tsx b/front/src/modules/people/components/PeopleCompanyPicker.tsx
index 8d659702f..c13cbaf61 100644
--- a/front/src/modules/people/components/PeopleCompanyPicker.tsx
+++ b/front/src/modules/people/components/PeopleCompanyPicker.tsx
@@ -1,3 +1,7 @@
+import { Key } from 'ts-key-enum';
+
+import { useScopedHotkeys } from '@/hotkeys/hooks/useScopedHotkeys';
+import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
import { SingleEntitySelect } from '@/relation-picker/components/SingleEntitySelect';
import { useFilteredSearchEntityQuery } from '@/relation-picker/hooks/useFilteredSearchEntityQuery';
@@ -57,6 +61,13 @@ export function PeopleCompanyPicker({ people }: OwnProps) {
setIsCreating(true);
}
+ useScopedHotkeys(
+ Key.Escape,
+ () => closeEditableCell(),
+ InternalHotkeysScope.RelationPicker,
+ [closeEditableCell],
+ );
+
return (
{
onEntitySelected(entitiesInDropdown[hoveredIndex]);
},
- {
- enableOnContentEditable: true,
- enableOnFormTags: true,
- },
+ InternalHotkeysScope.RelationPicker,
[entitiesInDropdown, hoveredIndex, onEntitySelected],
);
diff --git a/front/src/modules/relation-picker/hooks/useEntitySelectScroll.ts b/front/src/modules/relation-picker/hooks/useEntitySelectScroll.ts
index 5144e5b43..4becc4562 100644
--- a/front/src/modules/relation-picker/hooks/useEntitySelectScroll.ts
+++ b/front/src/modules/relation-picker/hooks/useEntitySelectScroll.ts
@@ -1,6 +1,8 @@
import scrollIntoView from 'scroll-into-view';
+import { Key } from 'ts-key-enum';
-import { useUpDownHotkeys } from '@/hotkeys/hooks/useUpDownHotkeys';
+import { useScopedHotkeys } from '@/hotkeys/hooks/useScopedHotkeys';
+import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
import { relationPickerHoverIndexScopedState } from '../states/relationPickerHoverIndexScopedState';
@@ -19,7 +21,8 @@ export function useEntitySelectScroll<
relationPickerHoverIndexScopedState,
);
- useUpDownHotkeys(
+ useScopedHotkeys(
+ Key.ArrowUp,
() => {
setHoveredIndex((prevSelectedIndex) =>
Math.max(prevSelectedIndex - 1, 0),
@@ -41,6 +44,12 @@ export function useEntitySelectScroll<
});
}
},
+ InternalHotkeysScope.RelationPicker,
+ [setHoveredIndex, entities],
+ );
+
+ useScopedHotkeys(
+ Key.ArrowDown,
() => {
setHoveredIndex((prevSelectedIndex) =>
Math.min(prevSelectedIndex + 1, (entities?.length ?? 0) - 1),
@@ -62,6 +71,7 @@ export function useEntitySelectScroll<
});
}
},
+ InternalHotkeysScope.RelationPicker,
[setHoveredIndex, entities],
);
diff --git a/front/src/modules/ui/components/editable-cell/EditableCell.tsx b/front/src/modules/ui/components/editable-cell/EditableCell.tsx
index 15152a501..65ec7fbf5 100644
--- a/front/src/modules/ui/components/editable-cell/EditableCell.tsx
+++ b/front/src/modules/ui/components/editable-cell/EditableCell.tsx
@@ -1,12 +1,16 @@
import { ReactElement } from 'react';
import styled from '@emotion/styled';
+import { useRecoilValue } from 'recoil';
-import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
+import { useAddToHotkeysScopeStack } from '@/hotkeys/hooks/useAddToHotkeysScopeStack';
+import { HotkeysScopeStackItem } from '@/hotkeys/types/internal/HotkeysScopeStackItems';
+import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
+import { isSoftFocusActiveState } from '@/ui/tables/states/isSoftFocusActiveState';
import { useEditableCell } from './hooks/useCloseEditableCell';
+import { useCurrentCellEditMode } from './hooks/useCurrentCellEditMode';
import { useIsSoftFocusOnCurrentCell } from './hooks/useIsSoftFocusOnCurrentCell';
-import { useSetSoftFocusOnCurrentCell } from './hooks/useSetSoftFocusOnCurrentCell';
-import { isEditModeScopedState } from './states/isEditModeScopedState';
+import { useSoftFocusOnCurrentCell } from './hooks/useSetSoftFocusOnCurrentCell';
import { EditableCellDisplayMode } from './EditableCellDisplayMode';
import { EditableCellEditMode } from './EditableCellEditMode';
import { EditableCellSoftFocusMode } from './EditableCellSoftFocusMode';
@@ -27,6 +31,7 @@ type OwnProps = {
nonEditModeContent: ReactElement;
editModeHorizontalAlign?: 'left' | 'right';
editModeVerticalPosition?: 'over' | 'below';
+ editHotkeysScope?: HotkeysScopeStackItem;
};
export function EditableCell({
@@ -34,39 +39,51 @@ export function EditableCell({
editModeVerticalPosition = 'over',
editModeContent,
nonEditModeContent,
+ editHotkeysScope,
}: OwnProps) {
- const [isEditMode] = useRecoilScopedState(isEditModeScopedState);
+ const { isCurrentCellInEditMode } = useCurrentCellEditMode();
- const setSoftFocusOnCurrentCell = useSetSoftFocusOnCurrentCell();
+ const setSoftFocusOnCurrentCell = useSoftFocusOnCurrentCell();
- const { closeEditableCell, openEditableCell } = useEditableCell();
+ const { openEditableCell } = useEditableCell();
+
+ const isSoftFocusActive = useRecoilValue(isSoftFocusActiveState);
+
+ const addToHotkeysScopeStack = useAddToHotkeysScopeStack();
// TODO: we might have silent problematic behavior because of the setTimeout in openEditableCell, investigate
// Maybe we could build a switchEditableCell to handle the case where we go from one cell to another.
// See https://github.com/twentyhq/twenty/issues/446
function handleOnClick() {
- openEditableCell();
- setSoftFocusOnCurrentCell();
- }
+ if (isCurrentCellInEditMode) {
+ return;
+ }
- function handleOnOutsideClick() {
- closeEditableCell();
+ if (isSoftFocusActive) {
+ openEditableCell();
+ addToHotkeysScopeStack(
+ editHotkeysScope ?? {
+ scope: InternalHotkeysScope.CellEditMode,
+ },
+ );
+ }
+
+ setSoftFocusOnCurrentCell();
}
const hasSoftFocus = useIsSoftFocusOnCurrentCell();
return (
- {isEditMode ? (
+ {isCurrentCellInEditMode ? (
{editModeContent}
) : hasSoftFocus ? (
-
+
{nonEditModeContent}
) : (
diff --git a/front/src/modules/ui/components/editable-cell/EditableCellEditMode.tsx b/front/src/modules/ui/components/editable-cell/EditableCellEditMode.tsx
index 79249bbe1..44cdbc050 100644
--- a/front/src/modules/ui/components/editable-cell/EditableCellEditMode.tsx
+++ b/front/src/modules/ui/components/editable-cell/EditableCellEditMode.tsx
@@ -1,7 +1,8 @@
import { ReactElement, useRef } from 'react';
-import { useHotkeys } from 'react-hotkeys-hook';
import styled from '@emotion/styled';
+import { useScopedHotkeys } from '@/hotkeys/hooks/useScopedHotkeys';
+import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { useListenClickOutsideArrayOfRef } from '@/ui/hooks/useListenClickOutsideArrayOfRef';
import { useMoveSoftFocus } from '@/ui/tables/hooks/useMoveSoftFocus';
import { overlayBackground } from '@/ui/themes/effects';
@@ -38,7 +39,6 @@ export function EditableCellEditMode({
editModeHorizontalAlign,
editModeVerticalPosition,
children,
- onOutsideClick,
}: OwnProps) {
const wrapperRef = useRef(null);
@@ -46,61 +46,45 @@ export function EditableCellEditMode({
const { moveRight, moveLeft, moveDown } = useMoveSoftFocus();
useListenClickOutsideArrayOfRef([wrapperRef], () => {
- onOutsideClick?.();
+ closeEditableCell();
});
- useHotkeys(
+ useScopedHotkeys(
'enter',
() => {
closeEditableCell();
moveDown();
},
- {
- enableOnContentEditable: true,
- enableOnFormTags: true,
- preventDefault: true,
- },
+ InternalHotkeysScope.CellEditMode,
[closeEditableCell],
);
- useHotkeys(
+ useScopedHotkeys(
'esc',
() => {
closeEditableCell();
},
- {
- enableOnContentEditable: true,
- enableOnFormTags: true,
- preventDefault: true,
- },
+ InternalHotkeysScope.CellEditMode,
[closeEditableCell],
);
- useHotkeys(
+ useScopedHotkeys(
'tab',
() => {
closeEditableCell();
moveRight();
},
- {
- enableOnContentEditable: true,
- enableOnFormTags: true,
- preventDefault: true,
- },
+ InternalHotkeysScope.CellEditMode,
[closeEditableCell, moveRight],
);
- useHotkeys(
+ useScopedHotkeys(
'shift+tab',
() => {
closeEditableCell();
moveLeft();
},
- {
- enableOnContentEditable: true,
- enableOnFormTags: true,
- preventDefault: true,
- },
+ InternalHotkeysScope.CellEditMode,
[closeEditableCell, moveRight],
);
diff --git a/front/src/modules/ui/components/editable-cell/EditableCellSoftFocusMode.tsx b/front/src/modules/ui/components/editable-cell/EditableCellSoftFocusMode.tsx
index d67da443a..f098534d1 100644
--- a/front/src/modules/ui/components/editable-cell/EditableCellSoftFocusMode.tsx
+++ b/front/src/modules/ui/components/editable-cell/EditableCellSoftFocusMode.tsx
@@ -1,8 +1,9 @@
import React from 'react';
-import { useHotkeys } from 'react-hotkeys-hook';
-import { useRecoilState } from 'recoil';
-import { captureHotkeyTypeInFocusState } from '@/hotkeys/states/captureHotkeyTypeInFocusState';
+import { useAddToHotkeysScopeStack } from '@/hotkeys/hooks/useAddToHotkeysScopeStack';
+import { useScopedHotkeys } from '@/hotkeys/hooks/useScopedHotkeys';
+import { HotkeysScopeStackItem } from '@/hotkeys/types/internal/HotkeysScopeStackItems';
+import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { isNonTextWritingKey } from '@/utils/hotkeys/isNonTextWritingKey';
import { useEditableCell } from './hooks/useCloseEditableCell';
@@ -10,26 +11,26 @@ import { EditableCellDisplayMode } from './EditableCellDisplayMode';
export function EditableCellSoftFocusMode({
children,
-}: React.PropsWithChildren) {
+ editHotkeysScope,
+}: React.PropsWithChildren<{ editHotkeysScope?: HotkeysScopeStackItem }>) {
const { closeEditableCell, openEditableCell } = useEditableCell();
- const [captureHotkeyTypeInFocus] = useRecoilState(
- captureHotkeyTypeInFocusState,
- );
+ const addToHotkeysScopeStack = useAddToHotkeysScopeStack();
- useHotkeys(
+ useScopedHotkeys(
'enter',
() => {
openEditableCell();
+ addToHotkeysScopeStack(
+ editHotkeysScope ?? {
+ scope: InternalHotkeysScope.CellEditMode,
+ },
+ );
},
- {
- enableOnContentEditable: true,
- enableOnFormTags: true,
- preventDefault: true,
- },
+ InternalHotkeysScope.TableSoftFocus,
[closeEditableCell],
);
- useHotkeys(
+ useScopedHotkeys(
'*',
(keyboardEvent) => {
const isWritingText =
@@ -41,14 +42,16 @@ export function EditableCellSoftFocusMode({
return;
}
- if (captureHotkeyTypeInFocus) {
- return;
- }
openEditableCell();
+ addToHotkeysScopeStack(
+ editHotkeysScope ?? {
+ scope: InternalHotkeysScope.CellEditMode,
+ },
+ );
},
+ InternalHotkeysScope.TableSoftFocus,
+ [openEditableCell, addToHotkeysScopeStack, editHotkeysScope],
{
- enableOnContentEditable: true,
- enableOnFormTags: true,
preventDefault: false,
},
);
diff --git a/front/src/modules/ui/components/editable-cell/hooks/useCloseEditableCell.ts b/front/src/modules/ui/components/editable-cell/hooks/useCloseEditableCell.ts
index c2cc3b4e8..2cec0221b 100644
--- a/front/src/modules/ui/components/editable-cell/hooks/useCloseEditableCell.ts
+++ b/front/src/modules/ui/components/editable-cell/hooks/useCloseEditableCell.ts
@@ -1,24 +1,24 @@
import { useRecoilCallback } from 'recoil';
-import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
+import { useRemoveHighestHotkeysScopeStackItem } from '@/hotkeys/hooks/useRemoveHighestHotkeysScopeStackItem';
+import { useCloseCurrentCellInEditMode } from '@/ui/tables/hooks/useClearCellInEditMode';
+import { isSoftFocusActiveState } from '@/ui/tables/states/isSoftFocusActiveState';
import { isSomeInputInEditModeState } from '@/ui/tables/states/isSomeInputInEditModeState';
-import { isEditModeScopedState } from '../states/isEditModeScopedState';
+import { useCurrentCellEditMode } from './useCurrentCellEditMode';
export function useEditableCell() {
- const [, setIsEditMode] = useRecoilScopedState(isEditModeScopedState);
+ const { setCurrentCellInEditMode } = useCurrentCellEditMode();
- const closeEditableCell = useRecoilCallback(
- ({ set }) =>
- async () => {
- setIsEditMode(false);
+ const closeCurrentCellInEditMode = useCloseCurrentCellInEditMode();
- await new Promise((resolve) => setTimeout(resolve, 20));
+ const removeHighestHotkeysScopedStackItem =
+ useRemoveHighestHotkeysScopeStackItem();
- set(isSomeInputInEditModeState, false);
- },
- [setIsEditMode],
- );
+ function closeEditableCell() {
+ closeCurrentCellInEditMode();
+ removeHighestHotkeysScopedStackItem();
+ }
const openEditableCell = useRecoilCallback(
({ snapshot, set }) =>
@@ -29,11 +29,12 @@ export function useEditableCell() {
if (!isSomeInputInEditMode) {
set(isSomeInputInEditModeState, true);
+ set(isSoftFocusActiveState, false);
- setIsEditMode(true);
+ setCurrentCellInEditMode();
}
},
- [setIsEditMode],
+ [setCurrentCellInEditMode],
);
return {
diff --git a/front/src/modules/ui/components/editable-cell/hooks/useCurrentCellEditMode.ts b/front/src/modules/ui/components/editable-cell/hooks/useCurrentCellEditMode.ts
new file mode 100644
index 000000000..44453e903
--- /dev/null
+++ b/front/src/modules/ui/components/editable-cell/hooks/useCurrentCellEditMode.ts
@@ -0,0 +1,23 @@
+import { useCallback } from 'react';
+import { useRecoilState } from 'recoil';
+
+import { useMoveEditModeToCellPosition } from '@/ui/tables/hooks/useMoveEditModeToCellPosition';
+import { isCellInEditModeFamilyState } from '@/ui/tables/states/isCellInEditModeFamilyState';
+
+import { useCurrentCellPosition } from './useCurrentCellPosition';
+
+export function useCurrentCellEditMode() {
+ const moveEditModeToCellPosition = useMoveEditModeToCellPosition();
+
+ const currentCellPosition = useCurrentCellPosition();
+
+ const [isCurrentCellInEditMode] = useRecoilState(
+ isCellInEditModeFamilyState(currentCellPosition),
+ );
+
+ const setCurrentCellInEditMode = useCallback(() => {
+ moveEditModeToCellPosition(currentCellPosition);
+ }, [currentCellPosition, moveEditModeToCellPosition]);
+
+ return { isCurrentCellInEditMode, setCurrentCellInEditMode };
+}
diff --git a/front/src/modules/ui/components/editable-cell/hooks/useCurrentCellPosition.ts b/front/src/modules/ui/components/editable-cell/hooks/useCurrentCellPosition.ts
new file mode 100644
index 000000000..c2811971e
--- /dev/null
+++ b/front/src/modules/ui/components/editable-cell/hooks/useCurrentCellPosition.ts
@@ -0,0 +1,30 @@
+import { useMemo } from 'react';
+
+import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
+import { CellContext } from '@/ui/tables/states/CellContext';
+import { currentColumnNumberScopedState } from '@/ui/tables/states/currentColumnNumberScopedState';
+import { currentRowNumberScopedState } from '@/ui/tables/states/currentRowNumberScopedState';
+import { RowContext } from '@/ui/tables/states/RowContext';
+import { CellPosition } from '@/ui/tables/types/CellPosition';
+
+export function useCurrentCellPosition() {
+ const [currentRowNumber] = useRecoilScopedState(
+ currentRowNumberScopedState,
+ RowContext,
+ );
+
+ const [currentColumnNumber] = useRecoilScopedState(
+ currentColumnNumberScopedState,
+ CellContext,
+ );
+
+ const currentCellPosition: CellPosition = useMemo(
+ () => ({
+ column: currentColumnNumber,
+ row: currentRowNumber,
+ }),
+ [currentColumnNumber, currentRowNumber],
+ );
+
+ return currentCellPosition;
+}
diff --git a/front/src/modules/ui/components/editable-cell/hooks/useIsSoftFocusOnCurrentCell.ts b/front/src/modules/ui/components/editable-cell/hooks/useIsSoftFocusOnCurrentCell.ts
index 329946eac..3a04d08a9 100644
--- a/front/src/modules/ui/components/editable-cell/hooks/useIsSoftFocusOnCurrentCell.ts
+++ b/front/src/modules/ui/components/editable-cell/hooks/useIsSoftFocusOnCurrentCell.ts
@@ -1,35 +1,14 @@
-import { useMemo } from 'react';
import { useRecoilValue } from 'recoil';
-import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
-import { CellContext } from '@/ui/tables/states/CellContext';
-import { currentColumnNumberScopedState } from '@/ui/tables/states/currentColumnNumberScopedState';
-import { currentRowNumberScopedState } from '@/ui/tables/states/currentRowNumberScopedState';
import { isSoftFocusOnCellFamilyState } from '@/ui/tables/states/isSoftFocusOnCellFamilyState';
-import { RowContext } from '@/ui/tables/states/RowContext';
-import { TablePosition } from '@/ui/tables/types/TablePosition';
+
+import { useCurrentCellPosition } from './useCurrentCellPosition';
export function useIsSoftFocusOnCurrentCell() {
- const [currentRowNumber] = useRecoilScopedState(
- currentRowNumberScopedState,
- RowContext,
- );
-
- const [currentColumnNumber] = useRecoilScopedState(
- currentColumnNumberScopedState,
- CellContext,
- );
-
- const currentTablePosition: TablePosition = useMemo(
- () => ({
- column: currentColumnNumber,
- row: currentRowNumber,
- }),
- [currentColumnNumber, currentRowNumber],
- );
+ const currentCellPosition = useCurrentCellPosition();
const isSoftFocusOnCell = useRecoilValue(
- isSoftFocusOnCellFamilyState(currentTablePosition),
+ isSoftFocusOnCellFamilyState(currentCellPosition),
);
return isSoftFocusOnCell;
diff --git a/front/src/modules/ui/components/editable-cell/hooks/useSetSoftFocusOnCurrentCell.ts b/front/src/modules/ui/components/editable-cell/hooks/useSetSoftFocusOnCurrentCell.ts
index e31aa6818..cea6636a0 100644
--- a/front/src/modules/ui/components/editable-cell/hooks/useSetSoftFocusOnCurrentCell.ts
+++ b/front/src/modules/ui/components/editable-cell/hooks/useSetSoftFocusOnCurrentCell.ts
@@ -1,14 +1,18 @@
import { useCallback, useMemo } from 'react';
+import { useRecoilState } from 'recoil';
+import { useAddToHotkeysScopeStack } from '@/hotkeys/hooks/useAddToHotkeysScopeStack';
+import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
import { useSetSoftFocusPosition } from '@/ui/tables/hooks/useSetSoftFocusPosition';
import { CellContext } from '@/ui/tables/states/CellContext';
import { currentColumnNumberScopedState } from '@/ui/tables/states/currentColumnNumberScopedState';
import { currentRowNumberScopedState } from '@/ui/tables/states/currentRowNumberScopedState';
+import { isSoftFocusActiveState } from '@/ui/tables/states/isSoftFocusActiveState';
import { RowContext } from '@/ui/tables/states/RowContext';
-import { TablePosition } from '@/ui/tables/types/TablePosition';
+import { CellPosition } from '@/ui/tables/types/CellPosition';
-export function useSetSoftFocusOnCurrentCell() {
+export function useSoftFocusOnCurrentCell() {
const setSoftFocusPosition = useSetSoftFocusPosition();
const [currentRowNumber] = useRecoilScopedState(
currentRowNumberScopedState,
@@ -20,7 +24,7 @@ export function useSetSoftFocusOnCurrentCell() {
CellContext,
);
- const currentTablePosition: TablePosition = useMemo(
+ const currentTablePosition: CellPosition = useMemo(
() => ({
column: currentColumnNumber,
row: currentRowNumber,
@@ -28,7 +32,18 @@ export function useSetSoftFocusOnCurrentCell() {
[currentColumnNumber, currentRowNumber],
);
+ const [, setIsSoftFocusActive] = useRecoilState(isSoftFocusActiveState);
+
+ const addToHotkeysScopeStack = useAddToHotkeysScopeStack();
+
return useCallback(() => {
setSoftFocusPosition(currentTablePosition);
- }, [setSoftFocusPosition, currentTablePosition]);
+ setIsSoftFocusActive(true);
+ addToHotkeysScopeStack({ scope: InternalHotkeysScope.TableSoftFocus });
+ }, [
+ setSoftFocusPosition,
+ currentTablePosition,
+ setIsSoftFocusActive,
+ addToHotkeysScopeStack,
+ ]);
}
diff --git a/front/src/modules/ui/components/editable-cell/states/isEditModeScopedState.ts b/front/src/modules/ui/components/editable-cell/states/isEditModeScopedState.ts
deleted file mode 100644
index 74b04feb4..000000000
--- a/front/src/modules/ui/components/editable-cell/states/isEditModeScopedState.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { atomFamily } from 'recoil';
-
-export const isEditModeScopedState = atomFamily({
- key: 'isEditModeScopedState',
- default: false,
-});
diff --git a/front/src/modules/ui/components/editable-cell/types/EditableDoubleText.tsx b/front/src/modules/ui/components/editable-cell/types/EditableDoubleText.tsx
index c450c7eee..ecdcfb6cd 100644
--- a/front/src/modules/ui/components/editable-cell/types/EditableDoubleText.tsx
+++ b/front/src/modules/ui/components/editable-cell/types/EditableDoubleText.tsx
@@ -1,10 +1,11 @@
-import { ChangeEvent, ReactElement, useRef } from 'react';
-import styled from '@emotion/styled';
+import { ReactElement } from 'react';
-import { textInputStyle } from '@/ui/themes/effects';
+import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { EditableCell } from '../EditableCell';
+import { EditableDoubleTextEditMode } from './EditableDoubleTextEditMode';
+
type OwnProps = {
firstValue: string;
secondValue: string;
@@ -14,57 +15,25 @@ type OwnProps = {
onChange: (firstValue: string, secondValue: string) => void;
};
-const StyledContainer = styled.div`
- align-items: center;
- display: flex;
- justify-content: space-between;
-
- & > input:last-child {
- border-left: 1px solid ${({ theme }) => theme.border.color.medium};
- padding-left: ${({ theme }) => theme.spacing(2)};
- }
-`;
-
-const StyledEditInplaceInput = styled.input`
- height: 18px;
- margin: 0;
- width: 45%;
-
- ${textInputStyle}
-`;
-
export function EditableDoubleText({
firstValue,
secondValue,
firstValuePlaceholder,
secondValuePlaceholder,
- nonEditModeContent,
onChange,
+ nonEditModeContent,
}: OwnProps) {
- const firstValueInputRef = useRef(null);
-
return (
- ) => {
- onChange(event.target.value, secondValue);
- }}
- />
- ) => {
- onChange(firstValue, event.target.value);
- }}
- />
-
+
}
nonEditModeContent={nonEditModeContent}
>
diff --git a/front/src/modules/ui/components/editable-cell/types/EditableDoubleTextEditMode.tsx b/front/src/modules/ui/components/editable-cell/types/EditableDoubleTextEditMode.tsx
new file mode 100644
index 000000000..2b1c0a39a
--- /dev/null
+++ b/front/src/modules/ui/components/editable-cell/types/EditableDoubleTextEditMode.tsx
@@ -0,0 +1,129 @@
+import { ChangeEvent, useRef, useState } from 'react';
+import styled from '@emotion/styled';
+import { Key } from 'ts-key-enum';
+
+import { useScopedHotkeys } from '@/hotkeys/hooks/useScopedHotkeys';
+import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
+import { useMoveSoftFocus } from '@/ui/tables/hooks/useMoveSoftFocus';
+import { textInputStyle } from '@/ui/themes/effects';
+
+import { useEditableCell } from '../hooks/useCloseEditableCell';
+
+type OwnProps = {
+ firstValue: string;
+ secondValue: string;
+ firstValuePlaceholder: string;
+ secondValuePlaceholder: string;
+ onChange: (firstValue: string, secondValue: string) => void;
+};
+
+const StyledContainer = styled.div`
+ align-items: center;
+ display: flex;
+ justify-content: space-between;
+
+ & > input:last-child {
+ border-left: 1px solid ${({ theme }) => theme.border.color.medium};
+ padding-left: ${({ theme }) => theme.spacing(2)};
+ }
+`;
+
+const StyledEditInplaceInput = styled.input`
+ height: 18px;
+ margin: 0;
+ width: 45%;
+
+ ${textInputStyle}
+`;
+
+export function EditableDoubleTextEditMode({
+ firstValue,
+ secondValue,
+ firstValuePlaceholder,
+ secondValuePlaceholder,
+ onChange,
+}: OwnProps) {
+ const [focusPosition, setFocusPosition] = useState<'left' | 'right'>('left');
+
+ const firstValueInputRef = useRef(null);
+ const secondValueInputRef = useRef(null);
+
+ const { closeEditableCell } = useEditableCell();
+ const { moveRight, moveLeft, moveDown } = useMoveSoftFocus();
+
+ function closeCell() {
+ setFocusPosition('left');
+ closeEditableCell();
+ }
+
+ useScopedHotkeys(
+ Key.Enter,
+ () => {
+ closeCell();
+ moveDown();
+ },
+ InternalHotkeysScope.CellDoubleTextInput,
+ [closeCell],
+ );
+
+ useScopedHotkeys(
+ Key.Escape,
+ () => {
+ closeCell();
+ },
+ InternalHotkeysScope.CellDoubleTextInput,
+ [closeCell],
+ );
+
+ useScopedHotkeys(
+ 'tab',
+ async (keyboardEvent, hotkeyEvent) => {
+ if (focusPosition === 'left') {
+ setFocusPosition('right');
+ secondValueInputRef.current?.focus();
+ } else {
+ closeCell();
+ moveRight();
+ }
+ },
+ InternalHotkeysScope.CellDoubleTextInput,
+ [closeCell, moveRight, focusPosition],
+ );
+
+ useScopedHotkeys(
+ 'shift+tab',
+ () => {
+ if (focusPosition === 'right') {
+ setFocusPosition('left');
+ firstValueInputRef.current?.focus();
+ } else {
+ closeCell();
+ moveLeft();
+ }
+ },
+ InternalHotkeysScope.CellDoubleTextInput,
+ [closeCell, moveRight, focusPosition],
+ );
+
+ return (
+
+ ) => {
+ onChange(event.target.value, secondValue);
+ }}
+ />
+ ) => {
+ onChange(firstValue, event.target.value);
+ }}
+ />
+
+ );
+}
diff --git a/front/src/modules/ui/components/table/EntityTable.tsx b/front/src/modules/ui/components/table/EntityTable.tsx
index 5390f4219..579743742 100644
--- a/front/src/modules/ui/components/table/EntityTable.tsx
+++ b/front/src/modules/ui/components/table/EntityTable.tsx
@@ -13,6 +13,8 @@ import {
SortType,
} from '@/filters-and-sorts/interfaces/sorts/interface';
import { RecoilScope } from '@/recoil-scope/components/RecoilScope';
+import { useListenClickOutsideArrayOfRef } from '@/ui/hooks/useListenClickOutsideArrayOfRef';
+import { useLeaveTableFocus } from '@/ui/tables/hooks/useLeaveTableFocus';
import { RowContext } from '@/ui/tables/states/RowContext';
import { currentRowSelectionState } from '../../tables/states/rowSelectionState';
@@ -103,6 +105,8 @@ export function EntityTable({
availableSorts,
onSortsUpdate,
}: OwnProps) {
+ const tableBodyRef = React.useRef(null);
+
const [currentRowSelection, setCurrentRowSelection] = useRecoilState(
currentRowSelectionState,
);
@@ -119,6 +123,12 @@ export function EntityTable({
getRowId: (row) => row.id,
});
+ const leaveTableFocus = useLeaveTableFocus();
+
+ useListenClickOutsideArrayOfRef([tableBodyRef], () => {
+ leaveTableFocus();
+ });
+
return (
({
availableSorts={availableSorts}
onSortsUpdate={onSortsUpdate}
/>
-
+
{table.getHeaderGroups().map((headerGroup) => (
diff --git a/front/src/modules/ui/components/table/table-header/DropdownButton.tsx b/front/src/modules/ui/components/table/table-header/DropdownButton.tsx
index 72e0b8c72..a89a3cc2d 100644
--- a/front/src/modules/ui/components/table/table-header/DropdownButton.tsx
+++ b/front/src/modules/ui/components/table/table-header/DropdownButton.tsx
@@ -1,8 +1,9 @@
import { ReactNode, useRef } from 'react';
import styled from '@emotion/styled';
-import { useRecoilState } from 'recoil';
+import { Key } from 'ts-key-enum';
-import { captureHotkeyTypeInFocusState } from '@/hotkeys/states/captureHotkeyTypeInFocusState';
+import { useScopedHotkeys } from '@/hotkeys/hooks/useScopedHotkeys';
+import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { IconChevronDown } from '@/ui/icons/index';
import { overlayBackground, textInputStyle } from '@/ui/themes/effects';
@@ -13,7 +14,7 @@ type OwnProps = {
isActive: boolean;
children?: ReactNode;
isUnfolded?: boolean;
- setIsUnfolded?: React.Dispatch>;
+ onIsUnfoldedChange?: (newIsUnfolded: boolean) => void;
resetState?: () => void;
};
@@ -158,22 +159,23 @@ function DropdownButton({
isActive,
children,
isUnfolded = false,
- setIsUnfolded,
- resetState,
+ onIsUnfoldedChange,
}: OwnProps) {
- const [, setCaptureHotkeyTypeInFocus] = useRecoilState(
- captureHotkeyTypeInFocusState,
+ useScopedHotkeys(
+ [Key.Enter, Key.Escape],
+ () => {
+ onIsUnfoldedChange?.(false);
+ },
+ InternalHotkeysScope.TableHeaderDropdownButton,
+ [onIsUnfoldedChange],
);
const onButtonClick = () => {
- setIsUnfolded && setIsUnfolded(!isUnfolded);
- setCaptureHotkeyTypeInFocus((isPreviousUnfolded) => !isPreviousUnfolded);
+ onIsUnfoldedChange?.(!isUnfolded);
};
const onOutsideClick = () => {
- setCaptureHotkeyTypeInFocus(false);
- setIsUnfolded && setIsUnfolded(false);
- resetState && resetState();
+ onIsUnfoldedChange?.(false);
};
const dropdownRef = useRef(null);
diff --git a/front/src/modules/ui/components/table/table-header/FilterDropdownButton.tsx b/front/src/modules/ui/components/table/table-header/FilterDropdownButton.tsx
index 2762497d7..164e0277d 100644
--- a/front/src/modules/ui/components/table/table-header/FilterDropdownButton.tsx
+++ b/front/src/modules/ui/components/table/table-header/FilterDropdownButton.tsx
@@ -5,6 +5,8 @@ import { filterDropdownSearchInputScopedState } from '@/filters-and-sorts/states
import { isFilterDropdownOperandSelectUnfoldedScopedState } from '@/filters-and-sorts/states/isFilterDropdownOperandSelectUnfoldedScopedState';
import { selectedOperandInDropdownScopedState } from '@/filters-and-sorts/states/selectedOperandInDropdownScopedState';
import { tableFilterDefinitionUsedInDropdownScopedState } from '@/filters-and-sorts/states/tableFilterDefinitionUsedInDropdownScopedState';
+import { useHotkeysScopeOnBooleanState } from '@/hotkeys/hooks/useHotkeysScopeOnBooleanState';
+import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
import { TableContext } from '@/ui/tables/states/TableContext';
@@ -21,6 +23,11 @@ import { FilterDropdownTextSearchInput } from './FilterDropdownTextSearchInput';
export function FilterDropdownButton() {
const [isUnfolded, setIsUnfolded] = useState(false);
+ useHotkeysScopeOnBooleanState(
+ { scope: InternalHotkeysScope.TableHeaderDropdownButton },
+ isUnfolded,
+ );
+
const [
isFilterDropdownOperandSelectUnfolded,
setIsFilterDropdownOperandSelectUnfolded,
@@ -64,13 +71,21 @@ export function FilterDropdownButton() {
const isFilterSelected = (activeTableFilters?.length ?? 0) > 0;
+ function handleIsUnfoldedChange(newIsUnfolded: boolean) {
+ if (newIsUnfolded) {
+ setIsUnfolded(true);
+ } else {
+ setIsUnfolded(false);
+ resetState();
+ }
+ }
+
return (
{!tableFilterDefinitionUsedInDropdown ? (
diff --git a/front/src/modules/ui/components/table/table-header/SortDropdownButton.tsx b/front/src/modules/ui/components/table/table-header/SortDropdownButton.tsx
index ce0e59ca3..0d8f12b24 100644
--- a/front/src/modules/ui/components/table/table-header/SortDropdownButton.tsx
+++ b/front/src/modules/ui/components/table/table-header/SortDropdownButton.tsx
@@ -1,11 +1,9 @@
import { useCallback, useState } from 'react';
-import { useRecoilState } from 'recoil';
import {
SelectedSortType,
SortType,
} from '@/filters-and-sorts/interfaces/sorts/interface';
-import { captureHotkeyTypeInFocusState } from '@/hotkeys/states/captureHotkeyTypeInFocusState';
import DropdownButton from './DropdownButton';
@@ -23,9 +21,6 @@ export function SortDropdownButton({
onSortSelect,
}: OwnProps) {
const [isUnfolded, setIsUnfolded] = useState(false);
- const [, setCaptureHotkeyTypeInFocus] = useRecoilState(
- captureHotkeyTypeInFocusState,
- );
const [isOptionUnfolded, setIsOptionUnfolded] = useState(false);
@@ -41,17 +36,24 @@ export function SortDropdownButton({
const resetState = useCallback(() => {
setIsOptionUnfolded(false);
- setCaptureHotkeyTypeInFocus(false);
setSelectedSortDirection('asc');
- }, [setCaptureHotkeyTypeInFocus]);
+ }, []);
+
+ function handleIsUnfoldedChange(newIsUnfolded: boolean) {
+ if (newIsUnfolded) {
+ setIsUnfolded(true);
+ } else {
+ setIsUnfolded(false);
+ resetState();
+ }
+ }
return (
{isOptionUnfolded
? options.map((option, index) => (
@@ -59,7 +61,6 @@ export function SortDropdownButton({
key={index}
onClick={() => {
setSelectedSortDirection(option);
- setCaptureHotkeyTypeInFocus(false);
setIsOptionUnfolded(false);
}}
>
@@ -80,7 +81,6 @@ export function SortDropdownButton({
key={index + 1}
onClick={() => {
setIsUnfolded(false);
- setCaptureHotkeyTypeInFocus(false);
onSortItemSelect(sort);
}}
>
diff --git a/front/src/modules/ui/layout/right-drawer/components/RightDrawer.tsx b/front/src/modules/ui/layout/right-drawer/components/RightDrawer.tsx
index 78292cff5..b08461f6e 100644
--- a/front/src/modules/ui/layout/right-drawer/components/RightDrawer.tsx
+++ b/front/src/modules/ui/layout/right-drawer/components/RightDrawer.tsx
@@ -1,8 +1,8 @@
-import { useEffect } from 'react';
import styled from '@emotion/styled';
import { useRecoilState } from 'recoil';
-import { captureHotkeyTypeInFocusState } from '@/hotkeys/states/captureHotkeyTypeInFocusState';
+import { useHotkeysScopeOnBooleanState } from '@/hotkeys/hooks/useHotkeysScopeOnBooleanState';
+import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { isDefined } from '@/utils/type-guards/isDefined';
import { Panel } from '../../Panel';
@@ -18,14 +18,14 @@ const StyledRightDrawer = styled.div`
`;
export function RightDrawer() {
- const [, setCaptureHotkeyTypeInFocus] = useRecoilState(
- captureHotkeyTypeInFocusState,
- );
const [isRightDrawerOpen] = useRecoilState(isRightDrawerOpenState);
const [rightDrawerPage] = useRecoilState(rightDrawerPageState);
- useEffect(() => {
- setCaptureHotkeyTypeInFocus(isRightDrawerOpen);
- }, [isRightDrawerOpen, setCaptureHotkeyTypeInFocus]);
+
+ useHotkeysScopeOnBooleanState(
+ { scope: InternalHotkeysScope.RightDrawer },
+ isRightDrawerOpen,
+ );
+
if (!isRightDrawerOpen || !isDefined(rightDrawerPage)) {
return <>>;
}
diff --git a/front/src/modules/ui/layout/top-bar/TopBar.tsx b/front/src/modules/ui/layout/top-bar/TopBar.tsx
index 3601b19a6..6be11ef03 100644
--- a/front/src/modules/ui/layout/top-bar/TopBar.tsx
+++ b/front/src/modules/ui/layout/top-bar/TopBar.tsx
@@ -1,7 +1,8 @@
import { ReactNode } from 'react';
import styled from '@emotion/styled';
-import { useDirectHotkeys } from '@/hotkeys/hooks/useDirectHotkeys';
+import { useScopedHotkeys } from '@/hotkeys/hooks/useScopedHotkeys';
+import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { IconPlus } from '@/ui/icons/index';
import NavCollapseButton from '../navbar/NavCollapseButton';
@@ -50,7 +51,7 @@ type OwnProps = {
};
export function TopBar({ title, icon, onAddButtonClick }: OwnProps) {
- useDirectHotkeys('c', () => onAddButtonClick && onAddButtonClick());
+ useScopedHotkeys('c', () => onAddButtonClick?.(), InternalHotkeysScope.Table);
return (
<>
diff --git a/front/src/modules/ui/tables/hooks/useClearCellInEditMode.ts b/front/src/modules/ui/tables/hooks/useClearCellInEditMode.ts
new file mode 100644
index 000000000..275d37dfe
--- /dev/null
+++ b/front/src/modules/ui/tables/hooks/useClearCellInEditMode.ts
@@ -0,0 +1,21 @@
+import { useRecoilCallback } from 'recoil';
+
+import { currentCellInEditModePositionState } from '../states/currentCellInEditModePositionState';
+import { isCellInEditModeFamilyState } from '../states/isCellInEditModeFamilyState';
+import { isSomeInputInEditModeState } from '../states/isSomeInputInEditModeState';
+
+export function useCloseCurrentCellInEditMode() {
+ return useRecoilCallback(({ set, snapshot }) => {
+ return async () => {
+ const currentCellInEditModePosition = await snapshot.getPromise(
+ currentCellInEditModePositionState,
+ );
+
+ set(isCellInEditModeFamilyState(currentCellInEditModePosition), false);
+
+ await new Promise((resolve) => setTimeout(resolve, 20));
+
+ set(isSomeInputInEditModeState, false);
+ };
+ }, []);
+}
diff --git a/front/src/modules/ui/tables/hooks/useDisableSoftFocus.ts b/front/src/modules/ui/tables/hooks/useDisableSoftFocus.ts
new file mode 100644
index 000000000..45e6b4d0e
--- /dev/null
+++ b/front/src/modules/ui/tables/hooks/useDisableSoftFocus.ts
@@ -0,0 +1,19 @@
+import { useRecoilCallback } from 'recoil';
+
+import { isSoftFocusActiveState } from '../states/isSoftFocusActiveState';
+import { isSoftFocusOnCellFamilyState } from '../states/isSoftFocusOnCellFamilyState';
+import { softFocusPositionState } from '../states/softFocusPositionState';
+
+export function useDisableSoftFocus() {
+ return useRecoilCallback(({ set, snapshot }) => {
+ return () => {
+ const currentPosition = snapshot
+ .getLoadable(softFocusPositionState)
+ .valueOrThrow();
+
+ set(isSoftFocusActiveState, false);
+
+ set(isSoftFocusOnCellFamilyState(currentPosition), false);
+ };
+ }, []);
+}
diff --git a/front/src/modules/ui/tables/hooks/useInitializeEntityTable.ts b/front/src/modules/ui/tables/hooks/useInitializeEntityTable.ts
index 11ee8e774..c9445e220 100644
--- a/front/src/modules/ui/tables/hooks/useInitializeEntityTable.ts
+++ b/front/src/modules/ui/tables/hooks/useInitializeEntityTable.ts
@@ -1,11 +1,9 @@
import { useEffect } from 'react';
import { useRecoilState } from 'recoil';
-import { TABLE_MIN_COLUMN_NUMBER_BECAUSE_OF_CHECKBOX_COLUMN } from '../constants';
import { entityTableDimensionsState } from '../states/entityTableDimensionsState';
import { useResetTableRowSelection } from './useResetTableRowSelection';
-import { useSetSoftFocusPosition } from './useSetSoftFocusPosition';
export type TableDimensions = {
numberOfRows: number;
@@ -30,13 +28,4 @@ export function useInitializeEntityTable({
numberOfRows,
});
}, [numberOfRows, numberOfColumns, setTableDimensions]);
-
- const setSoftFocusPosition = useSetSoftFocusPosition();
-
- useEffect(() => {
- setSoftFocusPosition({
- row: 0,
- column: TABLE_MIN_COLUMN_NUMBER_BECAUSE_OF_CHECKBOX_COLUMN,
- });
- }, [setSoftFocusPosition]);
}
diff --git a/front/src/modules/ui/tables/hooks/useLeaveTableFocus.ts b/front/src/modules/ui/tables/hooks/useLeaveTableFocus.ts
new file mode 100644
index 000000000..abc58fb9a
--- /dev/null
+++ b/front/src/modules/ui/tables/hooks/useLeaveTableFocus.ts
@@ -0,0 +1,24 @@
+import { useCurrentHotkeysScope } from '@/hotkeys/hooks/useCurrentHotkeysScope';
+import { useResetHotkeysScopeStack } from '@/hotkeys/hooks/useResetHotkeysScopeStack';
+import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
+
+import { useCloseCurrentCellInEditMode } from './useClearCellInEditMode';
+import { useDisableSoftFocus } from './useDisableSoftFocus';
+
+export function useLeaveTableFocus() {
+ const resetHotkeysScopeStack = useResetHotkeysScopeStack();
+ const currentHotkeysScope = useCurrentHotkeysScope();
+
+ const disableSoftFocus = useDisableSoftFocus();
+ const closeCurrentCellInEditMode = useCloseCurrentCellInEditMode();
+
+ return async function leaveTableFocus() {
+ if (currentHotkeysScope?.scope === InternalHotkeysScope.Table) {
+ return;
+ }
+
+ closeCurrentCellInEditMode();
+ disableSoftFocus();
+ resetHotkeysScopeStack(InternalHotkeysScope.Table);
+ };
+}
diff --git a/front/src/modules/ui/tables/hooks/useMapKeyboardToSoftFocus.ts b/front/src/modules/ui/tables/hooks/useMapKeyboardToSoftFocus.ts
index 8177d54a7..28d538c83 100644
--- a/front/src/modules/ui/tables/hooks/useMapKeyboardToSoftFocus.ts
+++ b/front/src/modules/ui/tables/hooks/useMapKeyboardToSoftFocus.ts
@@ -1,72 +1,74 @@
-import { useHotkeys } from 'react-hotkeys-hook';
import { useRecoilState } from 'recoil';
+import { Key } from 'ts-key-enum';
+
+import { useRemoveFromHotkeysScopeStack } from '@/hotkeys/hooks/useRemoveFromHotkeysScopeStack';
+import { useScopedHotkeys } from '@/hotkeys/hooks/useScopedHotkeys';
+import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { isSomeInputInEditModeState } from '../states/isSomeInputInEditModeState';
+import { useDisableSoftFocus } from './useDisableSoftFocus';
import { useMoveSoftFocus } from './useMoveSoftFocus';
export function useMapKeyboardToSoftFocus() {
const { moveDown, moveLeft, moveRight, moveUp } = useMoveSoftFocus();
+ const removeFromHotkeysScopedStack = useRemoveFromHotkeysScopeStack();
+ const disableSoftFocus = useDisableSoftFocus();
+
const [isSomeInputInEditMode] = useRecoilState(isSomeInputInEditModeState);
- useHotkeys(
- 'up, shift+enter',
+ useScopedHotkeys(
+ [Key.ArrowUp, `${Key.Shift}+${Key.Enter}`],
() => {
if (!isSomeInputInEditMode) {
moveUp();
}
},
+ InternalHotkeysScope.TableSoftFocus,
[moveUp, isSomeInputInEditMode],
- {
- preventDefault: true,
- enableOnContentEditable: true,
- enableOnFormTags: true,
- },
);
- useHotkeys(
- 'down',
+ useScopedHotkeys(
+ Key.ArrowDown,
() => {
if (!isSomeInputInEditMode) {
moveDown();
}
},
+ InternalHotkeysScope.TableSoftFocus,
[moveDown, isSomeInputInEditMode],
- {
- preventDefault: true,
- enableOnContentEditable: true,
- enableOnFormTags: true,
- },
);
- useHotkeys(
- ['left', 'shift+tab'],
+ useScopedHotkeys(
+ [Key.ArrowLeft, `${Key.Shift}+${Key.Tab}`],
() => {
if (!isSomeInputInEditMode) {
moveLeft();
}
},
+ InternalHotkeysScope.TableSoftFocus,
[moveLeft, isSomeInputInEditMode],
- {
- preventDefault: true,
- enableOnContentEditable: true,
- enableOnFormTags: true,
- },
);
- useHotkeys(
- ['right', 'tab'],
+ useScopedHotkeys(
+ [Key.ArrowRight, Key.Tab],
() => {
if (!isSomeInputInEditMode) {
moveRight();
}
},
+ InternalHotkeysScope.TableSoftFocus,
[moveRight, isSomeInputInEditMode],
- {
- preventDefault: true,
- enableOnContentEditable: true,
- enableOnFormTags: true,
+ );
+
+ useScopedHotkeys(
+ [Key.Escape],
+ () => {
+ removeFromHotkeysScopedStack(InternalHotkeysScope.TableSoftFocus);
+ disableSoftFocus();
},
+ InternalHotkeysScope.TableSoftFocus,
+ [removeFromHotkeysScopedStack, disableSoftFocus],
);
}
diff --git a/front/src/modules/ui/tables/hooks/useMoveEditModeToCellPosition.ts b/front/src/modules/ui/tables/hooks/useMoveEditModeToCellPosition.ts
new file mode 100644
index 000000000..ab597615b
--- /dev/null
+++ b/front/src/modules/ui/tables/hooks/useMoveEditModeToCellPosition.ts
@@ -0,0 +1,21 @@
+import { useRecoilCallback } from 'recoil';
+
+import { currentCellInEditModePositionState } from '../states/currentCellInEditModePositionState';
+import { isCellInEditModeFamilyState } from '../states/isCellInEditModeFamilyState';
+import { CellPosition } from '../types/CellPosition';
+
+export function useMoveEditModeToCellPosition() {
+ return useRecoilCallback(({ set, snapshot }) => {
+ return (newPosition: CellPosition) => {
+ const currentCellInEditModePosition = snapshot
+ .getLoadable(currentCellInEditModePositionState)
+ .valueOrThrow();
+
+ set(isCellInEditModeFamilyState(currentCellInEditModePosition), false);
+
+ set(currentCellInEditModePositionState, newPosition);
+
+ set(isCellInEditModeFamilyState(newPosition), true);
+ };
+ }, []);
+}
diff --git a/front/src/modules/ui/tables/hooks/useSetSoftFocusPosition.ts b/front/src/modules/ui/tables/hooks/useSetSoftFocusPosition.ts
index 97b8926a9..a15bb394a 100644
--- a/front/src/modules/ui/tables/hooks/useSetSoftFocusPosition.ts
+++ b/front/src/modules/ui/tables/hooks/useSetSoftFocusPosition.ts
@@ -1,16 +1,19 @@
import { useRecoilCallback } from 'recoil';
+import { isSoftFocusActiveState } from '../states/isSoftFocusActiveState';
import { isSoftFocusOnCellFamilyState } from '../states/isSoftFocusOnCellFamilyState';
import { softFocusPositionState } from '../states/softFocusPositionState';
-import { TablePosition } from '../types/TablePosition';
+import { CellPosition } from '../types/CellPosition';
export function useSetSoftFocusPosition() {
return useRecoilCallback(({ set, snapshot }) => {
- return (newPosition: TablePosition) => {
+ return (newPosition: CellPosition) => {
const currentPosition = snapshot
.getLoadable(softFocusPositionState)
.valueOrThrow();
+ set(isSoftFocusActiveState, true);
+
set(isSoftFocusOnCellFamilyState(currentPosition), false);
set(softFocusPositionState, newPosition);
diff --git a/front/src/modules/ui/tables/states/currentCellInEditModePositionState.ts b/front/src/modules/ui/tables/states/currentCellInEditModePositionState.ts
new file mode 100644
index 000000000..ca3e04999
--- /dev/null
+++ b/front/src/modules/ui/tables/states/currentCellInEditModePositionState.ts
@@ -0,0 +1,11 @@
+import { atom } from 'recoil';
+
+import { CellPosition } from '../types/CellPosition';
+
+export const currentCellInEditModePositionState = atom({
+ key: 'currentCellInEditModePositionState',
+ default: {
+ row: 0,
+ column: 1,
+ },
+});
diff --git a/front/src/modules/ui/tables/states/isCellInEditModeFamilyState.ts b/front/src/modules/ui/tables/states/isCellInEditModeFamilyState.ts
new file mode 100644
index 000000000..d84b20f34
--- /dev/null
+++ b/front/src/modules/ui/tables/states/isCellInEditModeFamilyState.ts
@@ -0,0 +1,8 @@
+import { atomFamily } from 'recoil';
+
+import { CellPosition } from '../types/CellPosition';
+
+export const isCellInEditModeFamilyState = atomFamily({
+ key: 'isCellInEditModeFamilyState',
+ default: false,
+});
diff --git a/front/src/modules/ui/tables/states/isSoftFocusActiveState.ts b/front/src/modules/ui/tables/states/isSoftFocusActiveState.ts
new file mode 100644
index 000000000..b82347899
--- /dev/null
+++ b/front/src/modules/ui/tables/states/isSoftFocusActiveState.ts
@@ -0,0 +1,6 @@
+import { atom } from 'recoil';
+
+export const isSoftFocusActiveState = atom({
+ key: 'isSoftFocusActiveState',
+ default: false,
+});
diff --git a/front/src/modules/ui/tables/states/isSoftFocusOnCellFamilyState.ts b/front/src/modules/ui/tables/states/isSoftFocusOnCellFamilyState.ts
index 972514805..1db20a4c3 100644
--- a/front/src/modules/ui/tables/states/isSoftFocusOnCellFamilyState.ts
+++ b/front/src/modules/ui/tables/states/isSoftFocusOnCellFamilyState.ts
@@ -1,8 +1,8 @@
import { atomFamily } from 'recoil';
-import { TablePosition } from '../types/TablePosition';
+import { CellPosition } from '../types/CellPosition';
-export const isSoftFocusOnCellFamilyState = atomFamily({
+export const isSoftFocusOnCellFamilyState = atomFamily({
key: 'isSoftFocusOnCellFamilyState',
default: false,
});
diff --git a/front/src/modules/ui/tables/states/softFocusPositionState.ts b/front/src/modules/ui/tables/states/softFocusPositionState.ts
index f1b95f84c..e4045918a 100644
--- a/front/src/modules/ui/tables/states/softFocusPositionState.ts
+++ b/front/src/modules/ui/tables/states/softFocusPositionState.ts
@@ -1,8 +1,8 @@
import { atom } from 'recoil';
-import { TablePosition } from '../types/TablePosition';
+import { CellPosition } from '../types/CellPosition';
-export const softFocusPositionState = atom({
+export const softFocusPositionState = atom({
key: 'softFocusPositionState',
default: {
row: 0,
diff --git a/front/src/modules/ui/tables/types/TablePosition.ts b/front/src/modules/ui/tables/types/CellPosition.ts
similarity index 54%
rename from front/src/modules/ui/tables/types/TablePosition.ts
rename to front/src/modules/ui/tables/types/CellPosition.ts
index 21c4829c4..ed2cbca42 100644
--- a/front/src/modules/ui/tables/types/TablePosition.ts
+++ b/front/src/modules/ui/tables/types/CellPosition.ts
@@ -1,4 +1,4 @@
-export type TablePosition = {
+export type CellPosition = {
row: number;
column: number;
};
diff --git a/front/src/modules/ui/tables/types/guards/isTablePosition.ts b/front/src/modules/ui/tables/types/guards/isTablePosition.ts
index 381494f0f..363d1c626 100644
--- a/front/src/modules/ui/tables/types/guards/isTablePosition.ts
+++ b/front/src/modules/ui/tables/types/guards/isTablePosition.ts
@@ -1,6 +1,6 @@
-import { TablePosition } from '../TablePosition';
+import { CellPosition } from '../CellPosition';
-export function isTablePosition(value: any): value is TablePosition {
+export function isTablePosition(value: any): value is CellPosition {
return (
value && typeof value.row === 'number' && typeof value.column === 'number'
);
diff --git a/front/src/modules/ui/types/PositionType.tsx b/front/src/modules/ui/types/PositionType.ts
similarity index 54%
rename from front/src/modules/ui/types/PositionType.tsx
rename to front/src/modules/ui/types/PositionType.ts
index 46bc3c540..4754eff68 100644
--- a/front/src/modules/ui/types/PositionType.tsx
+++ b/front/src/modules/ui/types/PositionType.ts
@@ -1,4 +1,4 @@
-export interface PositionType {
+export type PositionType = {
x: number | null;
y: number | null;
-}
+};
diff --git a/front/src/pages/auth/CreateProfile.tsx b/front/src/pages/auth/CreateProfile.tsx
index 3c310e4c8..faef8b7d7 100644
--- a/front/src/pages/auth/CreateProfile.tsx
+++ b/front/src/pages/auth/CreateProfile.tsx
@@ -1,16 +1,17 @@
import { useCallback, useEffect, useState } from 'react';
-import { useHotkeys } from 'react-hotkeys-hook';
import { useNavigate } from 'react-router-dom';
import { getOperationName } from '@apollo/client/utilities';
import styled from '@emotion/styled';
import { useRecoilState } from 'recoil';
+import { Key } from 'ts-key-enum';
import { SubTitle } from '@/auth/components/ui/SubTitle';
import { Title } from '@/auth/components/ui/Title';
import { useOnboardingStatus } from '@/auth/hooks/useOnboardingStatus';
import { currentUserState } from '@/auth/states/currentUserState';
import { OnboardingStatus } from '@/auth/utils/getOnboardingStatus';
-import { captureHotkeyTypeInFocusState } from '@/hotkeys/states/captureHotkeyTypeInFocusState';
+import { useScopedHotkeys } from '@/hotkeys/hooks/useScopedHotkeys';
+import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { MainButton } from '@/ui/components/buttons/MainButton';
import { ImageInput } from '@/ui/components/inputs/ImageInput';
import { TextInput } from '@/ui/components/inputs/TextInput';
@@ -48,9 +49,6 @@ export function CreateProfile() {
const onboardingStatus = useOnboardingStatus();
const [currentUser] = useRecoilState(currentUserState);
- const [, setCaptureHotkeyTypeInFocus] = useRecoilState(
- captureHotkeyTypeInFocusState,
- );
const [updateUser] = useUpdateUserMutation();
@@ -85,29 +83,18 @@ export function CreateProfile() {
throw errors;
}
- setCaptureHotkeyTypeInFocus(false);
navigate('/');
} catch (error) {
console.error(error);
}
- }, [
- currentUser?.id,
- firstName,
- lastName,
- navigate,
- setCaptureHotkeyTypeInFocus,
- updateUser,
- ]);
+ }, [currentUser?.id, firstName, lastName, navigate, updateUser]);
- useHotkeys(
- 'enter',
+ useScopedHotkeys(
+ Key.Enter,
() => {
handleCreate();
},
- {
- enableOnContentEditable: true,
- enableOnFormTags: true,
- },
+ InternalHotkeysScope.Modal,
[handleCreate],
);
@@ -117,10 +104,6 @@ export function CreateProfile() {
}
}, [onboardingStatus, navigate]);
- useEffect(() => {
- setCaptureHotkeyTypeInFocus(true);
- }, [setCaptureHotkeyTypeInFocus]);
-
return (
<>
Create profile
diff --git a/front/src/pages/auth/CreateWorkspace.tsx b/front/src/pages/auth/CreateWorkspace.tsx
index 76a4a7f9c..1b3ab9e1b 100644
--- a/front/src/pages/auth/CreateWorkspace.tsx
+++ b/front/src/pages/auth/CreateWorkspace.tsx
@@ -1,5 +1,4 @@
import { useCallback, useEffect, useState } from 'react';
-import { useHotkeys } from 'react-hotkeys-hook';
import { useNavigate } from 'react-router-dom';
import { getOperationName } from '@apollo/client/utilities';
import styled from '@emotion/styled';
@@ -8,6 +7,8 @@ import { SubTitle } from '@/auth/components/ui/SubTitle';
import { Title } from '@/auth/components/ui/Title';
import { useOnboardingStatus } from '@/auth/hooks/useOnboardingStatus';
import { OnboardingStatus } from '@/auth/utils/getOnboardingStatus';
+import { useScopedHotkeys } from '@/hotkeys/hooks/useScopedHotkeys';
+import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { MainButton } from '@/ui/components/buttons/MainButton';
import { ImageInput } from '@/ui/components/inputs/ImageInput';
import { TextInput } from '@/ui/components/inputs/TextInput';
@@ -68,15 +69,12 @@ export function CreateWorkspace() {
}
}, [navigate, updateWorkspace, workspaceName]);
- useHotkeys(
+ useScopedHotkeys(
'enter',
() => {
handleCreate();
},
- {
- enableOnContentEditable: true,
- enableOnFormTags: true,
- },
+ InternalHotkeysScope.Modal,
[handleCreate],
);
diff --git a/front/src/pages/auth/Index.tsx b/front/src/pages/auth/Index.tsx
index bf14d2fe5..895c8a546 100644
--- a/front/src/pages/auth/Index.tsx
+++ b/front/src/pages/auth/Index.tsx
@@ -1,5 +1,4 @@
import { useCallback, useEffect, useState } from 'react';
-import { useHotkeys } from 'react-hotkeys-hook';
import { useNavigate } from 'react-router-dom';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
@@ -13,7 +12,8 @@ import { Title } from '@/auth/components/ui/Title';
import { authFlowUserEmailState } from '@/auth/states/authFlowUserEmailState';
import { isMockModeState } from '@/auth/states/isMockModeState';
import { authProvidersState } from '@/client-config/states/authProvidersState';
-import { captureHotkeyTypeInFocusState } from '@/hotkeys/states/captureHotkeyTypeInFocusState';
+import { useScopedHotkeys } from '@/hotkeys/hooks/useScopedHotkeys';
+import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { MainButton } from '@/ui/components/buttons/MainButton';
import { TextInput } from '@/ui/components/inputs/TextInput';
import { AnimatedEaseIn } from '@/ui/components/motion/AnimatedEaseIn';
@@ -31,9 +31,6 @@ const StyledFooterNote = styled(FooterNote)`
`;
export function Index() {
- const [, setCaptureHotkeyTypeInFocus] = useRecoilState(
- captureHotkeyTypeInFocusState,
- );
const navigate = useNavigate();
const theme = useTheme();
const [, setMockMode] = useRecoilState(isMockModeState);
@@ -59,29 +56,19 @@ export function Index() {
navigate('/auth/password-login');
}, [navigate, visible]);
- useHotkeys(
+ useScopedHotkeys(
'enter',
() => {
onPasswordLoginClick();
},
- {
- enableOnContentEditable: true,
- enableOnFormTags: true,
- },
+ InternalHotkeysScope.Modal,
[onPasswordLoginClick],
);
useEffect(() => {
setMockMode(true);
- setCaptureHotkeyTypeInFocus(true);
setAuthFlowUserEmail(demoMode ? 'tim@apple.dev' : '');
- }, [
- navigate,
- setMockMode,
- setCaptureHotkeyTypeInFocus,
- setAuthFlowUserEmail,
- demoMode,
- ]);
+ }, [navigate, setMockMode, setAuthFlowUserEmail, demoMode]);
return (
<>
diff --git a/front/src/pages/auth/PasswordLogin.tsx b/front/src/pages/auth/PasswordLogin.tsx
index 04ca70693..3040c19d3 100644
--- a/front/src/pages/auth/PasswordLogin.tsx
+++ b/front/src/pages/auth/PasswordLogin.tsx
@@ -1,5 +1,4 @@
import { useCallback, useState } from 'react';
-import { useHotkeys } from 'react-hotkeys-hook';
import { useNavigate } from 'react-router-dom';
import styled from '@emotion/styled';
import { motion } from 'framer-motion';
@@ -12,6 +11,8 @@ import { useAuth } from '@/auth/hooks/useAuth';
import { authFlowUserEmailState } from '@/auth/states/authFlowUserEmailState';
import { isMockModeState } from '@/auth/states/isMockModeState';
import { isDemoModeState } from '@/client-config/states/isDemoModeState';
+import { useScopedHotkeys } from '@/hotkeys/hooks/useScopedHotkeys';
+import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { MainButton } from '@/ui/components/buttons/MainButton';
import { TextInput } from '@/ui/components/inputs/TextInput';
import { SubSectionTitle } from '@/ui/components/section-titles/SubSectionTitle';
@@ -74,15 +75,12 @@ export function PasswordLogin() {
}
}, [login, authFlowUserEmail, internalPassword, setMockMode, navigate]);
- useHotkeys(
+ useScopedHotkeys(
'enter',
() => {
handleLogin();
},
- {
- enableOnContentEditable: true,
- enableOnFormTags: true,
- },
+ InternalHotkeysScope.Modal,
[handleLogin],
);
diff --git a/front/src/pages/companies/Companies.tsx b/front/src/pages/companies/Companies.tsx
index 755e954d4..d9ffc16b6 100644
--- a/front/src/pages/companies/Companies.tsx
+++ b/front/src/pages/companies/Companies.tsx
@@ -1,9 +1,13 @@
import { getOperationName } from '@apollo/client/utilities';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
+import { useRecoilState } from 'recoil';
import { v4 as uuidv4 } from 'uuid';
+import { isMockModeState } from '@/auth/states/isMockModeState';
import { GET_COMPANIES } from '@/companies/services';
+import { useHotkeysScopeOnMountOnly } from '@/hotkeys/hooks/useHotkeysScopeOnMountOnly';
+import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { RecoilScope } from '@/recoil-scope/components/RecoilScope';
import { EntityTableActionBar } from '@/ui/components/table/action-bar/EntityTableActionBar';
import { IconBuildingSkyscraper } from '@/ui/icons/index';
@@ -24,6 +28,18 @@ const StyledTableContainer = styled.div`
`;
export function Companies() {
+ const [isMockMode] = useRecoilState(isMockModeState);
+
+ const hotkeysEnabled = !isMockMode;
+
+ useHotkeysScopeOnMountOnly(
+ {
+ scope: InternalHotkeysScope.Table,
+ customScopes: { 'command-menu': true, goto: true },
+ },
+ hotkeysEnabled,
+ );
+
const [insertCompany] = useInsertCompanyMutation();
async function handleAddButtonClick() {
@@ -45,20 +61,22 @@ export function Companies() {
const theme = useTheme();
return (
- }
- onAddButtonClick={handleAddButtonClick}
- >
-
-
-
-
-
-
-
-
-
-
+ <>
+ }
+ onAddButtonClick={handleAddButtonClick}
+ >
+
+
+
+
+
+
+
+
+
+
+ >
);
}
diff --git a/front/src/pages/people/People.tsx b/front/src/pages/people/People.tsx
index 708fc9c3e..b79cb6142 100644
--- a/front/src/pages/people/People.tsx
+++ b/front/src/pages/people/People.tsx
@@ -3,6 +3,8 @@ import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { v4 as uuidv4 } from 'uuid';
+import { useHotkeysScopeOnMountOnly } from '@/hotkeys/hooks/useHotkeysScopeOnMountOnly';
+import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { GET_PEOPLE } from '@/people/services';
import { RecoilScope } from '@/recoil-scope/components/RecoilScope';
import { EntityTableActionBar } from '@/ui/components/table/action-bar/EntityTableActionBar';
@@ -22,6 +24,11 @@ const StyledPeopleContainer = styled.div`
`;
export function People() {
+ useHotkeysScopeOnMountOnly({
+ scope: InternalHotkeysScope.Table,
+ customScopes: { 'command-menu': true, goto: true },
+ });
+
const [insertPersonMutation] = useInsertPersonMutation();
async function handleAddButtonClick() {
diff --git a/front/src/sync-hooks/AnalyticsHook.tsx b/front/src/sync-hooks/AnalyticsHook.tsx
new file mode 100644
index 000000000..3f3088815
--- /dev/null
+++ b/front/src/sync-hooks/AnalyticsHook.tsx
@@ -0,0 +1,7 @@
+import { useTrackPageView } from '@/analytics/hooks/useTrackPageView';
+
+export function AnalyticsHook() {
+ useTrackPageView();
+
+ return <>>;
+}
diff --git a/front/src/sync-hooks/GotoHotkeysHooks.tsx b/front/src/sync-hooks/GotoHotkeysHooks.tsx
new file mode 100644
index 000000000..b7d8c956f
--- /dev/null
+++ b/front/src/sync-hooks/GotoHotkeysHooks.tsx
@@ -0,0 +1,10 @@
+import { useGoToHotkeys } from '@/hotkeys/hooks/useGoToHotkeys';
+
+export function GotoHotkeysHooks() {
+ useGoToHotkeys('p', '/people');
+ useGoToHotkeys('c', '/companies');
+ useGoToHotkeys('o', '/opportunities');
+ useGoToHotkeys('s', '/settings/profile');
+
+ return <>>;
+}
diff --git a/front/src/sync-hooks/HotkeysScopeStackAutoSyncHook.tsx b/front/src/sync-hooks/HotkeysScopeStackAutoSyncHook.tsx
new file mode 100644
index 000000000..95135b259
--- /dev/null
+++ b/front/src/sync-hooks/HotkeysScopeStackAutoSyncHook.tsx
@@ -0,0 +1,7 @@
+import { useHotkeysScopeStackAutoSync } from '@/hotkeys/hooks/internal/useHotkeysScopeStackAutoSync';
+
+export function HotkeysScopeStackAutoSyncHook() {
+ useHotkeysScopeStackAutoSync();
+
+ return <>>;
+}
diff --git a/front/yarn.lock b/front/yarn.lock
index d30e9f8d6..3107a9f42 100644
--- a/front/yarn.lock
+++ b/front/yarn.lock
@@ -16413,6 +16413,11 @@ ts-jest@^29.1.0:
semver "^7.5.3"
yargs-parser "^21.0.1"
+ts-key-enum@^2.0.12:
+ version "2.0.12"
+ resolved "https://registry.yarnpkg.com/ts-key-enum/-/ts-key-enum-2.0.12.tgz#4f7f35eb041fa5847f8f9ed8c38beaaa047a33ba"
+ integrity sha512-Ety4IvKMaeG34AyXMp5r11XiVZNDRL+XWxXbVVJjLvq2vxKRttEANBE7Za1bxCAZRdH2/sZT6jFyyTWxXz28hw==
+
ts-log@^2.2.3:
version "2.2.5"
resolved "https://registry.yarnpkg.com/ts-log/-/ts-log-2.2.5.tgz#aef3252f1143d11047e2cb6f7cfaac7408d96623"