From 66dcc9b2e14f9bc10bdf3b89d508f9753d49f817 Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Sat, 8 Jul 2023 03:53:05 +0200 Subject: [PATCH] Feat/better hotkeys scope (#526) * Working version * fix * Fixed console log * Fix lint * wip * Fix * Fix * consolelog --------- Co-authored-by: Charles Bochet --- front/package.json | 1 + front/src/App.tsx | 84 ++++++------ front/src/AppInternalHooks.tsx | 13 ++ front/src/index.tsx | 12 +- .../auth/components/RequireOnboarded.tsx | 12 -- .../src/modules/auth/components/ui/Modal.tsx | 7 + .../command-menu/components/CommandMenu.tsx | 13 +- .../CommentThreadRelationPicker.tsx | 45 +++--- front/src/modules/hotkeys/constants/index.ts | 17 +++ .../hotkeys/hooks/internal/useHotkeysScope.ts | 103 ++++++++++++++ .../internal/useHotkeysScopeStackAutoSync.ts | 37 +++++ .../hooks/useAddToHotkeysScopeStack.ts | 48 +++++++ .../hotkeys/hooks/useCurrentHotkeysScope.ts | 16 +++ .../modules/hotkeys/hooks/useGoToHotkeys.ts | 18 ++- .../hooks/useHotkeysScopeOnBooleanState.ts | 27 ++++ .../hooks/useHotkeysScopeOnMountOnly.ts | 32 +++++ .../hooks/useRemoveFromHotkeysScopeStack.ts | 54 ++++++++ .../useRemoveHighestHotkeysScopeStackItem.ts | 42 ++++++ .../hooks/useResetHotkeysScopeStack.ts | 18 +++ ...seDirectHotkeys.ts => useScopedHotkeys.ts} | 21 ++- ...Hotkeys.ts => useSequenceScopedHotkeys.ts} | 17 ++- .../modules/hotkeys/hooks/useUpDownHotkeys.ts | 31 ----- .../states/captureHotkeyTypeInFocusState.ts | 6 - .../internal/customHotkeysScopesState.ts | 16 +++ .../states/internal/hotkeysScopeStackState.ts | 9 ++ .../internalHotkeysEnabledScopesState.ts | 6 + .../states/internal/pendingHotkeysState.ts | 7 + .../hotkeys/states/pendingHotkeysState.ts | 6 - .../src/modules/hotkeys/types/HotkeysScope.ts | 3 + .../types/internal/HotkeysScopeStackItems.ts | 7 + .../types/internal/InternalHotkeysScope.ts | 14 ++ .../people/components/PeopleCompanyCell.tsx | 4 + .../people/components/PeopleCompanyPicker.tsx | 11 ++ .../components/SingleEntitySelectBase.tsx | 11 +- .../hooks/useEntitySelectScroll.ts | 14 +- .../components/editable-cell/EditableCell.tsx | 45 ++++-- .../editable-cell/EditableCellEditMode.tsx | 38 ++---- .../EditableCellSoftFocusMode.tsx | 41 +++--- .../hooks/useCloseEditableCell.ts | 29 ++-- .../hooks/useCurrentCellEditMode.ts | 23 ++++ .../hooks/useCurrentCellPosition.ts | 30 ++++ .../hooks/useIsSoftFocusOnCurrentCell.ts | 29 +--- .../hooks/useSetSoftFocusOnCurrentCell.ts | 23 +++- .../states/isEditModeScopedState.ts | 6 - .../types/EditableDoubleText.tsx | 57 ++------ .../types/EditableDoubleTextEditMode.tsx | 129 ++++++++++++++++++ .../ui/components/table/EntityTable.tsx | 12 +- .../table/table-header/DropdownButton.tsx | 26 ++-- .../table-header/FilterDropdownButton.tsx | 19 ++- .../table/table-header/SortDropdownButton.tsx | 22 +-- .../right-drawer/components/RightDrawer.tsx | 16 +-- .../src/modules/ui/layout/top-bar/TopBar.tsx | 5 +- .../ui/tables/hooks/useClearCellInEditMode.ts | 21 +++ .../ui/tables/hooks/useDisableSoftFocus.ts | 19 +++ .../tables/hooks/useInitializeEntityTable.ts | 11 -- .../ui/tables/hooks/useLeaveTableFocus.ts | 24 ++++ .../tables/hooks/useMapKeyboardToSoftFocus.ts | 58 ++++---- .../hooks/useMoveEditModeToCellPosition.ts | 21 +++ .../tables/hooks/useSetSoftFocusPosition.ts | 7 +- .../currentCellInEditModePositionState.ts | 11 ++ .../states/isCellInEditModeFamilyState.ts | 8 ++ .../tables/states/isSoftFocusActiveState.ts | 6 + .../states/isSoftFocusOnCellFamilyState.ts | 4 +- .../tables/states/softFocusPositionState.ts | 4 +- .../{TablePosition.ts => CellPosition.ts} | 2 +- .../ui/tables/types/guards/isTablePosition.ts | 4 +- .../{PositionType.tsx => PositionType.ts} | 4 +- front/src/pages/auth/CreateProfile.tsx | 31 +---- front/src/pages/auth/CreateWorkspace.tsx | 10 +- front/src/pages/auth/Index.tsx | 23 +--- front/src/pages/auth/PasswordLogin.tsx | 10 +- front/src/pages/companies/Companies.tsx | 48 +++++-- front/src/pages/people/People.tsx | 7 + front/src/sync-hooks/AnalyticsHook.tsx | 7 + front/src/sync-hooks/GotoHotkeysHooks.tsx | 10 ++ .../HotkeysScopeStackAutoSyncHook.tsx | 7 + front/yarn.lock | 5 + 77 files changed, 1240 insertions(+), 454 deletions(-) create mode 100644 front/src/AppInternalHooks.tsx create mode 100644 front/src/modules/hotkeys/constants/index.ts create mode 100644 front/src/modules/hotkeys/hooks/internal/useHotkeysScope.ts create mode 100644 front/src/modules/hotkeys/hooks/internal/useHotkeysScopeStackAutoSync.ts create mode 100644 front/src/modules/hotkeys/hooks/useAddToHotkeysScopeStack.ts create mode 100644 front/src/modules/hotkeys/hooks/useCurrentHotkeysScope.ts create mode 100644 front/src/modules/hotkeys/hooks/useHotkeysScopeOnBooleanState.ts create mode 100644 front/src/modules/hotkeys/hooks/useHotkeysScopeOnMountOnly.ts create mode 100644 front/src/modules/hotkeys/hooks/useRemoveFromHotkeysScopeStack.ts create mode 100644 front/src/modules/hotkeys/hooks/useRemoveHighestHotkeysScopeStackItem.ts create mode 100644 front/src/modules/hotkeys/hooks/useResetHotkeysScopeStack.ts rename front/src/modules/hotkeys/hooks/{useDirectHotkeys.ts => useScopedHotkeys.ts} (59%) rename front/src/modules/hotkeys/hooks/{useSequenceHotkeys.ts => useSequenceScopedHotkeys.ts} (53%) delete mode 100644 front/src/modules/hotkeys/hooks/useUpDownHotkeys.ts delete mode 100644 front/src/modules/hotkeys/states/captureHotkeyTypeInFocusState.ts create mode 100644 front/src/modules/hotkeys/states/internal/customHotkeysScopesState.ts create mode 100644 front/src/modules/hotkeys/states/internal/hotkeysScopeStackState.ts create mode 100644 front/src/modules/hotkeys/states/internal/internalHotkeysEnabledScopesState.ts create mode 100644 front/src/modules/hotkeys/states/internal/pendingHotkeysState.ts delete mode 100644 front/src/modules/hotkeys/states/pendingHotkeysState.ts create mode 100644 front/src/modules/hotkeys/types/HotkeysScope.ts create mode 100644 front/src/modules/hotkeys/types/internal/HotkeysScopeStackItems.ts create mode 100644 front/src/modules/hotkeys/types/internal/InternalHotkeysScope.ts create mode 100644 front/src/modules/ui/components/editable-cell/hooks/useCurrentCellEditMode.ts create mode 100644 front/src/modules/ui/components/editable-cell/hooks/useCurrentCellPosition.ts delete mode 100644 front/src/modules/ui/components/editable-cell/states/isEditModeScopedState.ts create mode 100644 front/src/modules/ui/components/editable-cell/types/EditableDoubleTextEditMode.tsx create mode 100644 front/src/modules/ui/tables/hooks/useClearCellInEditMode.ts create mode 100644 front/src/modules/ui/tables/hooks/useDisableSoftFocus.ts create mode 100644 front/src/modules/ui/tables/hooks/useLeaveTableFocus.ts create mode 100644 front/src/modules/ui/tables/hooks/useMoveEditModeToCellPosition.ts create mode 100644 front/src/modules/ui/tables/states/currentCellInEditModePositionState.ts create mode 100644 front/src/modules/ui/tables/states/isCellInEditModeFamilyState.ts create mode 100644 front/src/modules/ui/tables/states/isSoftFocusActiveState.ts rename front/src/modules/ui/tables/types/{TablePosition.ts => CellPosition.ts} (54%) rename front/src/modules/ui/types/{PositionType.tsx => PositionType.ts} (54%) create mode 100644 front/src/sync-hooks/AnalyticsHook.tsx create mode 100644 front/src/sync-hooks/GotoHotkeysHooks.tsx create mode 100644 front/src/sync-hooks/HotkeysScopeStackAutoSyncHook.tsx 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"