diff --git a/packages/twenty-front/src/hooks/useHotkeyScopeOnMount.ts b/packages/twenty-front/src/hooks/useHotkeyScopeOnMount.ts deleted file mode 100644 index a4418a67c..000000000 --- a/packages/twenty-front/src/hooks/useHotkeyScopeOnMount.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; -import { useEffect } from 'react'; - -/** - * @deprecated This hook uses useEffect - * Use event handlers and imperative code to manage hotkey scope changes. - */ -export const useHotkeyScopeOnMount = (hotkeyScope: string) => { - const { - goBackToPreviousHotkeyScope, - setHotkeyScopeAndMemorizePreviousScope, - } = usePreviousHotkeyScope(); - - useEffect(() => { - setHotkeyScopeAndMemorizePreviousScope({ - scope: hotkeyScope, - }); - return () => { - goBackToPreviousHotkeyScope(); - }; - }, [ - hotkeyScope, - setHotkeyScopeAndMemorizePreviousScope, - goBackToPreviousHotkeyScope, - ]); -}; diff --git a/packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx b/packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx index 03b656898..d4b997c97 100644 --- a/packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx +++ b/packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx @@ -1,5 +1,5 @@ import { useCallback, useMemo } from 'react'; -import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil'; +import { useRecoilCallback, useRecoilState } from 'recoil'; import { v4 } from 'uuid'; import { useUploadAttachmentFile } from '@/activities/files/hooks/useUploadAttachmentFile'; @@ -11,7 +11,6 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi import { modifyRecordFromCache } from '@/object-record/cache/utils/modifyRecordFromCache'; import { isFieldValueReadOnly } from '@/object-record/record-field/utils/isFieldValueReadOnly'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; -import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { isNonTextWritingKey } from '@/ui/utilities/hotkey/utils/isNonTextWritingKey'; import { Key } from 'ts-key-enum'; import { useDebouncedCallback } from 'use-debounce'; @@ -24,9 +23,6 @@ import { Task } from '@/activities/types/Task'; import { filterAttachmentsToRestore } from '@/activities/utils/filterAttachmentsToRestore'; import { getActivityAttachmentIdsToDelete } from '@/activities/utils/getActivityAttachmentIdsToDelete'; import { getActivityAttachmentPathsToRestore } from '@/activities/utils/getActivityAttachmentPathsToRestore'; -import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState'; -import { CommandMenuHotkeyScope } from '@/command-menu/types/CommandMenuHotkeyScope'; -import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient'; import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords'; import { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRecords'; @@ -39,6 +35,7 @@ import { BlockEditor } from '@/ui/input/editor/components/BlockEditor'; import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack'; import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById'; import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType'; +import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement'; import type { PartialBlock } from '@blocknote/core'; import '@blocknote/core/fonts/inter.css'; import '@blocknote/mantine/style.css'; @@ -304,60 +301,56 @@ export const ActivityRichTextEditor = ({ uploadFile: handleEditorBuiltInUploadFile, }); - const commandMenuPage = useRecoilValue(commandMenuPageState); - - useScopedHotkeys( - Key.Escape, - () => { + useHotkeysOnFocusedElement({ + keys: Key.Escape, + callback: () => { editor.domElement?.blur(); }, - ActivityEditorHotkeyScope.ActivityBody, - ); + focusId: activityId, + scope: ActivityEditorHotkeyScope.ActivityBody, + dependencies: [editor], + }); - useScopedHotkeys( - '*', - (keyboardEvent) => { - // TODO: remove once stacked hotkeys / focusKeys are in place - if (commandMenuPage !== CommandMenuPages.EditRichText) { - return; - } + const handleAllKeys = (keyboardEvent: KeyboardEvent) => { + if (keyboardEvent.key === Key.Escape) { + return; + } - if (keyboardEvent.key === Key.Escape) { - return; - } + const isWritingText = + !isNonTextWritingKey(keyboardEvent.key) && + !keyboardEvent.ctrlKey && + !keyboardEvent.metaKey; - const isWritingText = - !isNonTextWritingKey(keyboardEvent.key) && - !keyboardEvent.ctrlKey && - !keyboardEvent.metaKey; + if (!isWritingText) { + return; + } - if (!isWritingText) { - return; - } + keyboardEvent.preventDefault(); + keyboardEvent.stopPropagation(); + keyboardEvent.stopImmediatePropagation(); - keyboardEvent.preventDefault(); - keyboardEvent.stopPropagation(); - keyboardEvent.stopImmediatePropagation(); + const newBlockId = v4(); + const newBlock = { + id: newBlockId, + type: 'paragraph' as const, + content: keyboardEvent.key, + }; - const newBlockId = v4(); - const newBlock = { - id: newBlockId, - type: 'paragraph' as const, - content: keyboardEvent.key, - }; + const lastBlock = editor.document[editor.document.length - 1]; + editor.insertBlocks([newBlock], lastBlock); - const lastBlock = editor.document[editor.document.length - 1]; - editor.insertBlocks([newBlock], lastBlock); + editor.setTextCursorPosition(newBlockId, 'end'); + editor.focus(); + }; + + useHotkeysOnFocusedElement({ + keys: '*', + callback: handleAllKeys, + focusId: activityId, + scope: ActivityEditorHotkeyScope.ActivityBody, + dependencies: [handleAllKeys], + }); - editor.setTextCursorPosition(newBlockId, 'end'); - editor.focus(); - }, - CommandMenuHotkeyScope.CommandMenuFocused, - [], - { - preventDefault: false, - }, - ); const { labelIdentifierFieldMetadataItem } = useRecordShowContainerData({ objectNameSingular: activityObjectNameSingular, objectRecordId: activityId, diff --git a/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx b/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx index 522f27689..a876b3bd2 100644 --- a/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx +++ b/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx @@ -31,8 +31,10 @@ import { useFocusedRecordTableRow } from '@/object-record/record-table/hooks/use import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId'; import { AppBasePath } from '@/types/AppBasePath'; import { AppPath } from '@/types/AppPath'; +import { PageFocusId } from '@/types/PageFocusId'; import { PageHotkeyScope } from '@/types/PageHotkeyScope'; -import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; +import { useResetFocusStackToFocusItem } from '@/ui/utilities/focus/hooks/useResetFocusStackToFocusItem'; +import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { isDefined } from 'twenty-shared/utils'; import { AnalyticsType } from '~/generated/graphql'; @@ -48,8 +50,6 @@ export const PageChangeEffect = () => { const [previousLocation, setPreviousLocation] = useState(''); - const setHotkeyScope = useSetHotkeyScope(); - const location = useLocation(); const pageChangeEffectNavigateLocation = @@ -92,6 +92,8 @@ export const PageChangeEffect = () => { const { closeCommandMenu } = useCommandMenu(); + const { resetFocusStackToFocusItem } = useResetFocusStackToFocusItem(); + const { resetFocusStackToRecordIndex } = useResetFocusStackToRecordIndex(); useEffect(() => { @@ -140,55 +142,200 @@ export const PageChangeEffect = () => { break; } case isMatchingLocation(location, AppPath.RecordShowPage): { - setHotkeyScope(PageHotkeyScope.RecordShowPage, { - goto: true, - keyboardShortcutMenu: true, - searchRecords: true, + resetFocusStackToFocusItem({ + focusStackItem: { + focusId: PageFocusId.RecordShowPage, + componentInstance: { + componentType: FocusComponentType.PAGE, + componentInstanceId: PageFocusId.RecordShowPage, + }, + globalHotkeysConfig: { + enableGlobalHotkeysWithModifiers: true, + enableGlobalHotkeysConflictingWithKeyboard: true, + }, + memoizeKey: 'global', + }, + hotkeyScope: { + scope: PageHotkeyScope.RecordShowPage, + customScopes: { + goto: true, + keyboardShortcutMenu: true, + searchRecords: true, + }, + }, }); break; } case isMatchingLocation(location, AppPath.SignInUp): { - setHotkeyScope(PageHotkeyScope.SignInUp); + resetFocusStackToFocusItem({ + focusStackItem: { + focusId: PageFocusId.SignInUp, + componentInstance: { + componentType: FocusComponentType.PAGE, + componentInstanceId: PageFocusId.SignInUp, + }, + globalHotkeysConfig: { + enableGlobalHotkeysWithModifiers: false, + enableGlobalHotkeysConflictingWithKeyboard: false, + }, + memoizeKey: 'global', + }, + hotkeyScope: { + scope: PageHotkeyScope.SignInUp, + }, + }); break; } case isMatchingLocation(location, AppPath.Invite): { - setHotkeyScope(PageHotkeyScope.SignInUp); + resetFocusStackToFocusItem({ + focusStackItem: { + focusId: PageFocusId.InviteTeam, + componentInstance: { + componentType: FocusComponentType.PAGE, + componentInstanceId: PageFocusId.InviteTeam, + }, + globalHotkeysConfig: { + enableGlobalHotkeysWithModifiers: false, + enableGlobalHotkeysConflictingWithKeyboard: false, + }, + memoizeKey: 'global', + }, + hotkeyScope: { + scope: PageHotkeyScope.InviteTeam, + }, + }); break; } case isMatchingLocation(location, AppPath.CreateProfile): { - setHotkeyScope(PageHotkeyScope.CreateProfile); + resetFocusStackToFocusItem({ + focusStackItem: { + focusId: PageFocusId.CreateProfile, + componentInstance: { + componentType: FocusComponentType.PAGE, + componentInstanceId: PageFocusId.CreateProfile, + }, + globalHotkeysConfig: { + enableGlobalHotkeysWithModifiers: false, + enableGlobalHotkeysConflictingWithKeyboard: false, + }, + memoizeKey: 'global', + }, + hotkeyScope: { + scope: PageHotkeyScope.CreateProfile, + }, + }); break; } case isMatchingLocation(location, AppPath.CreateWorkspace): { - setHotkeyScope(PageHotkeyScope.CreateWorkspace); + resetFocusStackToFocusItem({ + focusStackItem: { + focusId: PageFocusId.CreateWorkspace, + componentInstance: { + componentType: FocusComponentType.PAGE, + componentInstanceId: PageFocusId.CreateWorkspace, + }, + globalHotkeysConfig: { + enableGlobalHotkeysWithModifiers: false, + enableGlobalHotkeysConflictingWithKeyboard: false, + }, + memoizeKey: 'global', + }, + hotkeyScope: { + scope: PageHotkeyScope.CreateWorkspace, + }, + }); break; } case isMatchingLocation(location, AppPath.SyncEmails): { - setHotkeyScope(PageHotkeyScope.SyncEmail); + resetFocusStackToFocusItem({ + focusStackItem: { + focusId: PageFocusId.SyncEmail, + componentInstance: { + componentType: FocusComponentType.PAGE, + componentInstanceId: PageFocusId.SyncEmail, + }, + globalHotkeysConfig: { + enableGlobalHotkeysWithModifiers: false, + enableGlobalHotkeysConflictingWithKeyboard: false, + }, + memoizeKey: 'global', + }, + hotkeyScope: { + scope: PageHotkeyScope.SyncEmail, + }, + }); break; } case isMatchingLocation(location, AppPath.InviteTeam): { - setHotkeyScope(PageHotkeyScope.InviteTeam); + resetFocusStackToFocusItem({ + focusStackItem: { + focusId: PageFocusId.InviteTeam, + componentInstance: { + componentType: FocusComponentType.PAGE, + componentInstanceId: PageFocusId.InviteTeam, + }, + globalHotkeysConfig: { + enableGlobalHotkeysWithModifiers: false, + enableGlobalHotkeysConflictingWithKeyboard: false, + }, + memoizeKey: 'global', + }, + hotkeyScope: { + scope: PageHotkeyScope.InviteTeam, + }, + }); break; } case isMatchingLocation(location, AppPath.PlanRequired): { - setHotkeyScope(PageHotkeyScope.PlanRequired); + resetFocusStackToFocusItem({ + focusStackItem: { + focusId: PageFocusId.PlanRequired, + componentInstance: { + componentType: FocusComponentType.PAGE, + componentInstanceId: PageFocusId.PlanRequired, + }, + globalHotkeysConfig: { + enableGlobalHotkeysWithModifiers: false, + enableGlobalHotkeysConflictingWithKeyboard: false, + }, + memoizeKey: 'global', + }, + hotkeyScope: { + scope: PageHotkeyScope.PlanRequired, + }, + }); break; } case location.pathname.startsWith(AppBasePath.Settings): { - setHotkeyScope(PageHotkeyScope.Settings, { - goto: false, - keyboardShortcutMenu: false, - commandMenu: false, - commandMenuOpen: false, - searchRecords: false, + resetFocusStackToFocusItem({ + focusStackItem: { + focusId: PageFocusId.Settings, + componentInstance: { + componentType: FocusComponentType.PAGE, + componentInstanceId: PageFocusId.Settings, + }, + globalHotkeysConfig: { + enableGlobalHotkeysWithModifiers: false, + enableGlobalHotkeysConflictingWithKeyboard: false, + }, + memoizeKey: 'global', + }, + hotkeyScope: { + scope: PageHotkeyScope.Settings, + customScopes: { + goto: false, + keyboardShortcutMenu: false, + commandMenu: false, + commandMenuOpen: false, + searchRecords: false, + }, + }, }); break; } } }, [ location, - setHotkeyScope, previousLocation, contextStoreCurrentViewType, resetTableSelections, @@ -198,6 +345,7 @@ export const PageChangeEffect = () => { deactivateBoardCard, unfocusBoardCard, resetFocusStackToRecordIndex, + resetFocusStackToFocusItem, ]); useEffect(() => { diff --git a/packages/twenty-front/src/modules/keyboard-shortcut-menu/components/KeyboardShortcutMenuOpenContent.tsx b/packages/twenty-front/src/modules/keyboard-shortcut-menu/components/KeyboardShortcutMenuOpenContent.tsx index 9155fed53..720a85339 100644 --- a/packages/twenty-front/src/modules/keyboard-shortcut-menu/components/KeyboardShortcutMenuOpenContent.tsx +++ b/packages/twenty-front/src/modules/keyboard-shortcut-menu/components/KeyboardShortcutMenuOpenContent.tsx @@ -4,9 +4,12 @@ import { KEYBOARD_SHORTCUTS_GENERAL } from '@/keyboard-shortcut-menu/constants/K import { KEYBOARD_SHORTCUTS_TABLE } from '@/keyboard-shortcut-menu/constants/KeyboardShortcutsTable'; import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope'; -import { useKeyboardShortcutMenu } from '../hooks/useKeyboardShortcutMenu'; +import { + KEYBOARD_SHORTCUT_MENU_INSTANCE_ID, + useKeyboardShortcutMenu, +} from '../hooks/useKeyboardShortcutMenu'; -import { useGlobalHotkeys } from '@/ui/utilities/hotkey/hooks/useGlobalHotkeys'; +import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement'; import { KeyboardMenuDialog } from './KeyboardShortcutMenuDialog'; import { KeyboardMenuGroup } from './KeyboardShortcutMenuGroup'; import { KeyboardMenuItem } from './KeyboardShortcutMenuItem'; @@ -15,15 +18,15 @@ export const KeyboardShortcutMenuOpenContent = () => { const { toggleKeyboardShortcutMenu, closeKeyboardShortcutMenu } = useKeyboardShortcutMenu(); - useGlobalHotkeys( - [Key.Escape], - () => { + useHotkeysOnFocusedElement({ + keys: [Key.Escape], + callback: () => { closeKeyboardShortcutMenu(); }, - false, - AppHotkeyScope.KeyboardShortcutMenuOpen, - [closeKeyboardShortcutMenu], - ); + focusId: KEYBOARD_SHORTCUT_MENU_INSTANCE_ID, + scope: AppHotkeyScope.KeyboardShortcutMenuOpen, + dependencies: [closeKeyboardShortcutMenu], + }); return ( <> diff --git a/packages/twenty-front/src/modules/keyboard-shortcut-menu/hooks/__tests__/useKeyboardShortcutMenu.test.tsx b/packages/twenty-front/src/modules/keyboard-shortcut-menu/hooks/__tests__/useKeyboardShortcutMenu.test.tsx index 292730ea0..082d04df8 100644 --- a/packages/twenty-front/src/modules/keyboard-shortcut-menu/hooks/__tests__/useKeyboardShortcutMenu.test.tsx +++ b/packages/twenty-front/src/modules/keyboard-shortcut-menu/hooks/__tests__/useKeyboardShortcutMenu.test.tsx @@ -8,18 +8,24 @@ import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope'; import { useKeyboardShortcutMenu } from '../useKeyboardShortcutMenu'; -const mockSetHotkeyScopeAndMemorizePreviousScope = jest.fn(); +const mockPushFocusItemToFocusStack = jest.fn(); +const mockRemoveFocusItemFromFocusStackById = jest.fn(); -const mockGoBackToPreviousHotkeyScope = jest.fn(); - -jest.mock('@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope', () => ({ - usePreviousHotkeyScope: () => ({ - setHotkeyScopeAndMemorizePreviousScope: - mockSetHotkeyScopeAndMemorizePreviousScope, - goBackToPreviousHotkeyScope: mockGoBackToPreviousHotkeyScope, +jest.mock('@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack', () => ({ + usePushFocusItemToFocusStack: () => ({ + pushFocusItemToFocusStack: mockPushFocusItemToFocusStack, }), })); +jest.mock( + '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById', + () => ({ + useRemoveFocusItemFromFocusStackById: () => ({ + removeFocusItemFromFocusStackById: mockRemoveFocusItemFromFocusStackById, + }), + }), +); + const renderHookConfig = () => { const { result } = renderHook( () => { @@ -39,6 +45,10 @@ const renderHookConfig = () => { }; describe('useKeyboardShortcutMenu', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + it('should toggle keyboard shortcut menu correctly', async () => { const { result } = renderHookConfig(); expect(result.current.toggleKeyboardShortcutMenu).toBeDefined(); @@ -48,8 +58,19 @@ describe('useKeyboardShortcutMenu', () => { result.current.toggleKeyboardShortcutMenu(); }); - expect(mockSetHotkeyScopeAndMemorizePreviousScope).toHaveBeenCalledWith({ - scope: AppHotkeyScope.KeyboardShortcutMenu, + expect(mockPushFocusItemToFocusStack).toHaveBeenCalledWith({ + focusId: 'keyboard-shortcut-menu', + component: { + type: 'keyboard-shortcut-menu', + instanceId: 'keyboard-shortcut-menu', + }, + globalHotkeysConfig: { + enableGlobalHotkeysConflictingWithKeyboard: false, + enableGlobalHotkeysWithModifiers: false, + }, + hotkeyScope: { + scope: AppHotkeyScope.KeyboardShortcutMenuOpen, + }, }); expect(result.current.isKeyboardShortcutMenuOpened).toBe(true); @@ -57,8 +78,8 @@ describe('useKeyboardShortcutMenu', () => { result.current.toggleKeyboardShortcutMenu(); }); - expect(mockSetHotkeyScopeAndMemorizePreviousScope).toHaveBeenCalledWith({ - scope: AppHotkeyScope.KeyboardShortcutMenu, + expect(mockRemoveFocusItemFromFocusStackById).toHaveBeenCalledWith({ + focusId: 'keyboard-shortcut-menu', }); expect(result.current.isKeyboardShortcutMenuOpened).toBe(false); }); @@ -69,8 +90,19 @@ describe('useKeyboardShortcutMenu', () => { result.current.openKeyboardShortcutMenu(); }); - expect(mockSetHotkeyScopeAndMemorizePreviousScope).toHaveBeenCalledWith({ - scope: AppHotkeyScope.KeyboardShortcutMenu, + expect(mockPushFocusItemToFocusStack).toHaveBeenCalledWith({ + focusId: 'keyboard-shortcut-menu', + component: { + type: 'keyboard-shortcut-menu', + instanceId: 'keyboard-shortcut-menu', + }, + globalHotkeysConfig: { + enableGlobalHotkeysConflictingWithKeyboard: false, + enableGlobalHotkeysWithModifiers: false, + }, + hotkeyScope: { + scope: AppHotkeyScope.KeyboardShortcutMenuOpen, + }, }); expect(result.current.isKeyboardShortcutMenuOpened).toBe(true); @@ -78,7 +110,9 @@ describe('useKeyboardShortcutMenu', () => { result.current.closeKeyboardShortcutMenu(); }); - expect(mockGoBackToPreviousHotkeyScope).toHaveBeenCalled(); + expect(mockRemoveFocusItemFromFocusStackById).toHaveBeenCalledWith({ + focusId: 'keyboard-shortcut-menu', + }); expect(result.current.isKeyboardShortcutMenuOpened).toBe(false); }); }); diff --git a/packages/twenty-front/src/modules/keyboard-shortcut-menu/hooks/useKeyboardShortcutMenu.ts b/packages/twenty-front/src/modules/keyboard-shortcut-menu/hooks/useKeyboardShortcutMenu.ts index bd3286d7a..91e32dfb0 100644 --- a/packages/twenty-front/src/modules/keyboard-shortcut-menu/hooks/useKeyboardShortcutMenu.ts +++ b/packages/twenty-front/src/modules/keyboard-shortcut-menu/hooks/useKeyboardShortcutMenu.ts @@ -1,25 +1,39 @@ import { useRecoilCallback } from 'recoil'; -import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope'; +import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack'; +import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById'; +import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType'; import { isKeyboardShortcutMenuOpenedState } from '../states/isKeyboardShortcutMenuOpenedState'; +export const KEYBOARD_SHORTCUT_MENU_INSTANCE_ID = 'keyboard-shortcut-menu'; + export const useKeyboardShortcutMenu = () => { - const { - setHotkeyScopeAndMemorizePreviousScope, - goBackToPreviousHotkeyScope, - } = usePreviousHotkeyScope(); + const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack(); + const { removeFocusItemFromFocusStackById } = + useRemoveFocusItemFromFocusStackById(); const openKeyboardShortcutMenu = useRecoilCallback( ({ set }) => () => { set(isKeyboardShortcutMenuOpenedState, true); - setHotkeyScopeAndMemorizePreviousScope({ - scope: AppHotkeyScope.KeyboardShortcutMenu, + pushFocusItemToFocusStack({ + focusId: KEYBOARD_SHORTCUT_MENU_INSTANCE_ID, + component: { + type: FocusComponentType.KEYBOARD_SHORTCUT_MENU, + instanceId: KEYBOARD_SHORTCUT_MENU_INSTANCE_ID, + }, + globalHotkeysConfig: { + enableGlobalHotkeysConflictingWithKeyboard: false, + enableGlobalHotkeysWithModifiers: false, + }, + hotkeyScope: { + scope: AppHotkeyScope.KeyboardShortcutMenuOpen, + }, }); }, - [setHotkeyScopeAndMemorizePreviousScope], + [pushFocusItemToFocusStack], ); const closeKeyboardShortcutMenu = useRecoilCallback( @@ -31,10 +45,12 @@ export const useKeyboardShortcutMenu = () => { if (isKeyboardShortcutMenuOpened) { set(isKeyboardShortcutMenuOpenedState, false); - goBackToPreviousHotkeyScope(); + removeFocusItemFromFocusStackById({ + focusId: KEYBOARD_SHORTCUT_MENU_INSTANCE_ID, + }); } }, - [goBackToPreviousHotkeyScope], + [removeFocusItemFromFocusStackById], ); const toggleKeyboardShortcutMenu = useRecoilCallback( diff --git a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardHotkeyEffect.tsx b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardHotkeyEffect.tsx index 8f7cb1226..d6527743a 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardHotkeyEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardHotkeyEffect.tsx @@ -4,8 +4,8 @@ import { Key } from 'ts-key-enum'; import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext'; import { useRecordBoardCardNavigation } from '@/object-record/record-board/hooks/useRecordBoardCardNavigation'; import { useRecordBoardSelectAllHotkeys } from '@/object-record/record-board/hooks/useRecordBoardSelectAllHotkeys'; -import { RECORD_INDEX_FOCUS_ID } from '@/object-record/record-index/constants/RecordIndexFocusId'; import { RecordIndexHotkeyScope } from '@/object-record/record-index/types/RecordIndexHotkeyScope'; +import { PageFocusId } from '@/types/PageFocusId'; import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement'; export const RecordBoardHotkeyEffect = () => { @@ -18,7 +18,7 @@ export const RecordBoardHotkeyEffect = () => { callback: () => { move('down'); }, - focusId: RECORD_INDEX_FOCUS_ID, + focusId: PageFocusId.RecordIndex, scope: RecordIndexHotkeyScope.RecordIndex, dependencies: [move], }); diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnDropdownMenu.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnDropdownMenu.tsx index 9bfc1930f..7e15d75d5 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnDropdownMenu.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnDropdownMenu.tsx @@ -1,65 +1,28 @@ -import styled from '@emotion/styled'; -import { useCallback, useRef } from 'react'; - import { useRecordGroupActions } from '@/object-record/record-group/hooks/useRecordGroupActions'; import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; -import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer'; -import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { ViewType } from '@/views/types/ViewType'; import { MenuItem } from 'twenty-ui/navigation'; -const StyledMenuContainer = styled.div` - position: absolute; - top: ${({ theme }) => theme.spacing(10)}; - width: 200px; - z-index: 1; -`; - -type RecordBoardColumnDropdownMenuProps = { - onClose: () => void; - onDelete?: (id: string) => void; - stageId: string; -}; - -export const RecordBoardColumnDropdownMenu = ({ - onClose, -}: RecordBoardColumnDropdownMenuProps) => { - const boardColumnMenuRef = useRef(null); - +export const RecordBoardColumnDropdownMenu = () => { const recordGroupActions = useRecordGroupActions({ viewType: ViewType.Kanban, }); - const closeMenu = useCallback(() => { - onClose(); - }, [onClose]); - - useListenClickOutside({ - refs: [boardColumnMenuRef], - callback: closeMenu, - listenerId: 'record-board-column-dropdown-menu', - }); - return ( - - - - - {recordGroupActions.map((action) => ( - { - action.callback(); - closeMenu(); - }} - LeftIcon={action.icon} - text={action.label} - /> - ))} - - - - + + + {recordGroupActions.map((action) => ( + { + action.callback(); + }} + LeftIcon={action.icon} + text={action.label} + /> + ))} + + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeader.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeader.tsx index 125a0f86d..b7fd2b506 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeader.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeader.tsx @@ -7,10 +7,10 @@ import { RecordBoardColumnDropdownMenu } from '@/object-record/record-board/reco import { RecordBoardColumnHeaderAggregateDropdown } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdown'; import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext'; import { useAggregateRecordsForRecordBoardColumn } from '@/object-record/record-board/record-board-column/hooks/useAggregateRecordsForRecordBoardColumn'; -import { RecordBoardColumnHotkeyScope } from '@/object-record/record-board/types/BoardColumnHotkeyScope'; import { RecordGroupDefinitionType } from '@/object-record/record-group/types/RecordGroupDefinition'; import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord'; -import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; +import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; +import { useToggleDropdown } from '@/ui/layout/dropdown/hooks/useToggleDropdown'; import { Tag } from 'twenty-ui/components'; import { IconDotsVertical, IconPlus } from 'twenty-ui/display'; import { LightIconButton } from 'twenty-ui/input'; @@ -66,32 +66,11 @@ const StyledTag = styled(Tag)` export const RecordBoardColumnHeader = () => { const { columnDefinition } = useContext(RecordBoardColumnContext); - const [isBoardColumnMenuOpen, setIsBoardColumnMenuOpen] = useState(false); const [isHeaderHovered, setIsHeaderHovered] = useState(false); const { objectMetadataItem, selectFieldMetadataItem } = useContext(RecordBoardContext); - const { - setHotkeyScopeAndMemorizePreviousScope, - goBackToPreviousHotkeyScope, - } = usePreviousHotkeyScope(); - - const handleBoardColumnMenuOpen = () => { - setIsBoardColumnMenuOpen(true); - setHotkeyScopeAndMemorizePreviousScope({ - scope: RecordBoardColumnHotkeyScope.BoardColumn, - customScopes: { - goto: false, - }, - }); - }; - - const handleBoardColumnMenuClose = () => { - goBackToPreviousHotkeyScope(); - setIsBoardColumnMenuOpen(false); - }; - const { aggregateValue, aggregateLabel } = useAggregateRecordsForRecordBoardColumn(); @@ -105,6 +84,10 @@ export const RecordBoardColumnHeader = () => { objectMetadataItem: objectMetadataItem, }); + const { toggleDropdown } = useToggleDropdown(); + + const dropdownId = `record-board-column-dropdown-${columnDefinition.id}`; + return ( { > - } + dropdownComponents={} /> + { { + toggleDropdown({ + dropdownComponentInstanceIdFromProps: dropdownId, + }); + }} /> {hasObjectUpdatePermissions && ( { - {isBoardColumnMenuOpen && ( - - )} ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownMenuContent.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownMenuContent.tsx index f06c970dc..319c2b204 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownMenuContent.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownMenuContent.tsx @@ -1,36 +1,24 @@ -import { Key } from 'ts-key-enum'; - import { useDropdownContextStateManagement } from '@/dropdown-context-state-management/hooks/useDropdownContextStateManagement'; import { RecordBoardColumnHeaderAggregateDropdownContext, RecordBoardColumnHeaderAggregateDropdownContextValue, } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownContext'; -import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope'; import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; -import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useLingui } from '@lingui/react/macro'; import { MenuItem } from 'twenty-ui/navigation'; export const RecordBoardColumnHeaderAggregateDropdownMenuContent = () => { const { t } = useLingui(); - const { onContentChange, closeDropdown } = + const { onContentChange } = useDropdownContextStateManagement( { context: RecordBoardColumnHeaderAggregateDropdownContext, }, ); - useScopedHotkeys( - [Key.Escape], - () => { - closeDropdown(); - }, - TableOptionsHotkeyScope.Dropdown, - ); - return ( diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownOptionsContent.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownOptionsContent.tsx index 2892fff36..faa70c885 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownOptionsContent.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownOptionsContent.tsx @@ -10,18 +10,15 @@ import { getAggregateOperationLabel } from '@/object-record/record-board/record- import { recordIndexKanbanAggregateOperationState } from '@/object-record/record-index/states/recordIndexKanbanAggregateOperationState'; import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations'; import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations'; -import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope'; import { AvailableFieldsForAggregateOperation } from '@/object-record/types/AvailableFieldsForAggregateOperation'; import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader'; import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; -import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { useUpdateViewAggregate } from '@/views/hooks/useUpdateViewAggregate'; import isEmpty from 'lodash.isempty'; import { useRecoilValue } from 'recoil'; -import { Key } from 'ts-key-enum'; import { IconCheck, IconChevronLeft } from 'twenty-ui/display'; export const RecordBoardColumnHeaderAggregateDropdownOptionsContent = ({ @@ -38,14 +35,6 @@ export const RecordBoardColumnHeaderAggregateDropdownOptionsContent = ({ }, ); - useScopedHotkeys( - [Key.Escape], - () => { - closeDropdown(); - }, - TableOptionsHotkeyScope.Dropdown, - ); - const setAggregateOperation = useSetRecoilComponentStateV2( aggregateOperationComponentState, ); diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormBooleanFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormBooleanFieldInput.tsx index 744049857..850d735d3 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormBooleanFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormBooleanFieldInput.tsx @@ -85,6 +85,7 @@ export const FormBooleanFieldInput = ({ {draftValue.type === 'static' ? ( diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormDateTimeFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormDateTimeFieldInput.tsx index 454d3b6d7..4bccfac14 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormDateTimeFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormDateTimeFieldInput.tsx @@ -296,6 +296,7 @@ export const FormDateTimeFieldInput = ({ diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormFieldInputInnerContainer.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormFieldInputInnerContainer.tsx index 8ce867948..5d97d4889 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormFieldInputInnerContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormFieldInputInnerContainer.tsx @@ -1,5 +1,7 @@ import { FormFieldInputHotKeyScope } from '@/object-record/record-field/form-types/constants/FormFieldInputHotKeyScope'; -import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; +import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack'; +import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById'; +import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType'; import { css } from '@emotion/react'; import styled from '@emotion/styled'; import { forwardRef, HTMLAttributes, Ref } from 'react'; @@ -9,9 +11,12 @@ type FormFieldInputInnerContainerProps = { multiline?: boolean; readonly?: boolean; preventSetHotkeyScope?: boolean; + formFieldInputInstanceId: string; }; -const StyledFormFieldInputInnerContainer = styled.div` +const StyledFormFieldInputInnerContainer = styled.div< + Omit +>` background-color: ${({ theme }) => theme.background.transparent.lighter}; border: 1px solid ${({ theme }) => theme.border.color.medium}; border-top-left-radius: ${({ theme }) => theme.border.radius.sm}; @@ -48,20 +53,25 @@ export const FormFieldInputInnerContainer = forwardRef( readonly, preventSetHotkeyScope = false, onClick, + formFieldInputInstanceId, }: HTMLAttributes & FormFieldInputInnerContainerProps, ref: Ref, ) => { - const { - goBackToPreviousHotkeyScope, - setHotkeyScopeAndMemorizePreviousScope, - } = usePreviousHotkeyScope(); + const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack(); + const { removeFocusItemFromFocusStackById } = + useRemoveFocusItemFromFocusStackById(); const handleFocus = (e: React.FocusEvent) => { onFocus?.(e); if (!preventSetHotkeyScope) { - setHotkeyScopeAndMemorizePreviousScope({ - scope: FormFieldInputHotKeyScope.FormFieldInput, + pushFocusItemToFocusStack({ + focusId: formFieldInputInstanceId, + component: { + type: FocusComponentType.FORM_FIELD_INPUT, + instanceId: formFieldInputInstanceId, + }, + hotkeyScope: { scope: FormFieldInputHotKeyScope.FormFieldInput }, }); } }; @@ -70,7 +80,9 @@ export const FormFieldInputInnerContainer = forwardRef( onBlur?.(e); if (!preventSetHotkeyScope) { - goBackToPreviousHotkeyScope(); + removeFocusItemFromFocusStackById({ + focusId: formFieldInputInstanceId, + }); } }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormMultiSelectFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormMultiSelectFieldInput.tsx index 517a69c01..8533e333c 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormMultiSelectFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormMultiSelectFieldInput.tsx @@ -14,7 +14,9 @@ import { MultiSelectInput } from '@/ui/field/input/components/MultiSelectInput'; import { InputLabel } from '@/ui/input/components/InputLabel'; import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth'; import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer'; -import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; +import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack'; +import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById'; +import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType'; import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString'; import { useTheme } from '@emotion/react'; import { isArray } from '@sniptt/guards'; @@ -88,10 +90,9 @@ export const FormMultiSelectFieldInput = ({ const hotkeyScope = FormMultiSelectFieldInputHotKeyScope.FormMultiSelectFieldInput; - const { - setHotkeyScopeAndMemorizePreviousScope, - goBackToPreviousHotkeyScope, - } = usePreviousHotkeyScope(); + const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack(); + const { removeFocusItemFromFocusStackById } = + useRemoveFocusItemFromFocusStackById(); const [draftValue, setDraftValue] = useState< | { @@ -128,8 +129,13 @@ export const FormMultiSelectFieldInput = ({ editingMode: 'edit', }); - setHotkeyScopeAndMemorizePreviousScope({ - scope: hotkeyScope, + pushFocusItemToFocusStack({ + focusId: instanceId, + component: { + type: FocusComponentType.FORM_FIELD_INPUT, + instanceId, + }, + hotkeyScope: { scope: hotkeyScope }, }); }; @@ -157,7 +163,7 @@ export const FormMultiSelectFieldInput = ({ editingMode: 'view', }); - goBackToPreviousHotkeyScope(); + removeFocusItemFromFocusStackById({ focusId: instanceId }); }; const handleVariableTagInsert = (variableName: string) => { @@ -201,6 +207,7 @@ export const FormMultiSelectFieldInput = ({ {draftValue.type === 'static' ? ( diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormNumberFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormNumberFieldInput.tsx index 26d9ad034..f78467a32 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormNumberFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormNumberFieldInput.tsx @@ -121,6 +121,7 @@ export const FormNumberFieldInput = ({ diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormRawJsonFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormRawJsonFieldInput.tsx index 1f53da157..8c03c2de1 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormRawJsonFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormRawJsonFieldInput.tsx @@ -71,6 +71,7 @@ export const FormRawJsonFieldInput = ({ { - onCancel(); - }, - hotkeyScope, - [onCancel], - ); + useHotkeysOnFocusedElement({ + keys: Key.Escape, + callback: onCancel, + focusId: instanceId, + scope: hotkeyScope, + dependencies: [onCancel], + }); return ( @@ -149,6 +149,7 @@ export const FormSelectFieldInput = ({ /> ) : ( {label} : null} {disabled ? ( - + diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormTextFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormTextFieldInput.tsx index 0e595d907..76ec17896 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormTextFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormTextFieldInput.tsx @@ -71,6 +71,7 @@ export const FormTextFieldInput = ({ {draftValue.type === 'static' ? ( diff --git a/packages/twenty-front/src/modules/object-record/record-index/constants/RecordIndexFocusId.ts b/packages/twenty-front/src/modules/object-record/record-index/constants/RecordIndexFocusId.ts deleted file mode 100644 index 711788489..000000000 --- a/packages/twenty-front/src/modules/object-record/record-index/constants/RecordIndexFocusId.ts +++ /dev/null @@ -1 +0,0 @@ -export const RECORD_INDEX_FOCUS_ID = 'record-index'; diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useResetFocusStackToRecordIndex.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useResetFocusStackToRecordIndex.ts index 9fad1fb41..4ce67a141 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useResetFocusStackToRecordIndex.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useResetFocusStackToRecordIndex.ts @@ -1,5 +1,5 @@ -import { RECORD_INDEX_FOCUS_ID } from '@/object-record/record-index/constants/RecordIndexFocusId'; import { RecordIndexHotkeyScope } from '@/object-record/record-index/types/RecordIndexHotkeyScope'; +import { PageFocusId } from '@/types/PageFocusId'; import { useResetFocusStackToFocusItem } from '@/ui/utilities/focus/hooks/useResetFocusStackToFocusItem'; import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType'; @@ -9,10 +9,10 @@ export const useResetFocusStackToRecordIndex = () => { const resetFocusStackToRecordIndex = () => { resetFocusStackToFocusItem({ focusStackItem: { - focusId: RECORD_INDEX_FOCUS_ID, + focusId: PageFocusId.RecordIndex, componentInstance: { componentType: FocusComponentType.PAGE, - componentInstanceId: RECORD_INDEX_FOCUS_ID, + componentInstanceId: PageFocusId.RecordIndex, }, globalHotkeysConfig: { enableGlobalHotkeysWithModifiers: true, diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx index ac8e44b62..63f5d8c0c 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx @@ -14,7 +14,6 @@ import { RecordIndexHotkeyScope } from '@/object-record/record-index/types/Recor import { RecordTableComponentInstance } from '@/object-record/record-table/components/RecordTableComponentInstance'; import { RecordTableContextProvider } from '@/object-record/record-table/components/RecordTableContextProvider'; import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement'; -import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { RecordUpdateContext } from '../contexts/EntityUpdateMutationHookContext'; import { useRecordTable } from '../hooks/useRecordTable'; @@ -48,16 +47,6 @@ export const RecordTableWithWrappers = ({ selectAllRows(); }; - useScopedHotkeys( - 'ctrl+a,meta+a', - handleSelectAllRows, - RecordIndexHotkeyScope.RecordIndex, - [], - { - enableOnFormTags: false, - }, - ); - useHotkeysOnFocusedElement({ keys: ['ctrl+a,meta+a'], callback: handleSelectAllRows, diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyEscapeHotkeyEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyEscapeHotkeyEffect.tsx index 0ec1f1a04..ae378b005 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyEscapeHotkeyEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyEscapeHotkeyEffect.tsx @@ -1,10 +1,10 @@ import { Key } from 'ts-key-enum'; -import { RECORD_INDEX_FOCUS_ID } from '@/object-record/record-index/constants/RecordIndexFocusId'; import { RecordIndexHotkeyScope } from '@/object-record/record-index/types/RecordIndexHotkeyScope'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; import { isAtLeastOneTableRowSelectedSelector } from '@/object-record/record-table/record-table-row/states/isAtLeastOneTableRowSelectedSelector'; +import { PageFocusId } from '@/types/PageFocusId'; import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; @@ -28,7 +28,7 @@ export const RecordTableBodyEscapeHotkeyEffect = () => { useHotkeysOnFocusedElement({ keys: [Key.Escape], callback: handleEscape, - focusId: RECORD_INDEX_FOCUS_ID, + focusId: PageFocusId.RecordIndex, scope: RecordIndexHotkeyScope.RecordIndex, dependencies: [handleEscape], options: { diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyFocusKeyboardEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyFocusKeyboardEffect.tsx index 13d266259..e9e591850 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyFocusKeyboardEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyFocusKeyboardEffect.tsx @@ -1,10 +1,10 @@ -import { RECORD_INDEX_FOCUS_ID } from '@/object-record/record-index/constants/RecordIndexFocusId'; import { RecordIndexHotkeyScope } from '@/object-record/record-index/types/RecordIndexHotkeyScope'; import { useRecordTableRowFocusHotkeys } from '@/object-record/record-table/hooks/useRecordTableRowFocusHotkeys'; +import { PageFocusId } from '@/types/PageFocusId'; export const RecordTableBodyFocusKeyboardEffect = () => { useRecordTableRowFocusHotkeys({ - focusId: RECORD_INDEX_FOCUS_ID, + focusId: PageFocusId.RecordIndex, hotkeyScope: RecordIndexHotkeyScope.RecordIndex, }); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateDropdownSubmenuContent.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateDropdownSubmenuContent.tsx index 698265910..a7ccc68ea 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateDropdownSubmenuContent.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateDropdownSubmenuContent.tsx @@ -1,15 +1,11 @@ import { RecordTableColumnAggregateFooterAggregateOperationMenuItems } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterAggregateOperationMenuItems'; import { RecordTableColumnAggregateFooterDropdownContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdownContext'; import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations'; -import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope'; import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader'; import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; -import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown'; -import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useContext } from 'react'; -import { Key } from 'ts-key-enum'; import { IconChevronLeft } from 'twenty-ui/display'; export const RecordTableColumnAggregateFooterDropdownSubmenuContent = ({ @@ -19,19 +15,10 @@ export const RecordTableColumnAggregateFooterDropdownSubmenuContent = ({ aggregateOperations: ExtendedAggregateOperations[]; title: string; }) => { - const { dropdownId, resetContent } = useContext( + const { resetContent } = useContext( RecordTableColumnAggregateFooterDropdownContext, ); - const { closeDropdown } = useCloseDropdown(); - useScopedHotkeys( - [Key.Escape], - () => { - resetContent(); - closeDropdown(dropdownId); - }, - TableOptionsHotkeyScope.Dropdown, - ); return ( { const { closeDropdown } = useCloseDropdown(); const { objectMetadataItem } = useRecordTableContextOrThrow(); - useScopedHotkeys( - [Key.Escape], - () => { - closeDropdown(dropdownId); - }, - TableOptionsHotkeyScope.Dropdown, - ); - const availableAggregateOperation = useMemo( () => getAvailableAggregateOperationsForFieldMetadataType({ diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionCodeEditorTab.tsx b/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionCodeEditorTab.tsx index 00ec27e41..2a558e7d6 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionCodeEditorTab.tsx +++ b/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionCodeEditorTab.tsx @@ -3,14 +3,10 @@ import { SettingsServerlessFunctionCodeEditor, } from '@/settings/serverless-functions/components/SettingsServerlessFunctionCodeEditor'; import { SETTINGS_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID } from '@/settings/serverless-functions/constants/SettingsServerlessFunctionTabListComponentId'; -import { SettingsServerlessFunctionHotkeyScope } from '@/settings/serverless-functions/types/SettingsServerlessFunctionHotKeyScope'; -import { SettingsPath } from '@/types/SettingsPath'; import { TabList } from '@/ui/layout/tab-list/components/TabList'; import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState'; -import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import styled from '@emotion/styled'; -import { Key } from 'ts-key-enum'; import { H2Title, IconGitCommit, @@ -19,8 +15,6 @@ import { } from 'twenty-ui/display'; import { Button, CoreEditorHeader } from 'twenty-ui/input'; import { Section } from 'twenty-ui/layout'; -import { useHotkeyScopeOnMount } from '~/hooks/useHotkeyScopeOnMount'; -import { useNavigateSettings } from '~/hooks/useNavigateSettings'; const StyledTabList = styled(TabList)` border-bottom: none; @@ -91,19 +85,6 @@ export const SettingsServerlessFunctionCodeEditorTab = ({ /> ); - const navigate = useNavigateSettings(); - useHotkeyScopeOnMount( - SettingsServerlessFunctionHotkeyScope.ServerlessFunctionEditorTab, - ); - - useScopedHotkeys( - [Key.Escape], - () => { - navigate(SettingsPath.ServerlessFunctions); - }, - SettingsServerlessFunctionHotkeyScope.ServerlessFunctionEditorTab, - ); - return (
{ - openModal(DELETE_FUNCTION_MODAL_ID); - }, - SettingsServerlessFunctionHotkeyScope.ServerlessFunctionSettingsTab, - ); - - useScopedHotkeys( - [Key.Escape], - () => { - navigate(SettingsPath.ServerlessFunctions); - }, - SettingsServerlessFunctionHotkeyScope.ServerlessFunctionSettingsTab, - ); return ( <> { - navigate(SettingsPath.ServerlessFunctions); - }, - SettingsServerlessFunctionHotkeyScope.ServerlessFunctionTestTab, - ); - return (
{ - const confirmButton = buttons.find((button) => button.role === 'confirm'); + const handleEnter = (event: KeyboardEvent) => { + const confirmButton = buttons.find((button) => button.role === 'confirm'); - event.preventDefault(); + event.preventDefault(); - if (isDefined(confirmButton)) { - confirmButton?.onClick?.(event); - onClose?.(); - } - }, - DialogHotkeyScope.Dialog, - [], - ); - - useScopedHotkeys( - Key.Escape, - (event: KeyboardEvent) => { - event.preventDefault(); + if (isDefined(confirmButton)) { + confirmButton?.onClick?.(event); onClose?.(); - }, - DialogHotkeyScope.Dialog, - [], - ); + } + }; + + const handleEscape = (event: KeyboardEvent) => { + event.preventDefault(); + onClose?.(); + }; + + useHotkeysOnFocusedElement({ + keys: [Key.Enter], + callback: handleEnter, + focusId: DIALOG_FOCUS_ID, + scope: DialogHotkeyScope.Dialog, + dependencies: [buttons], + }); + + useHotkeysOnFocusedElement({ + keys: [Key.Escape], + callback: handleEscape, + focusId: DIALOG_FOCUS_ID, + scope: DialogHotkeyScope.Dialog, + dependencies: [handleEscape], + }); const dialogRef = useRef(null); diff --git a/packages/twenty-front/src/modules/ui/feedback/dialog-manager/components/DialogManagerEffect.tsx b/packages/twenty-front/src/modules/ui/feedback/dialog-manager/components/DialogManagerEffect.tsx index 9787bccc6..f5b5107b6 100644 --- a/packages/twenty-front/src/modules/ui/feedback/dialog-manager/components/DialogManagerEffect.tsx +++ b/packages/twenty-front/src/modules/ui/feedback/dialog-manager/components/DialogManagerEffect.tsx @@ -1,26 +1,34 @@ import { useEffect } from 'react'; -import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; - +import { DIALOG_FOCUS_ID } from '@/ui/feedback/dialog-manager/constants/DialogFocusId'; import { DIALOG_MANAGER_HOTKEY_SCOPE_MEMOIZE_KEY } from '@/ui/feedback/dialog-manager/constants/DialogManagerHotkeyScopeMemoizeKey'; +import { DialogHotkeyScope } from '@/ui/feedback/dialog-manager/types/DialogHotkeyScope'; +import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack'; +import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType'; import { useDialogManagerScopedStates } from '../hooks/internal/useDialogManagerScopedStates'; -import { DialogHotkeyScope } from '../types/DialogHotkeyScope'; export const DialogManagerEffect = () => { const { dialogInternal } = useDialogManagerScopedStates(); - const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope(); + const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack(); useEffect(() => { if (dialogInternal.queue.length === 0) { return; } - setHotkeyScopeAndMemorizePreviousScope({ - scope: DialogHotkeyScope.Dialog, + pushFocusItemToFocusStack({ + focusId: DIALOG_FOCUS_ID, + component: { + type: FocusComponentType.DIALOG, + instanceId: DIALOG_FOCUS_ID, + }, + hotkeyScope: { + scope: DialogHotkeyScope.Dialog, + }, memoizeKey: DIALOG_MANAGER_HOTKEY_SCOPE_MEMOIZE_KEY, }); - }, [dialogInternal.queue, setHotkeyScopeAndMemorizePreviousScope]); + }, [dialogInternal.queue, pushFocusItemToFocusStack]); return <>; }; diff --git a/packages/twenty-front/src/modules/ui/feedback/dialog-manager/constants/DialogFocusId.ts b/packages/twenty-front/src/modules/ui/feedback/dialog-manager/constants/DialogFocusId.ts new file mode 100644 index 000000000..47422071e --- /dev/null +++ b/packages/twenty-front/src/modules/ui/feedback/dialog-manager/constants/DialogFocusId.ts @@ -0,0 +1 @@ +export const DIALOG_FOCUS_ID = 'dialog'; diff --git a/packages/twenty-front/src/modules/ui/feedback/dialog-manager/hooks/useDialogManager.ts b/packages/twenty-front/src/modules/ui/feedback/dialog-manager/hooks/useDialogManager.ts index 251bc233c..0c248ad84 100644 --- a/packages/twenty-front/src/modules/ui/feedback/dialog-manager/hooks/useDialogManager.ts +++ b/packages/twenty-front/src/modules/ui/feedback/dialog-manager/hooks/useDialogManager.ts @@ -1,10 +1,10 @@ import { useRecoilCallback } from 'recoil'; import { v4 } from 'uuid'; -import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId'; -import { DIALOG_MANAGER_HOTKEY_SCOPE_MEMOIZE_KEY } from '@/ui/feedback/dialog-manager/constants/DialogManagerHotkeyScopeMemoizeKey'; +import { DIALOG_FOCUS_ID } from '@/ui/feedback/dialog-manager/constants/DialogFocusId'; +import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById'; import { DialogManagerScopeInternalContext } from '../scopes/scope-internal-context/DialogManagerScopeInternalContext'; import { dialogInternalScopedState } from '../states/dialogInternalScopedState'; import { DialogOptions } from '../types/DialogOptions'; @@ -19,7 +19,8 @@ export const useDialogManager = (props?: useDialogManagerProps) => { props?.dialogManagerScopeId, ); - const { goBackToPreviousHotkeyScope } = usePreviousHotkeyScope(); + const { removeFocusItemFromFocusStackById } = + useRemoveFocusItemFromFocusStackById(); const closeDialog = useRecoilCallback( ({ set }) => @@ -29,9 +30,9 @@ export const useDialogManager = (props?: useDialogManagerProps) => { queue: prevState.queue.filter((dialog) => dialog.id !== id), })); - goBackToPreviousHotkeyScope(DIALOG_MANAGER_HOTKEY_SCOPE_MEMOIZE_KEY); + removeFocusItemFromFocusStackById({ focusId: DIALOG_FOCUS_ID }); }, - [goBackToPreviousHotkeyScope, scopeId], + [removeFocusItemFromFocusStackById, scopeId], ); const setDialogQueue = useRecoilCallback( diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerInput.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerInput.tsx index 70da180cb..7c621a022 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerInput.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerInput.tsx @@ -1,11 +1,13 @@ import { TextInputV2 } from '@/ui/input/components/TextInputV2'; -import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; +import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack'; +import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById'; +import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType'; +import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { FocusEvent, useRef } from 'react'; import { Key } from 'ts-key-enum'; import { isDefined } from 'twenty-shared/utils'; import { IconComponent, TablerIconsProps } from 'twenty-ui/display'; -import { useHotkeyScopeOnMount } from '~/hooks/useHotkeyScopeOnMount'; type NavigationDrawerInputProps = { className?: string; @@ -19,6 +21,8 @@ type NavigationDrawerInputProps = { hotkeyScope: string; }; +const NAVIGATION_DRAWER_INPUT_FOCUS_ID = 'navigation-drawer-input'; + export const NavigationDrawerInput = ({ className, placeholder, @@ -32,37 +36,64 @@ export const NavigationDrawerInput = ({ }: NavigationDrawerInputProps) => { const inputRef = useRef(null); - useHotkeyScopeOnMount(hotkeyScope); - - useScopedHotkeys( - [Key.Escape], - () => { + useHotkeysOnFocusedElement({ + keys: Key.Escape, + callback: () => { onCancel(value); + removeFocusItemFromFocusStackById({ + focusId: NAVIGATION_DRAWER_INPUT_FOCUS_ID, + }); }, - hotkeyScope, - ); + focusId: NAVIGATION_DRAWER_INPUT_FOCUS_ID, + scope: hotkeyScope, + }); - useScopedHotkeys( - [Key.Enter], - () => { + useHotkeysOnFocusedElement({ + keys: Key.Enter, + callback: () => { onSubmit(value); + removeFocusItemFromFocusStackById({ + focusId: NAVIGATION_DRAWER_INPUT_FOCUS_ID, + }); }, - hotkeyScope, - ); + focusId: NAVIGATION_DRAWER_INPUT_FOCUS_ID, + scope: hotkeyScope, + }); useListenClickOutside({ refs: [inputRef], callback: (event) => { event.stopImmediatePropagation(); onClickOutside(event, value); + removeFocusItemFromFocusStackById({ + focusId: NAVIGATION_DRAWER_INPUT_FOCUS_ID, + }); }, listenerId: 'navigation-drawer-input', }); + const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack(); + const { removeFocusItemFromFocusStackById } = + useRemoveFocusItemFromFocusStackById(); + const handleFocus = (event: FocusEvent) => { if (isDefined(value)) { event.target.select(); } + pushFocusItemToFocusStack({ + focusId: NAVIGATION_DRAWER_INPUT_FOCUS_ID, + component: { + type: FocusComponentType.TEXT_INPUT, + instanceId: NAVIGATION_DRAWER_INPUT_FOCUS_ID, + }, + hotkeyScope: { scope: hotkeyScope }, + }); + }; + + const handleBlur = () => { + removeFocusItemFromFocusStackById({ + focusId: NAVIGATION_DRAWER_INPUT_FOCUS_ID, + }); }; return ( @@ -74,6 +105,7 @@ export const NavigationDrawerInput = ({ onChange={onChange} placeholder={placeholder} onFocus={handleFocus} + onBlur={handleBlur} sizeVariant="md" fullWidth autoFocus diff --git a/packages/twenty-front/src/modules/ui/utilities/focus/types/FocusComponentType.ts b/packages/twenty-front/src/modules/ui/utilities/focus/types/FocusComponentType.ts index 4af63acdc..e7e59c831 100644 --- a/packages/twenty-front/src/modules/ui/utilities/focus/types/FocusComponentType.ts +++ b/packages/twenty-front/src/modules/ui/utilities/focus/types/FocusComponentType.ts @@ -9,6 +9,9 @@ export enum FocusComponentType { RECORD_TABLE_CELL = 'record-table-cell', TEXT_AREA = 'text-area', TEXT_INPUT = 'text-input', + FORM_FIELD_INPUT = 'form-field-input', RECORD_BOARD_CARD = 'record-board-card', ACTIVITY_RICH_TEXT_EDITOR = 'activity-rich-text-editor', + KEYBOARD_SHORTCUT_MENU = 'keyboard-shortcut-menu', + DIALOG = 'dialog', } diff --git a/packages/twenty-front/src/modules/ui/utilities/hotkey/constants/DebugHotkeyScope.ts b/packages/twenty-front/src/modules/ui/utilities/hotkey/constants/DebugHotkeyScope.ts index a46853a42..0ac5ab524 100644 --- a/packages/twenty-front/src/modules/ui/utilities/hotkey/constants/DebugHotkeyScope.ts +++ b/packages/twenty-front/src/modules/ui/utilities/hotkey/constants/DebugHotkeyScope.ts @@ -1 +1 @@ -export const DEBUG_HOTKEY_SCOPE = true; +export const DEBUG_HOTKEY_SCOPE = false; diff --git a/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/__tests__/useScopedHotKeys.test.tsx b/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/__tests__/useScopedHotKeys.test.tsx deleted file mode 100644 index e2ed601e5..000000000 --- a/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/__tests__/useScopedHotKeys.test.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { act } from 'react-dom/test-utils'; -import { fireEvent, renderHook } from '@testing-library/react'; -import { RecoilRoot } from 'recoil'; - -import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; -import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; -import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope'; - -const hotKeyCallback = jest.fn(); - -describe('useScopedHotkeys', () => { - it('should work as expected', () => { - renderHook( - () => { - useScopedHotkeys('ctrl+k', hotKeyCallback, AppHotkeyScope.App); - - const setHotkeyScope = useSetHotkeyScope(); - - setHotkeyScope(AppHotkeyScope.App); - }, - { - wrapper: RecoilRoot, - }, - ); - - act(() => { - fireEvent.keyDown(document, { key: 'k', code: 'KeyK', ctrlKey: true }); - }); - - expect(hotKeyCallback).toHaveBeenCalled(); - }); -}); diff --git a/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useScopedHotkeyCallback.ts b/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useScopedHotkeyCallback.ts deleted file mode 100644 index f9c98ddd5..000000000 --- a/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useScopedHotkeyCallback.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { - Hotkey, - OptionsOrDependencyArray, -} from 'react-hotkeys-hook/dist/types'; -import { useRecoilCallback } from 'recoil'; - -import { logDebug } from '~/utils/logDebug'; - -import { DEBUG_HOTKEY_SCOPE } from '../constants/DebugHotkeyScope'; -import { internalHotkeysEnabledScopesState } from '../states/internal/internalHotkeysEnabledScopesState'; - -export const useScopedHotkeyCallback = ( - dependencies?: OptionsOrDependencyArray, -) => { - const dependencyArray = Array.isArray(dependencies) ? dependencies : []; - - return useRecoilCallback( - ({ snapshot }) => - ({ - callback, - hotkeysEvent, - keyboardEvent, - scope, - preventDefault, - }: { - keyboardEvent: KeyboardEvent; - hotkeysEvent: Hotkey; - callback: (keyboardEvent: KeyboardEvent, hotkeysEvent: Hotkey) => void; - scope: string; - preventDefault?: boolean; - }) => { - const currentHotkeyScopes = snapshot - .getLoadable(internalHotkeysEnabledScopesState) - .getValue(); - - if (!currentHotkeyScopes.includes(scope)) { - if (DEBUG_HOTKEY_SCOPE) { - logDebug( - `DEBUG: %cI can't call hotkey (${ - hotkeysEvent.keys - }) because I'm in scope [${scope}] and the active scopes are : [${currentHotkeyScopes.join( - ', ', - )}]`, - 'color: gray; ', - ); - } - - return; - } - - if (DEBUG_HOTKEY_SCOPE) { - logDebug( - `DEBUG: %cI can call hotkey (${ - hotkeysEvent.keys - }) because I'm in scope [${scope}] and the active scopes are : [${currentHotkeyScopes.join( - ', ', - )}]`, - 'color: green;', - ); - } - - if (preventDefault === true) { - if (DEBUG_HOTKEY_SCOPE) { - logDebug( - `DEBUG: %cI prevent default for hotkey (${hotkeysEvent.keys})`, - 'color: gray;', - ); - } - - keyboardEvent.stopPropagation(); - keyboardEvent.preventDefault(); - keyboardEvent.stopImmediatePropagation(); - } - - return callback(keyboardEvent, hotkeysEvent); - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - dependencyArray, - ); -}; diff --git a/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useScopedHotkeys.ts b/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useScopedHotkeys.ts deleted file mode 100644 index 9d186d0a2..000000000 --- a/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useScopedHotkeys.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { useHotkeys } from 'react-hotkeys-hook'; -import { HotkeyCallback, Keys, Options } from 'react-hotkeys-hook/dist/types'; -import { useRecoilState } from 'recoil'; - -import { isDefined } from 'twenty-shared/utils'; -import { pendingHotkeyState } from '../states/internal/pendingHotkeysState'; -import { useScopedHotkeyCallback } from './useScopedHotkeyCallback'; - -type UseHotkeysOptionsWithoutBuggyOptions = Omit; - -export const useScopedHotkeys = ( - keys: Keys, - callback: HotkeyCallback, - scope: string, - dependencies?: unknown[], - options?: UseHotkeysOptionsWithoutBuggyOptions, -) => { - const [pendingHotkey, setPendingHotkey] = useRecoilState(pendingHotkeyState); - - const callScopedHotkeyCallback = useScopedHotkeyCallback(dependencies); - - const enableOnContentEditable = isDefined(options?.enableOnContentEditable) - ? options.enableOnContentEditable - : true; - - const enableOnFormTags = isDefined(options?.enableOnFormTags) - ? options.enableOnFormTags - : true; - - const preventDefault = isDefined(options?.preventDefault) - ? options.preventDefault === true - : true; - - const ignoreModifiers = isDefined(options?.ignoreModifiers) - ? options.ignoreModifiers === true - : false; - - return useHotkeys( - keys, - (keyboardEvent, hotkeysEvent) => { - callScopedHotkeyCallback({ - keyboardEvent, - hotkeysEvent, - callback: () => { - if (!pendingHotkey) { - callback(keyboardEvent, hotkeysEvent); - return; - } - setPendingHotkey(null); - }, - scope, - preventDefault, - }); - }, - { - enableOnContentEditable, - enableOnFormTags, - ignoreModifiers, - }, - dependencies, - ); -}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/hooks/useAgentChat.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/hooks/useAgentChat.ts index 118cca8ea..d874527a5 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/hooks/useAgentChat.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/hooks/useAgentChat.ts @@ -1,10 +1,10 @@ import { InputHotkeyScope } from '@/ui/input/types/InputHotkeyScope'; -import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2'; import { useCallback, useState } from 'react'; import { useRecoilState } from 'recoil'; import { Key } from 'ts-key-enum'; +import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement'; import { useScrollWrapperElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperElement'; import { AgentChatMessageRole } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/constants/agent-chat-message-role'; import { v4 } from 'uuid'; @@ -117,17 +117,21 @@ export const useAgentChat = (agentId: string) => { await sendChatMessage(content); }; - useScopedHotkeys( - [Key.Enter], - (event) => { + useHotkeysOnFocusedElement({ + keys: [Key.Enter], + callback: (event: KeyboardEvent) => { if (!event.ctrlKey && !event.metaKey) { event.preventDefault(); handleSendMessage(); } }, - InputHotkeyScope.TextInput, - [agentChatInput, isLoading], - ); + focusId: `${agentId}-chat-input`, + scope: InputHotkeyScope.TextInput, + dependencies: [agentChatInput, isLoading], + options: { + enableOnFormTags: true, + }, + }); return { handleInputChange: (value: string) => setAgentChatInput(value), diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormBuilder.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormBuilder.tsx index f43b04568..0557285ba 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormBuilder.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormBuilder.tsx @@ -285,6 +285,7 @@ export const WorkflowEditActionFormBuilder = ({ { handleFieldClick(field.id); @@ -358,6 +359,7 @@ export const WorkflowEditActionFormBuilder = ({ { const { label, name } = getDefaultFormFieldSettings( diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowFormEmptyMessage.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowFormEmptyMessage.tsx index bf4f95fbf..7ffe4533b 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowFormEmptyMessage.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowFormEmptyMessage.tsx @@ -47,7 +47,10 @@ export const WorkflowFormEmptyMessage = () => { - + diff --git a/packages/twenty-front/src/pages/onboarding/CreateProfile.tsx b/packages/twenty-front/src/pages/onboarding/CreateProfile.tsx index 1975f6243..d407ac798 100644 --- a/packages/twenty-front/src/pages/onboarding/CreateProfile.tsx +++ b/packages/twenty-front/src/pages/onboarding/CreateProfile.tsx @@ -14,11 +14,12 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboardingStatus'; import { ProfilePictureUploader } from '@/settings/profile/components/ProfilePictureUploader'; +import { PageFocusId } from '@/types/PageFocusId'; import { PageHotkeyScope } from '@/types/PageHotkeyScope'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { TextInputV2 } from '@/ui/input/components/TextInputV2'; import { Modal } from '@/ui/layout/modal/components/Modal'; -import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; +import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement'; import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember'; import { ApolloError } from '@apollo/client'; import { Trans, useLingui } from '@lingui/react/macro'; @@ -148,15 +149,23 @@ export const CreateProfile = () => { const [isEditingMode, setIsEditingMode] = useState(false); - useScopedHotkeys( - Key.Enter, - () => { + const handleEnter = () => { + if (isEditingMode) { + onSubmit(getValues()); + } + }; + + useHotkeysOnFocusedElement({ + keys: Key.Enter, + callback: () => { if (isEditingMode) { onSubmit(getValues()); } }, - PageHotkeyScope.CreateProfile, - ); + focusId: PageFocusId.CreateProfile, + scope: PageHotkeyScope.CreateProfile, + dependencies: [handleEnter], + }); return ( diff --git a/packages/twenty-front/src/pages/onboarding/InviteTeam.tsx b/packages/twenty-front/src/pages/onboarding/InviteTeam.tsx index 81a957339..8114b8613 100644 --- a/packages/twenty-front/src/pages/onboarding/InviteTeam.tsx +++ b/packages/twenty-front/src/pages/onboarding/InviteTeam.tsx @@ -3,11 +3,12 @@ import { Title } from '@/auth/components/Title'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { calendarBookingPageIdState } from '@/client-config/states/calendarBookingPageIdState'; import { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboardingStatus'; +import { PageFocusId } from '@/types/PageFocusId'; import { PageHotkeyScope } from '@/types/PageHotkeyScope'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { TextInputV2 } from '@/ui/input/components/TextInputV2'; import { Modal } from '@/ui/layout/modal/components/Modal'; -import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; +import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { zodResolver } from '@hookform/resolvers/zod'; @@ -161,14 +162,15 @@ export const InviteTeam = () => { await onSubmit({ emails: [] }); }; - useScopedHotkeys( - [Key.Enter], - () => { + useHotkeysOnFocusedElement({ + keys: Key.Enter, + callback: () => { handleSubmit(onSubmit)(); }, - PageHotkeyScope.InviteTeam, - [handleSubmit], - ); + focusId: PageFocusId.InviteTeam, + scope: PageHotkeyScope.InviteTeam, + dependencies: [handleSubmit, onSubmit], + }); return ( diff --git a/packages/twenty-front/src/pages/onboarding/SyncEmails.tsx b/packages/twenty-front/src/pages/onboarding/SyncEmails.tsx index 5fb8bc508..2d8e15979 100644 --- a/packages/twenty-front/src/pages/onboarding/SyncEmails.tsx +++ b/packages/twenty-front/src/pages/onboarding/SyncEmails.tsx @@ -9,7 +9,6 @@ import { Title } from '@/auth/components/Title'; import { OnboardingSyncEmailsSettingsCard } from '@/onboarding/components/OnboardingSyncEmailsSettingsCard'; import { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboardingStatus'; import { PageHotkeyScope } from '@/types/PageHotkeyScope'; -import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { isGoogleCalendarEnabledState } from '@/client-config/states/isGoogleCalendarEnabledState'; import { isGoogleMessagingEnabledState } from '@/client-config/states/isGoogleMessagingEnabledState'; @@ -17,7 +16,9 @@ import { isMicrosoftCalendarEnabledState } from '@/client-config/states/isMicros import { isMicrosoftMessagingEnabledState } from '@/client-config/states/isMicrosoftMessagingEnabledState'; import { useTriggerApisOAuth } from '@/settings/accounts/hooks/useTriggerApiOAuth'; import { AppPath } from '@/types/AppPath'; +import { PageFocusId } from '@/types/PageFocusId'; import { Modal } from '@/ui/layout/modal/components/Modal'; +import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement'; import { ConnectedAccountProvider } from 'twenty-shared/types'; import { IconGoogle, IconMicrosoft } from 'twenty-ui/display'; import { MainButton } from 'twenty-ui/input'; @@ -95,14 +96,15 @@ export const SyncEmails = () => { const isMicrosoftProviderEnabled = isMicrosoftMessagingEnabled || isMicrosoftCalendarEnabled; - useScopedHotkeys( - [Key.Enter], - async () => { + useHotkeysOnFocusedElement({ + keys: Key.Enter, + callback: async () => { await continueWithoutSync(); }, - PageHotkeyScope.SyncEmail, - [continueWithoutSync], - ); + focusId: PageFocusId.SyncEmail, + scope: PageHotkeyScope.SyncEmail, + dependencies: [continueWithoutSync], + }); return ( diff --git a/packages/twenty-front/src/pages/settings/serverless-functions/SettingsServerlessFunctionsNew.tsx b/packages/twenty-front/src/pages/settings/serverless-functions/SettingsServerlessFunctionsNew.tsx index 565a97092..51facd258 100644 --- a/packages/twenty-front/src/pages/settings/serverless-functions/SettingsServerlessFunctionsNew.tsx +++ b/packages/twenty-front/src/pages/settings/serverless-functions/SettingsServerlessFunctionsNew.tsx @@ -5,15 +5,11 @@ import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBa import { SettingsServerlessFunctionNewForm } from '@/settings/serverless-functions/components/SettingsServerlessFunctionNewForm'; import { useCreateOneServerlessFunction } from '@/settings/serverless-functions/hooks/useCreateOneServerlessFunction'; import { ServerlessFunctionNewFormValues } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState'; -import { SettingsServerlessFunctionHotkeyScope } from '@/settings/serverless-functions/types/SettingsServerlessFunctionHotKeyScope'; import { SettingsPath } from '@/types/SettingsPath'; -import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useState } from 'react'; -import { Key } from 'ts-key-enum'; -import { useHotkeyScopeOnMount } from '~/hooks/useHotkeyScopeOnMount'; +import { isDefined } from 'twenty-shared/utils'; import { useNavigateSettings } from '~/hooks/useNavigateSettings'; import { getSettingsPath } from '~/utils/navigation/getSettingsPath'; -import { isDefined } from 'twenty-shared/utils'; export const SettingsServerlessFunctionsNew = () => { const navigate = useNavigateSettings(); @@ -50,28 +46,6 @@ export const SettingsServerlessFunctionsNew = () => { const canSave = !!formValues.name && createOneServerlessFunction; - useHotkeyScopeOnMount( - SettingsServerlessFunctionHotkeyScope.ServerlessFunctionNew, - ); - - useScopedHotkeys( - [Key.Enter], - () => { - if (canSave !== false) { - handleSave(); - } - }, - SettingsServerlessFunctionHotkeyScope.ServerlessFunctionNew, - [canSave], - ); - useScopedHotkeys( - [Key.Escape], - () => { - navigate(SettingsPath.ServerlessFunctions); - }, - SettingsServerlessFunctionHotkeyScope.ServerlessFunctionNew, - ); - return ( { const { parameters } = context; - const disableHotkeyInitialization = parameters.disableHotkeyInitialization; - return ( - {!disableHotkeyInitialization && ( - - )}