diff --git a/packages/twenty-front/src/hooks/useHotkeyScopeOnMount.ts b/packages/twenty-front/src/hooks/useHotkeyScopeOnMount.ts index e1e4c298c..a4418a67c 100644 --- a/packages/twenty-front/src/hooks/useHotkeyScopeOnMount.ts +++ b/packages/twenty-front/src/hooks/useHotkeyScopeOnMount.ts @@ -12,7 +12,9 @@ export const useHotkeyScopeOnMount = (hotkeyScope: string) => { } = usePreviousHotkeyScope(); useEffect(() => { - setHotkeyScopeAndMemorizePreviousScope(hotkeyScope); + setHotkeyScopeAndMemorizePreviousScope({ + scope: hotkeyScope, + }); return () => { goBackToPreviousHotkeyScope(); }; diff --git a/packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx b/packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx index 2a1dae154..bc7b9727d 100644 --- a/packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx +++ b/packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx @@ -353,9 +353,9 @@ export const ActivityRichTextEditor = ({ ); const handleBlockEditorFocus = () => { - setHotkeyScopeAndMemorizePreviousScope( - ActivityEditorHotkeyScope.ActivityBody, - ); + setHotkeyScopeAndMemorizePreviousScope({ + scope: ActivityEditorHotkeyScope.ActivityBody, + }); }; const handlerBlockEditorBlur = () => { diff --git a/packages/twenty-front/src/modules/activities/inline-cell/hooks/useOpenActivityTargetCellEditMode.ts b/packages/twenty-front/src/modules/activities/inline-cell/hooks/useOpenActivityTargetCellEditMode.ts index 6e4109d60..201e94e6a 100644 --- a/packages/twenty-front/src/modules/activities/inline-cell/hooks/useOpenActivityTargetCellEditMode.ts +++ b/packages/twenty-front/src/modules/activities/inline-cell/hooks/useOpenActivityTargetCellEditMode.ts @@ -84,9 +84,9 @@ export const useOpenActivityTargetCellEditMode = () => { ), }); - setHotkeyScopeAndMemorizePreviousScope( - MultipleRecordPickerHotkeyScope.MultipleRecordPicker, - ); + setHotkeyScopeAndMemorizePreviousScope({ + scope: MultipleRecordPickerHotkeyScope.MultipleRecordPicker, + }); }, [multipleRecordPickerPerformSearch, setHotkeyScopeAndMemorizePreviousScope], ); diff --git a/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useCommandMenu.test.tsx b/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useCommandMenu.test.tsx index 6c09a5580..f1ea6ccf6 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useCommandMenu.test.tsx +++ b/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useCommandMenu.test.tsx @@ -71,12 +71,13 @@ describe('useCommandMenu', () => { }); expect(result.current.isCommandMenuOpened).toBe(true); - expect(mockSetHotkeyScopeAndMemorizePreviousScope).toHaveBeenCalledWith( - CommandMenuHotkeyScope.CommandMenuFocused, - { + expect(mockSetHotkeyScopeAndMemorizePreviousScope).toHaveBeenCalledWith({ + scope: CommandMenuHotkeyScope.CommandMenuFocused, + memoizeKey: 'command-menu', + customScopes: { commandMenuOpen: true, }, - ); + }); act(() => { result.current.commandMenu.closeCommandMenu(); @@ -95,12 +96,13 @@ describe('useCommandMenu', () => { }); expect(result.current.isCommandMenuOpened).toBe(true); - expect(mockSetHotkeyScopeAndMemorizePreviousScope).toHaveBeenCalledWith( - CommandMenuHotkeyScope.CommandMenuFocused, - { + expect(mockSetHotkeyScopeAndMemorizePreviousScope).toHaveBeenCalledWith({ + scope: CommandMenuHotkeyScope.CommandMenuFocused, + memoizeKey: 'command-menu', + customScopes: { commandMenuOpen: true, }, - ); + }); act(() => { result.current.commandMenu.toggleCommandMenu(); diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts index fd5e57453..1ca2ef269 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts +++ b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts @@ -16,9 +16,7 @@ import { isCommandMenuOpenedState } from '../states/isCommandMenuOpenedState'; export const useCommandMenu = () => { const { navigateCommandMenu } = useNavigateCommandMenu(); const { closeAnyOpenDropdown } = useCloseAnyOpenDropdown(); - const { goBackToPreviousHotkeyScope } = usePreviousHotkeyScope( - COMMAND_MENU_COMPONENT_INSTANCE_ID, - ); + const { goBackToPreviousHotkeyScope } = usePreviousHotkeyScope(); const closeCommandMenu = useRecoilCallback( ({ set, snapshot }) => @@ -32,7 +30,7 @@ export const useCommandMenu = () => { set(isCommandMenuClosingState, true); set(isDragSelectionStartEnabledState, true); closeAnyOpenDropdown(); - goBackToPreviousHotkeyScope(); + goBackToPreviousHotkeyScope(COMMAND_MENU_COMPONENT_INSTANCE_ID); } }, [closeAnyOpenDropdown, goBackToPreviousHotkeyScope], diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuHotKeys.ts b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuHotKeys.ts index 574a55096..3c1ca5662 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuHotKeys.ts +++ b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuHotKeys.ts @@ -9,7 +9,7 @@ import { CommandMenuHotkeyScope } from '@/command-menu/types/CommandMenuHotkeySc import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; import { useKeyboardShortcutMenu } from '@/keyboard-shortcut-menu/hooks/useKeyboardShortcutMenu'; -import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; +import { useGlobalHotkeys } from '@/ui/utilities/hotkey/hooks/useGlobalHotkeys'; import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { isNonEmptyString } from '@sniptt/guards'; @@ -36,21 +36,23 @@ export const useCommandMenuHotKeys = () => { COMMAND_MENU_COMPONENT_INSTANCE_ID, ); - useScopedHotkeys( + useGlobalHotkeys( 'ctrl+k,meta+k', () => { closeKeyboardShortcutMenu(); toggleCommandMenu(); }, + true, AppHotkeyScope.CommandMenu, [closeKeyboardShortcutMenu, toggleCommandMenu], ); - useScopedHotkeys( + useGlobalHotkeys( ['/'], () => { openRecordsSearchPage(); }, + false, AppHotkeyScope.KeyboardShortcutMenu, [openRecordsSearchPage], { @@ -58,16 +60,17 @@ export const useCommandMenuHotKeys = () => { }, ); - useScopedHotkeys( + useGlobalHotkeys( [Key.Escape], () => { goBackFromCommandMenu(); }, + true, CommandMenuHotkeyScope.CommandMenuFocused, [goBackFromCommandMenu], ); - useScopedHotkeys( + useGlobalHotkeys( [Key.Backspace, Key.Delete], () => { if (isNonEmptyString(commandMenuSearch)) { @@ -88,6 +91,7 @@ export const useCommandMenuHotKeys = () => { goBackFromCommandMenu(); } }, + true, CommandMenuHotkeyScope.CommandMenuFocused, [ commandMenuPage, diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useNavigateCommandMenu.ts b/packages/twenty-front/src/modules/command-menu/hooks/useNavigateCommandMenu.ts index c92fb987d..5d76ae1f2 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/useNavigateCommandMenu.ts +++ b/packages/twenty-front/src/modules/command-menu/hooks/useNavigateCommandMenu.ts @@ -27,9 +27,7 @@ export type CommandMenuNavigationStackItem = { }; export const useNavigateCommandMenu = () => { - const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope( - COMMAND_MENU_COMPONENT_INSTANCE_ID, - ); + const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope(); const { copyContextStoreStates } = useCopyContextStoreStates(); @@ -55,12 +53,13 @@ export const useNavigateCommandMenu = () => { return; } - setHotkeyScopeAndMemorizePreviousScope( - CommandMenuHotkeyScope.CommandMenuFocused, - { + setHotkeyScopeAndMemorizePreviousScope({ + scope: CommandMenuHotkeyScope.CommandMenuFocused, + customScopes: { commandMenuOpen: true, }, - ); + memoizeKey: COMMAND_MENU_COMPONENT_INSTANCE_ID, + }); copyContextStoreStates({ instanceIdToCopyFrom: MAIN_CONTEXT_STORE_INSTANCE_ID, diff --git a/packages/twenty-front/src/modules/keyboard-shortcut-menu/components/KeyboardShortcutMenu.tsx b/packages/twenty-front/src/modules/keyboard-shortcut-menu/components/KeyboardShortcutMenu.tsx index 3c9c043e1..bcdbbb4e5 100644 --- a/packages/twenty-front/src/modules/keyboard-shortcut-menu/components/KeyboardShortcutMenu.tsx +++ b/packages/twenty-front/src/modules/keyboard-shortcut-menu/components/KeyboardShortcutMenu.tsx @@ -1,13 +1,13 @@ import { useRecoilValue } from 'recoil'; import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; -import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope'; import { useKeyboardShortcutMenu } from '../hooks/useKeyboardShortcutMenu'; import { isKeyboardShortcutMenuOpenedState } from '../states/isKeyboardShortcutMenuOpenedState'; import { KeyboardShortcutMenuOpenContent } from '@/keyboard-shortcut-menu/components/KeyboardShortcutMenuOpenContent'; +import { useGlobalHotkeys } from '@/ui/utilities/hotkey/hooks/useGlobalHotkeys'; export const KeyboardShortcutMenu = () => { const { toggleKeyboardShortcutMenu } = useKeyboardShortcutMenu(); @@ -16,12 +16,13 @@ export const KeyboardShortcutMenu = () => { ); const { closeCommandMenu } = useCommandMenu(); - useScopedHotkeys( + useGlobalHotkeys( 'shift+?,meta+?', () => { closeCommandMenu(); toggleKeyboardShortcutMenu(); }, + true, AppHotkeyScope.KeyboardShortcutMenu, [toggleKeyboardShortcutMenu], ); 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 ae68bdc4e..9155fed53 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 @@ -2,11 +2,11 @@ import { Key } from 'ts-key-enum'; import { KEYBOARD_SHORTCUTS_GENERAL } from '@/keyboard-shortcut-menu/constants/KeyboardShortcutsGeneral'; import { KEYBOARD_SHORTCUTS_TABLE } from '@/keyboard-shortcut-menu/constants/KeyboardShortcutsTable'; -import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope'; import { useKeyboardShortcutMenu } from '../hooks/useKeyboardShortcutMenu'; +import { useGlobalHotkeys } from '@/ui/utilities/hotkey/hooks/useGlobalHotkeys'; import { KeyboardMenuDialog } from './KeyboardShortcutMenuDialog'; import { KeyboardMenuGroup } from './KeyboardShortcutMenuGroup'; import { KeyboardMenuItem } from './KeyboardShortcutMenuItem'; @@ -15,11 +15,12 @@ export const KeyboardShortcutMenuOpenContent = () => { const { toggleKeyboardShortcutMenu, closeKeyboardShortcutMenu } = useKeyboardShortcutMenu(); - useScopedHotkeys( + useGlobalHotkeys( [Key.Escape], () => { closeKeyboardShortcutMenu(); }, + false, AppHotkeyScope.KeyboardShortcutMenuOpen, [closeKeyboardShortcutMenu], ); 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 d38b3b577..292730ea0 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 @@ -48,18 +48,18 @@ describe('useKeyboardShortcutMenu', () => { result.current.toggleKeyboardShortcutMenu(); }); - expect(mockSetHotkeyScopeAndMemorizePreviousScope).toHaveBeenCalledWith( - AppHotkeyScope.KeyboardShortcutMenu, - ); + expect(mockSetHotkeyScopeAndMemorizePreviousScope).toHaveBeenCalledWith({ + scope: AppHotkeyScope.KeyboardShortcutMenu, + }); expect(result.current.isKeyboardShortcutMenuOpened).toBe(true); act(() => { result.current.toggleKeyboardShortcutMenu(); }); - expect(mockSetHotkeyScopeAndMemorizePreviousScope).toHaveBeenCalledWith( - AppHotkeyScope.KeyboardShortcutMenu, - ); + expect(mockSetHotkeyScopeAndMemorizePreviousScope).toHaveBeenCalledWith({ + scope: AppHotkeyScope.KeyboardShortcutMenu, + }); expect(result.current.isKeyboardShortcutMenuOpened).toBe(false); }); @@ -69,9 +69,9 @@ describe('useKeyboardShortcutMenu', () => { result.current.openKeyboardShortcutMenu(); }); - expect(mockSetHotkeyScopeAndMemorizePreviousScope).toHaveBeenCalledWith( - AppHotkeyScope.KeyboardShortcutMenu, - ); + expect(mockSetHotkeyScopeAndMemorizePreviousScope).toHaveBeenCalledWith({ + scope: AppHotkeyScope.KeyboardShortcutMenu, + }); expect(result.current.isKeyboardShortcutMenuOpened).toBe(true); act(() => { 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 0d568a473..bd3286d7a 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 @@ -15,9 +15,9 @@ export const useKeyboardShortcutMenu = () => { ({ set }) => () => { set(isKeyboardShortcutMenuOpenedState, true); - setHotkeyScopeAndMemorizePreviousScope( - AppHotkeyScope.KeyboardShortcutMenu, - ); + setHotkeyScopeAndMemorizePreviousScope({ + scope: AppHotkeyScope.KeyboardShortcutMenu, + }); }, [setHotkeyScopeAndMemorizePreviousScope], ); 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 6ceebcdc1..f8eef8fea 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 @@ -52,7 +52,9 @@ export const RecordBoardHotkeyEffect = () => { useScopedHotkeys( Key.ArrowLeft, () => { - setHotkeyScopeAndMemorizePreviousScope(BoardHotkeyScope.BoardFocus); + setHotkeyScopeAndMemorizePreviousScope({ + scope: BoardHotkeyScope.BoardFocus, + }); move('left'); }, RecordIndexHotkeyScope.RecordIndex, @@ -61,7 +63,9 @@ export const RecordBoardHotkeyEffect = () => { useScopedHotkeys( Key.ArrowRight, () => { - setHotkeyScopeAndMemorizePreviousScope(BoardHotkeyScope.BoardFocus); + setHotkeyScopeAndMemorizePreviousScope({ + scope: BoardHotkeyScope.BoardFocus, + }); move('right'); }, RecordIndexHotkeyScope.RecordIndex, @@ -70,7 +74,9 @@ export const RecordBoardHotkeyEffect = () => { useScopedHotkeys( Key.ArrowUp, () => { - setHotkeyScopeAndMemorizePreviousScope(BoardHotkeyScope.BoardFocus); + setHotkeyScopeAndMemorizePreviousScope({ + scope: BoardHotkeyScope.BoardFocus, + }); move('up'); }, RecordIndexHotkeyScope.RecordIndex, @@ -79,7 +85,9 @@ export const RecordBoardHotkeyEffect = () => { useScopedHotkeys( Key.ArrowDown, () => { - setHotkeyScopeAndMemorizePreviousScope(BoardHotkeyScope.BoardFocus); + setHotkeyScopeAndMemorizePreviousScope({ + scope: BoardHotkeyScope.BoardFocus, + }); move('down'); }, RecordIndexHotkeyScope.RecordIndex, 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 13b2f4989..b0d70d287 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 @@ -79,12 +79,12 @@ export const RecordBoardColumnHeader = () => { const handleBoardColumnMenuOpen = () => { setIsBoardColumnMenuOpen(true); - setHotkeyScopeAndMemorizePreviousScope( - RecordBoardColumnHotkeyScope.BoardColumn, - { + setHotkeyScopeAndMemorizePreviousScope({ + scope: RecordBoardColumnHotkeyScope.BoardColumn, + customScopes: { goto: false, }, - ); + }); }; const handleBoardColumnMenuClose = () => { 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 60dbb10be..ec646f543 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 @@ -60,9 +60,9 @@ export const FormFieldInputInnerContainer = forwardRef( onFocus?.(e); if (!preventSetHotkeyScope) { - setHotkeyScopeAndMemorizePreviousScope( - FormFieldInputHotKeyScope.FormFieldInput, - ); + setHotkeyScopeAndMemorizePreviousScope({ + scope: FormFieldInputHotKeyScope.FormFieldInput, + }); } }; 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 fde125c70..28ae0760c 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 @@ -119,7 +119,9 @@ export const FormMultiSelectFieldInput = ({ editingMode: 'edit', }); - setHotkeyScopeAndMemorizePreviousScope(hotkeyScope); + setHotkeyScopeAndMemorizePreviousScope({ + scope: hotkeyScope, + }); }; const onOptionSelected = (value: FieldMultiSelectValue) => { diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/useOpenFieldInputEditMode.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/useOpenFieldInputEditMode.ts index 1f3ad72f1..889b6185f 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/hooks/useOpenFieldInputEditMode.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/useOpenFieldInputEditMode.ts @@ -31,9 +31,7 @@ export const useOpenFieldInputEditMode = () => { const { openActivityTargetCellEditMode } = useOpenActivityTargetCellEditMode(); - const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope( - INLINE_CELL_HOTKEY_SCOPE_MEMOIZE_KEY, - ); + const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope(); const openFieldInput = useRecoilCallback( ({ snapshot }) => @@ -105,10 +103,11 @@ export const useOpenFieldInputEditMode = () => { } } - setHotkeyScopeAndMemorizePreviousScope( - DEFAULT_CELL_SCOPE.scope, - DEFAULT_CELL_SCOPE.customScopes, - ); + setHotkeyScopeAndMemorizePreviousScope({ + scope: DEFAULT_CELL_SCOPE.scope, + customScopes: DEFAULT_CELL_SCOPE.customScopes, + memoizeKey: INLINE_CELL_HOTKEY_SCOPE_MEMOIZE_KEY, + }); }, [ openActivityTargetCellEditMode, diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useOpenRelationFromManyFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useOpenRelationFromManyFieldInput.tsx index ea45fbaae..0f17035ef 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useOpenRelationFromManyFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useOpenRelationFromManyFieldInput.tsx @@ -88,9 +88,9 @@ export const useOpenRelationFromManyFieldInput = () => { forcePickableMorphItems: pickableMorphItems, }); - setHotkeyScopeAndMemorizePreviousScope( - MultipleRecordPickerHotkeyScope.MultipleRecordPicker, - ); + setHotkeyScopeAndMemorizePreviousScope({ + scope: MultipleRecordPickerHotkeyScope.MultipleRecordPicker, + }); }, [performSearch, setHotkeyScopeAndMemorizePreviousScope], ); diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useOpenRelationToOneFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useOpenRelationToOneFieldInput.tsx index 27534386f..2428cdc62 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useOpenRelationToOneFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useOpenRelationToOneFieldInput.tsx @@ -34,9 +34,9 @@ export const useOpenRelationToOneFieldInput = () => { ); } - setHotkeyScopeAndMemorizePreviousScope( - SingleRecordPickerHotkeyScope.SingleRecordPicker, - ); + setHotkeyScopeAndMemorizePreviousScope({ + scope: SingleRecordPickerHotkeyScope.SingleRecordPicker, + }); }, [setHotkeyScopeAndMemorizePreviousScope], ); diff --git a/packages/twenty-front/src/modules/object-record/record-inline-cell/hooks/useInlineCell.ts b/packages/twenty-front/src/modules/object-record/record-inline-cell/hooks/useInlineCell.ts index 32741a62f..84756f7e2 100644 --- a/packages/twenty-front/src/modules/object-record/record-inline-cell/hooks/useInlineCell.ts +++ b/packages/twenty-front/src/modules/object-record/record-inline-cell/hooks/useInlineCell.ts @@ -35,9 +35,7 @@ export const useInlineCell = ( const { goBackToPreviousDropdownFocusId } = useGoBackToPreviousDropdownFocusId(); - const { goBackToPreviousHotkeyScope } = usePreviousHotkeyScope( - INLINE_CELL_HOTKEY_SCOPE_MEMOIZE_KEY, - ); + const { goBackToPreviousHotkeyScope } = usePreviousHotkeyScope(); const initFieldInputDraftValue = useInitDraftValueV2(); @@ -45,7 +43,7 @@ export const useInlineCell = ( onCloseEditMode?.(); setIsInlineCellInEditMode(false); - goBackToPreviousHotkeyScope(); + goBackToPreviousHotkeyScope(INLINE_CELL_HOTKEY_SCOPE_MEMOIZE_KEY); goBackToPreviousDropdownFocusId(); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useMapKeyboardToFocus.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useMapKeyboardToFocus.ts index 6cbb1edaf..0b1ef505d 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/useMapKeyboardToFocus.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useMapKeyboardToFocus.ts @@ -31,7 +31,9 @@ export const useMapKeyboardToFocus = (recordTableId?: string) => { useScopedHotkeys( [Key.ArrowUp], () => { - setHotkeyScopeAndMemorizePreviousScope(TableHotkeyScope.TableFocus); + setHotkeyScopeAndMemorizePreviousScope({ + scope: TableHotkeyScope.TableFocus, + }); move('up'); }, RecordIndexHotkeyScope.RecordIndex, @@ -41,7 +43,9 @@ export const useMapKeyboardToFocus = (recordTableId?: string) => { useScopedHotkeys( [Key.ArrowDown], () => { - setHotkeyScopeAndMemorizePreviousScope(TableHotkeyScope.TableFocus); + setHotkeyScopeAndMemorizePreviousScope({ + scope: TableHotkeyScope.TableFocus, + }); move('down'); }, RecordIndexHotkeyScope.RecordIndex, diff --git a/packages/twenty-front/src/modules/object-record/record-title-cell/components/RecordTitleCellTextFieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-title-cell/components/RecordTitleCellTextFieldDisplay.tsx index 25a7a0d87..cb7a15b9a 100644 --- a/packages/twenty-front/src/modules/object-record/record-title-cell/components/RecordTitleCellTextFieldDisplay.tsx +++ b/packages/twenty-front/src/modules/object-record/record-title-cell/components/RecordTitleCellTextFieldDisplay.tsx @@ -42,16 +42,15 @@ export const RecordTitleCellSingleTextDisplayMode = () => { const { openInlineCell } = useInlineCell(); - const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope( - INLINE_CELL_HOTKEY_SCOPE_MEMOIZE_KEY, - ); + const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope(); return ( { - setHotkeyScopeAndMemorizePreviousScope( - TitleInputHotkeyScope.TitleInput, - ); + setHotkeyScopeAndMemorizePreviousScope({ + scope: TitleInputHotkeyScope.TitleInput, + memoizeKey: INLINE_CELL_HOTKEY_SCOPE_MEMOIZE_KEY, + }); openInlineCell(); }} > diff --git a/packages/twenty-front/src/modules/object-record/record-title-cell/components/RecordTitleFullNameFieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-title-cell/components/RecordTitleFullNameFieldDisplay.tsx index cab5a5b09..963111b5f 100644 --- a/packages/twenty-front/src/modules/object-record/record-title-cell/components/RecordTitleFullNameFieldDisplay.tsx +++ b/packages/twenty-front/src/modules/object-record/record-title-cell/components/RecordTitleFullNameFieldDisplay.tsx @@ -45,15 +45,14 @@ export const RecordTitleFullNameFieldDisplay = () => { .join(' ') .trim(); - const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope( - INLINE_CELL_HOTKEY_SCOPE_MEMOIZE_KEY, - ); + const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope(); return ( { - setHotkeyScopeAndMemorizePreviousScope( - TitleInputHotkeyScope.TitleInput, - ); + setHotkeyScopeAndMemorizePreviousScope({ + scope: TitleInputHotkeyScope.TitleInput, + memoizeKey: INLINE_CELL_HOTKEY_SCOPE_MEMOIZE_KEY, + }); openInlineCell(); }} > diff --git a/packages/twenty-front/src/modules/object-record/record-title-cell/hooks/useRecordTitleCell.tsx b/packages/twenty-front/src/modules/object-record/record-title-cell/hooks/useRecordTitleCell.tsx index 8309dc458..a77615ebb 100644 --- a/packages/twenty-front/src/modules/object-record/record-title-cell/hooks/useRecordTitleCell.tsx +++ b/packages/twenty-front/src/modules/object-record/record-title-cell/hooks/useRecordTitleCell.tsx @@ -18,7 +18,7 @@ export const useRecordTitleCell = () => { const { setHotkeyScopeAndMemorizePreviousScope, goBackToPreviousHotkeyScope, - } = usePreviousHotkeyScope(INLINE_CELL_HOTKEY_SCOPE_MEMOIZE_KEY); + } = usePreviousHotkeyScope(); const closeRecordTitleCell = useRecoilCallback( ({ set }) => @@ -38,7 +38,7 @@ export const useRecordTitleCell = () => { false, ); - goBackToPreviousHotkeyScope(); + goBackToPreviousHotkeyScope(INLINE_CELL_HOTKEY_SCOPE_MEMOIZE_KEY); goBackToPreviousDropdownFocusId(); }, @@ -61,14 +61,16 @@ export const useRecordTitleCell = () => { customEditHotkeyScopeForField?: HotkeyScope; }) => { if (isDefined(customEditHotkeyScopeForField)) { - setHotkeyScopeAndMemorizePreviousScope( - customEditHotkeyScopeForField.scope, - customEditHotkeyScopeForField.customScopes, - ); + setHotkeyScopeAndMemorizePreviousScope({ + scope: customEditHotkeyScopeForField.scope, + customScopes: customEditHotkeyScopeForField.customScopes, + memoizeKey: INLINE_CELL_HOTKEY_SCOPE_MEMOIZE_KEY, + }); } else { - setHotkeyScopeAndMemorizePreviousScope( - TitleInputHotkeyScope.TitleInput, - ); + setHotkeyScopeAndMemorizePreviousScope({ + scope: TitleInputHotkeyScope.TitleInput, + memoizeKey: INLINE_CELL_HOTKEY_SCOPE_MEMOIZE_KEY, + }); } const recordTitleCellId = getRecordTitleCellId( diff --git a/packages/twenty-front/src/modules/ui/feedback/dialog-manager/components/Dialog.tsx b/packages/twenty-front/src/modules/ui/feedback/dialog-manager/components/Dialog.tsx index 893958fdc..9c2ea43fc 100644 --- a/packages/twenty-front/src/modules/ui/feedback/dialog-manager/components/Dialog.tsx +++ b/packages/twenty-front/src/modules/ui/feedback/dialog-manager/components/Dialog.tsx @@ -1,11 +1,13 @@ import styled from '@emotion/styled'; import { motion } from 'framer-motion'; -import { useCallback } from 'react'; import { Key } from 'ts-key-enum'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; +import { DIALOG_LISTENER_ID } from '@/ui/feedback/dialog-manager/constants/DialogListenerId'; import { RootStackingContextZIndices } from '@/ui/layout/constants/RootStackingContextZIndices'; +import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; +import { useRef } from 'react'; import { isDefined } from 'twenty-shared/utils'; import { Button } from 'twenty-ui/input'; import { DialogHotkeyScope } from '../types/DialogHotkeyScope'; @@ -69,7 +71,6 @@ export type DialogProps = React.ComponentPropsWithoutRef & { title?: string; message?: string; buttons?: DialogButtonOptions[]; - allowDismiss?: boolean; children?: React.ReactNode; className?: string; onClose?: () => void; @@ -79,16 +80,11 @@ export const Dialog = ({ title, message, buttons = [], - allowDismiss = true, children, className, onClose, id, }: DialogProps) => { - const closeSnackbar = useCallback(() => { - onClose && onClose(); - }, [onClose]); - const dialogVariants = { open: { opacity: 1 }, closed: { opacity: 0 }, @@ -108,7 +104,7 @@ export const Dialog = ({ if (isDefined(confirmButton)) { confirmButton?.onClick?.(event); - closeSnackbar(); + onClose?.(); } }, DialogHotkeyScope.Dialog, @@ -119,30 +115,35 @@ export const Dialog = ({ Key.Escape, (event: KeyboardEvent) => { event.preventDefault(); - closeSnackbar(); + onClose?.(); }, DialogHotkeyScope.Dialog, [], ); + const dialogRef = useRef(null); + + useListenClickOutside({ + refs: [dialogRef], + callback: () => { + onClose?.(); + }, + listenerId: DIALOG_LISTENER_ID, + }); + return ( { - if (allowDismiss) { - e.stopPropagation(); - closeSnackbar(); - } - }} className={className} > {title && {title}} {message && {message}} @@ -150,8 +151,8 @@ export const Dialog = ({ {buttons.map(({ accent, onClick, role, title: key, variant }) => ( { + onClose?.(); onClick?.(event); - closeSnackbar(); }} fullWidth={true} variant={variant ?? 'secondary'} diff --git a/packages/twenty-front/src/modules/ui/feedback/dialog-manager/components/DialogManager.tsx b/packages/twenty-front/src/modules/ui/feedback/dialog-manager/components/DialogManager.tsx index cf8040f51..b1c9e5ca5 100644 --- a/packages/twenty-front/src/modules/ui/feedback/dialog-manager/components/DialogManager.tsx +++ b/packages/twenty-front/src/modules/ui/feedback/dialog-manager/components/DialogManager.tsx @@ -15,6 +15,7 @@ export const DialogManager = ({ children }: React.PropsWithChildren) => { {dialogInternal.queue.map(({ buttons, children, id, message, title }) => ( closeDialog(id)} /> 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 b4f85c41d..9787bccc6 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 @@ -2,6 +2,7 @@ import { useEffect } from 'react'; import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; +import { DIALOG_MANAGER_HOTKEY_SCOPE_MEMOIZE_KEY } from '@/ui/feedback/dialog-manager/constants/DialogManagerHotkeyScopeMemoizeKey'; import { useDialogManagerScopedStates } from '../hooks/internal/useDialogManagerScopedStates'; import { DialogHotkeyScope } from '../types/DialogHotkeyScope'; @@ -15,7 +16,10 @@ export const DialogManagerEffect = () => { return; } - setHotkeyScopeAndMemorizePreviousScope(DialogHotkeyScope.Dialog); + setHotkeyScopeAndMemorizePreviousScope({ + scope: DialogHotkeyScope.Dialog, + memoizeKey: DIALOG_MANAGER_HOTKEY_SCOPE_MEMOIZE_KEY, + }); }, [dialogInternal.queue, setHotkeyScopeAndMemorizePreviousScope]); return <>; diff --git a/packages/twenty-front/src/modules/ui/feedback/dialog-manager/constants/DialogListenerId.ts b/packages/twenty-front/src/modules/ui/feedback/dialog-manager/constants/DialogListenerId.ts new file mode 100644 index 000000000..9b0ac2086 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/feedback/dialog-manager/constants/DialogListenerId.ts @@ -0,0 +1 @@ +export const DIALOG_LISTENER_ID = 'DIALOG_LISTENER_ID'; diff --git a/packages/twenty-front/src/modules/ui/feedback/dialog-manager/constants/DialogManagerHotkeyScopeMemoizeKey.ts b/packages/twenty-front/src/modules/ui/feedback/dialog-manager/constants/DialogManagerHotkeyScopeMemoizeKey.ts new file mode 100644 index 000000000..21e4fddcb --- /dev/null +++ b/packages/twenty-front/src/modules/ui/feedback/dialog-manager/constants/DialogManagerHotkeyScopeMemoizeKey.ts @@ -0,0 +1 @@ +export const DIALOG_MANAGER_HOTKEY_SCOPE_MEMOIZE_KEY = 'dialog-manager'; 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 84ce4187e..251bc233c 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 @@ -4,6 +4,7 @@ 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 { DialogManagerScopeInternalContext } from '../scopes/scope-internal-context/DialogManagerScopeInternalContext'; import { dialogInternalScopedState } from '../states/dialogInternalScopedState'; import { DialogOptions } from '../types/DialogOptions'; @@ -25,9 +26,10 @@ export const useDialogManager = (props?: useDialogManagerProps) => { (id: string) => { set(dialogInternalScopedState({ scopeId: scopeId }), (prevState) => ({ ...prevState, - queue: prevState.queue.filter((snackBar) => snackBar.id !== id), + queue: prevState.queue.filter((dialog) => dialog.id !== id), })); - goBackToPreviousHotkeyScope(); + + goBackToPreviousHotkeyScope(DIALOG_MANAGER_HOTKEY_SCOPE_MEMOIZE_KEY); }, [goBackToPreviousHotkeyScope, scopeId], ); diff --git a/packages/twenty-front/src/modules/ui/input/components/AutosizeTextInput.tsx b/packages/twenty-front/src/modules/ui/input/components/AutosizeTextInput.tsx index 3e50d250a..46b9afd86 100644 --- a/packages/twenty-front/src/modules/ui/input/components/AutosizeTextInput.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/AutosizeTextInput.tsx @@ -7,9 +7,9 @@ import { Key } from 'ts-key-enum'; import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; -import { InputHotkeyScope } from '../types/InputHotkeyScope'; -import { Button, RoundedIconButton } from 'twenty-ui/input'; import { IconArrowRight } from 'twenty-ui/display'; +import { Button, RoundedIconButton } from 'twenty-ui/input'; +import { InputHotkeyScope } from '../types/InputHotkeyScope'; const MAX_ROWS = 5; @@ -197,7 +197,9 @@ export const AutosizeTextInput = ({ const handleFocus = () => { onFocus?.(); setIsFocused(true); - setHotkeyScopeAndMemorizePreviousScope(InputHotkeyScope.TextInput); + setHotkeyScopeAndMemorizePreviousScope({ + scope: InputHotkeyScope.TextInput, + }); }; const handleBlur = () => { diff --git a/packages/twenty-front/src/modules/ui/input/components/IconPicker.tsx b/packages/twenty-front/src/modules/ui/input/components/IconPicker.tsx index aadf5c583..1ef713560 100644 --- a/packages/twenty-front/src/modules/ui/input/components/IconPicker.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/IconPicker.tsx @@ -197,9 +197,9 @@ export const IconPicker = ({
{ - setHotkeyScopeAndMemorizePreviousScope( - IconPickerHotkeyScope.IconPicker, - ); + setHotkeyScopeAndMemorizePreviousScope({ + scope: IconPickerHotkeyScope.IconPicker, + }); }} onMouseLeave={goBackToPreviousHotkeyScope} > diff --git a/packages/twenty-front/src/modules/ui/input/components/TextArea.tsx b/packages/twenty-front/src/modules/ui/input/components/TextArea.tsx index 4a9a42986..e2af0665b 100644 --- a/packages/twenty-front/src/modules/ui/input/components/TextArea.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/TextArea.tsx @@ -3,9 +3,9 @@ import { FocusEventHandler, useId } from 'react'; import TextareaAutosize from 'react-textarea-autosize'; import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; +import { RGBA } from 'twenty-ui/theme'; import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly'; import { InputHotkeyScope } from '../types/InputHotkeyScope'; -import { RGBA } from 'twenty-ui/theme'; const MAX_ROWS = 5; @@ -89,7 +89,9 @@ export const TextArea = ({ } = usePreviousHotkeyScope(); const handleFocus: FocusEventHandler = () => { - setHotkeyScopeAndMemorizePreviousScope(InputHotkeyScope.TextInput); + setHotkeyScopeAndMemorizePreviousScope({ + scope: InputHotkeyScope.TextInput, + }); }; const handleBlur: FocusEventHandler = () => { diff --git a/packages/twenty-front/src/modules/ui/input/components/TextInput.tsx b/packages/twenty-front/src/modules/ui/input/components/TextInput.tsx index 9262451f0..d9b1e298c 100644 --- a/packages/twenty-front/src/modules/ui/input/components/TextInput.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/TextInput.tsx @@ -55,7 +55,9 @@ export const TextInput = ({ setIsFocused(true); if (!disableHotkeys) { - setHotkeyScopeAndMemorizePreviousScope(InputHotkeyScope.TextInput); + setHotkeyScopeAndMemorizePreviousScope({ + scope: InputHotkeyScope.TextInput, + }); } }; diff --git a/packages/twenty-front/src/modules/ui/input/components/TitleInput.tsx b/packages/twenty-front/src/modules/ui/input/components/TitleInput.tsx index 53eb5d6d2..c6c544f6d 100644 --- a/packages/twenty-front/src/modules/ui/input/components/TitleInput.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/TitleInput.tsx @@ -170,7 +170,9 @@ export const TitleInput = ({ onClick={() => { if (!disabled) { setIsOpened(true); - setHotkeyScopeAndMemorizePreviousScope(hotkeyScope); + setHotkeyScopeAndMemorizePreviousScope({ + scope: hotkeyScope, + }); } }} > diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useDropdown.ts b/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useDropdown.ts index 393bbe56a..2835fcad6 100644 --- a/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useDropdown.ts +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useDropdown.ts @@ -65,10 +65,10 @@ export const useDropdown = (dropdownId?: string) => { dropdownHotkeyScopeFromProps ?? dropdownHotkeyScope; if (isDefined(dropdownHotkeyScopeForOpening)) { - setHotkeyScopeAndMemorizePreviousScope( - dropdownHotkeyScopeForOpening.scope, - dropdownHotkeyScopeForOpening.customScopes, - ); + setHotkeyScopeAndMemorizePreviousScope({ + scope: dropdownHotkeyScopeForOpening.scope, + customScopes: dropdownHotkeyScopeForOpening.customScopes, + }); } } }, diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useDropdownV2.ts b/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useDropdownV2.ts index e35b2e435..9cefe8f11 100644 --- a/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useDropdownV2.ts +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useDropdownV2.ts @@ -56,15 +56,15 @@ export const useDropdownV2 = () => { ); if (isDefined(customHotkeyScope)) { - setHotkeyScopeAndMemorizePreviousScope( - customHotkeyScope.scope, - customHotkeyScope.customScopes, - ); + setHotkeyScopeAndMemorizePreviousScope({ + scope: customHotkeyScope.scope, + customScopes: customHotkeyScope.customScopes, + }); } else if (isDefined(dropdownHotkeyScope)) { - setHotkeyScopeAndMemorizePreviousScope( - dropdownHotkeyScope.scope, - dropdownHotkeyScope.customScopes, - ); + setHotkeyScopeAndMemorizePreviousScope({ + scope: dropdownHotkeyScope.scope, + customScopes: dropdownHotkeyScope.customScopes, + }); } }, [setHotkeyScopeAndMemorizePreviousScope], diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useOpenDropdownFromOutside.ts b/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useOpenDropdownFromOutside.ts index 8ea2bae6d..7e1c98ab1 100644 --- a/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useOpenDropdownFromOutside.ts +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useOpenDropdownFromOutside.ts @@ -19,7 +19,9 @@ export const useOpenDropdownFromOutside = () => { ); setActiveDropdownFocusIdAndMemorizePrevious(dropdownId); - setHotkeyScopeAndMemorizePreviousScope(dropdownId); + setHotkeyScopeAndMemorizePreviousScope({ + scope: dropdownId, + }); set(dropdownOpenState, true); }; diff --git a/packages/twenty-front/src/modules/ui/layout/modal/components/ModalHotkeysAndClickOutsideEffect.tsx b/packages/twenty-front/src/modules/ui/layout/modal/components/ModalHotkeysAndClickOutsideEffect.tsx index cbb9e649d..18f8df02a 100644 --- a/packages/twenty-front/src/modules/ui/layout/modal/components/ModalHotkeysAndClickOutsideEffect.tsx +++ b/packages/twenty-front/src/modules/ui/layout/modal/components/ModalHotkeysAndClickOutsideEffect.tsx @@ -1,6 +1,6 @@ import { ModalHotkeyScope } from '@/ui/layout/modal/components/types/ModalHotkeyScope'; import { MODAL_CLICK_OUTSIDE_LISTENER_EXCLUDED_CLASS_NAME } from '@/ui/layout/modal/constants/ModalClickOutsideListenerExcludedClassName'; -import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; +import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { Key } from 'ts-key-enum'; @@ -19,27 +19,36 @@ export const ModalHotkeysAndClickOutsideEffect = ({ onClose, modalId, }: ModalHotkeysAndClickOutsideEffectProps) => { - useScopedHotkeys( - [Key.Enter], - () => { + useHotkeysOnFocusedElement({ + keys: [Key.Enter], + callback: () => { onEnter?.(); }, - ModalHotkeyScope.ModalFocus, - ); + focusId: modalId, + // TODO: Remove this once we've migrated hotkey scopes to the new api + scope: ModalHotkeyScope.ModalFocus, + dependencies: [onEnter], + }); - useScopedHotkeys( - [Key.Escape], - () => { + useHotkeysOnFocusedElement({ + keys: [Key.Escape], + callback: () => { if (isClosable && onClose !== undefined) { onClose(); } }, - ModalHotkeyScope.ModalFocus, - ); + focusId: modalId, + // TODO: Remove this once we've migrated hotkey scopes to the new api + scope: ModalHotkeyScope.ModalFocus, + dependencies: [isClosable, onClose], + }); useListenClickOutside({ refs: [modalRef], - excludeClassNames: [MODAL_CLICK_OUTSIDE_LISTENER_EXCLUDED_CLASS_NAME], + excludeClassNames: [ + MODAL_CLICK_OUTSIDE_LISTENER_EXCLUDED_CLASS_NAME, + 'dialog-manager-dialog', + ], listenerId: `MODAL_CLICK_OUTSIDE_LISTENER_ID_${modalId}`, callback: () => { if (isClosable && onClose !== undefined) { diff --git a/packages/twenty-front/src/modules/ui/layout/modal/components/__stories__/ConfirmationModal.stories.tsx b/packages/twenty-front/src/modules/ui/layout/modal/components/__stories__/ConfirmationModal.stories.tsx index 6990b9429..7b40c7672 100644 --- a/packages/twenty-front/src/modules/ui/layout/modal/components/__stories__/ConfirmationModal.stories.tsx +++ b/packages/twenty-front/src/modules/ui/layout/modal/components/__stories__/ConfirmationModal.stories.tsx @@ -2,6 +2,8 @@ import { Meta, StoryObj } from '@storybook/react'; import { expect, fn, userEvent, waitFor, within } from '@storybook/test'; import { ModalHotkeyScope } from '@/ui/layout/modal/components/types/ModalHotkeyScope'; +import { focusStackState } from '@/ui/utilities/focus/states/focusStackState'; +import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType'; import { currentHotkeyScopeState } from '@/ui/utilities/hotkey/states/internal/currentHotkeyScopeState'; import { internalHotkeysEnabledScopesState } from '@/ui/utilities/hotkey/states/internal/internalHotkeysEnabledScopesState'; import { ComponentDecorator } from 'twenty-ui/testing'; @@ -29,6 +31,20 @@ const initializeState = ({ set }: { set: (atom: any, value: any) => void }) => { }); set(internalHotkeysEnabledScopesState, [ModalHotkeyScope.ModalFocus]); + + set(focusStackState, [ + { + focusId: 'confirmation-modal', + componentInstance: { + componentType: FocusComponentType.MODAL, + componentInstanceId: 'confirmation-modal', + }, + globalHotkeysConfig: { + enableGlobalHotkeysWithModifiers: true, + enableGlobalHotkeysConflictingWithKeyboard: true, + }, + }, + ]); }; const meta: Meta = { diff --git a/packages/twenty-front/src/modules/ui/layout/modal/components/__stories__/Modal.stories.tsx b/packages/twenty-front/src/modules/ui/layout/modal/components/__stories__/Modal.stories.tsx index e75b04485..3d046d75c 100644 --- a/packages/twenty-front/src/modules/ui/layout/modal/components/__stories__/Modal.stories.tsx +++ b/packages/twenty-front/src/modules/ui/layout/modal/components/__stories__/Modal.stories.tsx @@ -2,6 +2,8 @@ import { Meta, StoryObj } from '@storybook/react'; import { expect, fn, userEvent, waitFor, within } from '@storybook/test'; import { ModalHotkeyScope } from '@/ui/layout/modal/components/types/ModalHotkeyScope'; +import { focusStackState } from '@/ui/utilities/focus/states/focusStackState'; +import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType'; import { currentHotkeyScopeState } from '@/ui/utilities/hotkey/states/internal/currentHotkeyScopeState'; import { internalHotkeysEnabledScopesState } from '@/ui/utilities/hotkey/states/internal/internalHotkeysEnabledScopesState'; import { ComponentDecorator } from 'twenty-ui/testing'; @@ -29,6 +31,20 @@ const initializeState = ({ set }: { set: (atom: any, value: any) => void }) => { }); set(internalHotkeysEnabledScopesState, [ModalHotkeyScope.ModalFocus]); + + set(focusStackState, [ + { + focusId: 'modal-id', + componentInstance: { + componentType: FocusComponentType.MODAL, + componentInstanceId: 'modal-id', + }, + globalHotkeysConfig: { + enableGlobalHotkeysWithModifiers: true, + enableGlobalHotkeysConflictingWithKeyboard: true, + }, + }, + ]); }; const meta: Meta = { diff --git a/packages/twenty-front/src/modules/ui/layout/modal/hooks/__tests__/useModal.spec.ts b/packages/twenty-front/src/modules/ui/layout/modal/hooks/__tests__/useModal.spec.ts index fd1e18c39..5f9511e1b 100644 --- a/packages/twenty-front/src/modules/ui/layout/modal/hooks/__tests__/useModal.spec.ts +++ b/packages/twenty-front/src/modules/ui/layout/modal/hooks/__tests__/useModal.spec.ts @@ -4,7 +4,6 @@ import { RecoilRoot, useRecoilValue } from 'recoil'; import { useModal } from '@/ui/layout/modal/hooks/useModal'; import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpenedComponentState'; import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; -import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; import { act } from 'react'; jest.mock('@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'); @@ -13,13 +12,6 @@ const mockSetHotkeyScopeAndMemorizePreviousScope = jest.fn(); const mockGoBackToPreviousHotkeyScope = jest.fn(); const modalId = 'test-modal-id'; -const customHotkeyScope: HotkeyScope = { - scope: 'test-scope', - customScopes: { - goto: true, - commandMenu: true, - }, -}; describe('useModal', () => { beforeEach(() => { @@ -52,31 +44,6 @@ describe('useModal', () => { expect(result.current.isModalOpened).toBe(true); }); - it('should open a modal with custom hotkey scope', () => { - const { result } = renderHook( - () => { - const modal = useModal(); - const isModalOpened = useRecoilValue( - isModalOpenedComponentState.atomFamily({ instanceId: modalId }), - ); - return { modal, isModalOpened }; - }, - { - wrapper: RecoilRoot, - }, - ); - - act(() => { - result.current.modal.openModal(modalId, customHotkeyScope); - }); - - expect(result.current.isModalOpened).toBe(true); - expect(mockSetHotkeyScopeAndMemorizePreviousScope).toHaveBeenCalledWith( - customHotkeyScope.scope, - customHotkeyScope.customScopes, - ); - }); - it('should close a modal', () => { const { result } = renderHook( () => { @@ -153,29 +120,4 @@ describe('useModal', () => { expect(result.current.isModalOpened).toBe(false); expect(mockGoBackToPreviousHotkeyScope).toHaveBeenCalled(); }); - - it('should toggle a modal with custom hotkey scope', () => { - const { result } = renderHook( - () => { - const modal = useModal(); - const isModalOpened = useRecoilValue( - isModalOpenedComponentState.atomFamily({ instanceId: modalId }), - ); - return { modal, isModalOpened }; - }, - { - wrapper: RecoilRoot, - }, - ); - - act(() => { - result.current.modal.toggleModal(modalId, customHotkeyScope); - }); - - expect(result.current.isModalOpened).toBe(true); - expect(mockSetHotkeyScopeAndMemorizePreviousScope).toHaveBeenCalledWith( - customHotkeyScope.scope, - customHotkeyScope.customScopes, - ); - }); }); diff --git a/packages/twenty-front/src/modules/ui/layout/modal/hooks/useModal.tsx b/packages/twenty-front/src/modules/ui/layout/modal/hooks/useModal.tsx index fe81cdb4e..da2a9c9a1 100644 --- a/packages/twenty-front/src/modules/ui/layout/modal/hooks/useModal.tsx +++ b/packages/twenty-front/src/modules/ui/layout/modal/hooks/useModal.tsx @@ -1,16 +1,13 @@ -import { useRecoilCallback } from 'recoil'; - import { ModalHotkeyScope } from '@/ui/layout/modal/components/types/ModalHotkeyScope'; import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpenedComponentState'; -import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; -import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; -import { isDefined } from 'twenty-shared/utils'; +import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack'; +import { useRemoveFocusIdFromFocusStack } from '@/ui/utilities/focus/hooks/useRemoveFocusIdFromFocusStack'; +import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType'; +import { useRecoilCallback } from 'recoil'; export const useModal = () => { - const { - setHotkeyScopeAndMemorizePreviousScope, - goBackToPreviousHotkeyScope, - } = usePreviousHotkeyScope('modal'); + const pushFocusItem = usePushFocusItemToFocusStack(); + const removeFocusId = useRemoveFocusIdFromFocusStack(); const closeModal = useRecoilCallback( ({ set, snapshot }) => @@ -21,20 +18,26 @@ export const useModal = () => { ) .getValue(); - if (isModalOpen) { - goBackToPreviousHotkeyScope(); - set( - isModalOpenedComponentState.atomFamily({ instanceId: modalId }), - false, - ); + if (!isModalOpen) { + return; } + + removeFocusId({ + focusId: modalId, + memoizeKey: modalId, + }); + + set( + isModalOpenedComponentState.atomFamily({ instanceId: modalId }), + false, + ); }, - [goBackToPreviousHotkeyScope], + [removeFocusId], ); const openModal = useRecoilCallback( ({ set, snapshot }) => - (modalId: string, customHotkeyScope?: HotkeyScope) => { + (modalId: string) => { const isModalOpened = snapshot .getLoadable( isModalOpenedComponentState.atomFamily({ instanceId: modalId }), @@ -50,26 +53,35 @@ export const useModal = () => { true, ); - if (isDefined(customHotkeyScope)) { - setHotkeyScopeAndMemorizePreviousScope( - customHotkeyScope.scope, - customHotkeyScope.customScopes, - ); - } else { - setHotkeyScopeAndMemorizePreviousScope(ModalHotkeyScope.ModalFocus, { - goto: false, - commandMenu: false, - commandMenuOpen: false, - keyboardShortcutMenu: false, - }); - } + pushFocusItem({ + focusId: modalId, + component: { + type: FocusComponentType.MODAL, + instanceId: modalId, + }, + globalHotkeysConfig: { + enableGlobalHotkeysWithModifiers: false, + enableGlobalHotkeysConflictingWithKeyboard: false, + }, + // TODO: Remove this once we've migrated hotkey scopes to the new api + hotkeyScope: { + scope: ModalHotkeyScope.ModalFocus, + customScopes: { + goto: false, + commandMenu: false, + commandMenuOpen: false, + keyboardShortcutMenu: false, + }, + }, + memoizeKey: modalId, + }); }, - [setHotkeyScopeAndMemorizePreviousScope], + [pushFocusItem], ); const toggleModal = useRecoilCallback( ({ snapshot }) => - (modalId: string, customHotkeyScope?: HotkeyScope) => { + (modalId: string) => { const isModalOpen = snapshot .getLoadable( isModalOpenedComponentState.atomFamily({ instanceId: modalId }), @@ -79,7 +91,7 @@ export const useModal = () => { if (isModalOpen) { closeModal(modalId); } else { - openModal(modalId, customHotkeyScope); + openModal(modalId); } }, [closeModal, openModal], diff --git a/packages/twenty-front/src/modules/ui/utilities/focus/hooks/__tests__/usePushFocusItemToFocusStack.test.tsx b/packages/twenty-front/src/modules/ui/utilities/focus/hooks/__tests__/usePushFocusItemToFocusStack.test.tsx new file mode 100644 index 000000000..f79ac064c --- /dev/null +++ b/packages/twenty-front/src/modules/ui/utilities/focus/hooks/__tests__/usePushFocusItemToFocusStack.test.tsx @@ -0,0 +1,90 @@ +import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack'; +import { currentFocusIdSelector } from '@/ui/utilities/focus/states/currentFocusIdSelector'; +import { focusStackState } from '@/ui/utilities/focus/states/focusStackState'; +import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType'; +import { renderHook } from '@testing-library/react'; +import { act } from 'react'; +import { RecoilRoot, useRecoilValue } from 'recoil'; + +const renderHooks = () => { + const { result } = renderHook( + () => { + const pushFocusItem = usePushFocusItemToFocusStack(); + const focusStack = useRecoilValue(focusStackState); + const currentFocusId = useRecoilValue(currentFocusIdSelector); + + return { + pushFocusItem, + focusStack, + currentFocusId, + }; + }, + { + wrapper: RecoilRoot, + }, + ); + + return { result }; +}; + +describe('usePushFocusItemToFocusStack', () => { + it('should push focus item to the stack', async () => { + const { result } = renderHooks(); + + expect(result.current.focusStack).toEqual([]); + + const focusItem = { + focusId: 'test-focus-id', + componentInstance: { + componentType: FocusComponentType.MODAL, + componentInstanceId: 'test-instance-id', + }, + globalHotkeysConfig: { + enableGlobalHotkeysWithModifiers: true, + enableGlobalHotkeysConflictingWithKeyboard: true, + }, + }; + + await act(async () => { + result.current.pushFocusItem({ + focusId: focusItem.focusId, + component: { + type: focusItem.componentInstance.componentType, + instanceId: focusItem.componentInstance.componentInstanceId, + }, + hotkeyScope: { scope: 'test-scope' }, + memoizeKey: 'global', + }); + }); + + expect(result.current.focusStack).toEqual([focusItem]); + expect(result.current.currentFocusId).toEqual(focusItem.focusId); + + const anotherFocusItem = { + focusId: 'another-focus-id', + componentInstance: { + componentType: FocusComponentType.MODAL, + componentInstanceId: 'another-instance-id', + }, + globalHotkeysConfig: { + enableGlobalHotkeysWithModifiers: true, + enableGlobalHotkeysConflictingWithKeyboard: true, + }, + }; + + await act(async () => { + result.current.pushFocusItem({ + focusId: anotherFocusItem.focusId, + component: { + type: anotherFocusItem.componentInstance.componentType, + instanceId: anotherFocusItem.componentInstance.componentInstanceId, + }, + hotkeyScope: { scope: 'test-scope' }, + memoizeKey: 'global', + }); + }); + + expect(result.current.focusStack).toEqual([focusItem, anotherFocusItem]); + expect(result.current.currentFocusId).toEqual(anotherFocusItem.focusId); + }); +}); diff --git a/packages/twenty-front/src/modules/ui/utilities/focus/hooks/__tests__/useRemoveFocusIdFromFocusStack.test.tsx b/packages/twenty-front/src/modules/ui/utilities/focus/hooks/__tests__/useRemoveFocusIdFromFocusStack.test.tsx new file mode 100644 index 000000000..20dd54ae2 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/utilities/focus/hooks/__tests__/useRemoveFocusIdFromFocusStack.test.tsx @@ -0,0 +1,101 @@ +import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack'; +import { useRemoveFocusIdFromFocusStack } from '@/ui/utilities/focus/hooks/useRemoveFocusIdFromFocusStack'; +import { currentFocusIdSelector } from '@/ui/utilities/focus/states/currentFocusIdSelector'; +import { focusStackState } from '@/ui/utilities/focus/states/focusStackState'; +import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType'; +import { renderHook } from '@testing-library/react'; +import { act } from 'react'; +import { RecoilRoot, useRecoilValue } from 'recoil'; + +const renderHooks = () => { + const { result } = renderHook( + () => { + const pushFocusItem = usePushFocusItemToFocusStack(); + const removeFocusId = useRemoveFocusIdFromFocusStack(); + const focusStack = useRecoilValue(focusStackState); + const currentFocusId = useRecoilValue(currentFocusIdSelector); + + return { + pushFocusItem, + removeFocusId, + focusStack, + currentFocusId, + }; + }, + { + wrapper: RecoilRoot, + }, + ); + + return { result }; +}; + +describe('useRemoveFocusIdFromFocusStack', () => { + it('should remove focus id from the stack', async () => { + const { result } = renderHooks(); + + const firstFocusItem = { + focusId: 'first-focus-id', + componentInstance: { + componentType: FocusComponentType.MODAL, + componentInstanceId: 'first-instance-id', + }, + globalHotkeysConfig: { + enableGlobalHotkeysWithModifiers: true, + enableGlobalHotkeysConflictingWithKeyboard: true, + }, + }; + + const secondFocusItem = { + focusId: 'second-focus-id', + componentInstance: { + componentType: FocusComponentType.MODAL, + componentInstanceId: 'second-instance-id', + }, + globalHotkeysConfig: { + enableGlobalHotkeysWithModifiers: true, + enableGlobalHotkeysConflictingWithKeyboard: true, + }, + }; + + await act(async () => { + result.current.pushFocusItem({ + focusId: firstFocusItem.focusId, + component: { + type: firstFocusItem.componentInstance.componentType, + instanceId: firstFocusItem.componentInstance.componentInstanceId, + }, + hotkeyScope: { scope: 'test-scope' }, + memoizeKey: 'global', + }); + }); + + await act(async () => { + result.current.pushFocusItem({ + focusId: secondFocusItem.focusId, + component: { + type: secondFocusItem.componentInstance.componentType, + instanceId: secondFocusItem.componentInstance.componentInstanceId, + }, + hotkeyScope: { scope: 'test-scope' }, + memoizeKey: 'global', + }); + }); + + expect(result.current.focusStack).toEqual([ + firstFocusItem, + secondFocusItem, + ]); + expect(result.current.currentFocusId).toEqual(secondFocusItem.focusId); + + await act(async () => { + result.current.removeFocusId({ + focusId: firstFocusItem.focusId, + memoizeKey: 'global', + }); + }); + + expect(result.current.focusStack).toEqual([secondFocusItem]); + expect(result.current.currentFocusId).toEqual(secondFocusItem.focusId); + }); +}); diff --git a/packages/twenty-front/src/modules/ui/utilities/focus/hooks/__tests__/useResetFocusStack.test.tsx b/packages/twenty-front/src/modules/ui/utilities/focus/hooks/__tests__/useResetFocusStack.test.tsx new file mode 100644 index 000000000..481fe4836 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/utilities/focus/hooks/__tests__/useResetFocusStack.test.tsx @@ -0,0 +1,71 @@ +import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack'; +import { useResetFocusStack } from '@/ui/utilities/focus/hooks/useResetFocusStack'; +import { currentFocusIdSelector } from '@/ui/utilities/focus/states/currentFocusIdSelector'; +import { focusStackState } from '@/ui/utilities/focus/states/focusStackState'; +import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType'; +import { renderHook } from '@testing-library/react'; +import { act } from 'react'; +import { RecoilRoot, useRecoilValue } from 'recoil'; + +const renderHooks = () => { + const { result } = renderHook( + () => { + const pushFocusItem = usePushFocusItemToFocusStack(); + const resetFocusStack = useResetFocusStack(); + const focusStack = useRecoilValue(focusStackState); + const currentFocusId = useRecoilValue(currentFocusIdSelector); + + return { + pushFocusItem, + resetFocusStack, + focusStack, + currentFocusId, + }; + }, + { + wrapper: RecoilRoot, + }, + ); + + return { result }; +}; + +describe('useResetFocusStack', () => { + it('should reset the focus stack', async () => { + const { result } = renderHooks(); + + const focusItem = { + focusId: 'test-focus-id', + componentInstance: { + componentType: FocusComponentType.MODAL, + componentInstanceId: 'test-instance-id', + }, + globalHotkeysConfig: { + enableGlobalHotkeysWithModifiers: true, + enableGlobalHotkeysConflictingWithKeyboard: true, + }, + }; + + await act(async () => { + result.current.pushFocusItem({ + focusId: focusItem.focusId, + component: { + type: focusItem.componentInstance.componentType, + instanceId: focusItem.componentInstance.componentInstanceId, + }, + hotkeyScope: { scope: 'test-scope' }, + memoizeKey: 'global', + }); + }); + + expect(result.current.focusStack).toEqual([focusItem]); + expect(result.current.currentFocusId).toEqual(focusItem.focusId); + + await act(async () => { + result.current.resetFocusStack(); + }); + + expect(result.current.focusStack).toEqual([]); + expect(result.current.currentFocusId).toEqual(undefined); + }); +}); diff --git a/packages/twenty-front/src/modules/ui/utilities/focus/hooks/__tests__/useResetFocusStackToFocusItem.test.tsx b/packages/twenty-front/src/modules/ui/utilities/focus/hooks/__tests__/useResetFocusStackToFocusItem.test.tsx new file mode 100644 index 000000000..af4171373 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/utilities/focus/hooks/__tests__/useResetFocusStackToFocusItem.test.tsx @@ -0,0 +1,102 @@ +import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack'; +import { useResetFocusStackToFocusItem } from '@/ui/utilities/focus/hooks/useResetFocusStackToFocusItem'; +import { currentFocusIdSelector } from '@/ui/utilities/focus/states/currentFocusIdSelector'; +import { focusStackState } from '@/ui/utilities/focus/states/focusStackState'; +import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType'; +import { renderHook } from '@testing-library/react'; +import { act } from 'react'; +import { RecoilRoot, useRecoilValue } from 'recoil'; + +const renderHooks = () => { + const { result } = renderHook( + () => { + const pushFocusItem = usePushFocusItemToFocusStack(); + const resetFocusStackToFocusItem = useResetFocusStackToFocusItem(); + const focusStack = useRecoilValue(focusStackState); + const currentFocusId = useRecoilValue(currentFocusIdSelector); + + return { + pushFocusItem, + resetFocusStackToFocusItem, + focusStack, + currentFocusId, + }; + }, + { + wrapper: RecoilRoot, + }, + ); + + return { result }; +}; + +describe('useResetFocusStackToFocusItem', () => { + it('should reset the focus stack to a specific focus item', async () => { + const { result } = renderHooks(); + + const firstFocusItem = { + focusId: 'first-focus-id', + componentInstance: { + componentType: FocusComponentType.MODAL, + componentInstanceId: 'first-instance-id', + }, + globalHotkeysConfig: { + enableGlobalHotkeysWithModifiers: true, + enableGlobalHotkeysConflictingWithKeyboard: true, + }, + }; + + const secondFocusItem = { + focusId: 'second-focus-id', + componentInstance: { + componentType: FocusComponentType.MODAL, + componentInstanceId: 'second-instance-id', + }, + globalHotkeysConfig: { + enableGlobalHotkeysWithModifiers: true, + enableGlobalHotkeysConflictingWithKeyboard: true, + }, + }; + + await act(async () => { + result.current.pushFocusItem({ + focusId: firstFocusItem.focusId, + component: { + type: firstFocusItem.componentInstance.componentType, + instanceId: firstFocusItem.componentInstance.componentInstanceId, + }, + hotkeyScope: { scope: 'test-scope' }, + memoizeKey: 'global', + }); + }); + + await act(async () => { + result.current.pushFocusItem({ + focusId: secondFocusItem.focusId, + component: { + type: secondFocusItem.componentInstance.componentType, + instanceId: secondFocusItem.componentInstance.componentInstanceId, + }, + hotkeyScope: { scope: 'test-scope' }, + memoizeKey: 'global', + }); + }); + + expect(result.current.focusStack).toEqual([ + firstFocusItem, + secondFocusItem, + ]); + expect(result.current.currentFocusId).toEqual(secondFocusItem.focusId); + + await act(async () => { + result.current.resetFocusStackToFocusItem({ + focusStackItem: firstFocusItem, + hotkeyScope: { scope: 'test-scope' }, + memoizeKey: 'global', + }); + }); + + expect(result.current.focusStack).toEqual([firstFocusItem]); + expect(result.current.currentFocusId).toEqual(firstFocusItem.focusId); + }); +}); diff --git a/packages/twenty-front/src/modules/ui/utilities/focus/hooks/usePushFocusItemToFocusStack.ts b/packages/twenty-front/src/modules/ui/utilities/focus/hooks/usePushFocusItemToFocusStack.ts new file mode 100644 index 000000000..49173eadd --- /dev/null +++ b/packages/twenty-front/src/modules/ui/utilities/focus/hooks/usePushFocusItemToFocusStack.ts @@ -0,0 +1,71 @@ +import { focusStackState } from '@/ui/utilities/focus/states/focusStackState'; +import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType'; +import { FocusStackItem } from '@/ui/utilities/focus/types/FocusStackItem'; +import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; +import { GlobalHotkeysConfig } from '@/ui/utilities/hotkey/types/GlobalHotkeysConfig'; +import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; +import { useRecoilCallback } from 'recoil'; + +export const usePushFocusItemToFocusStack = () => { + const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope(); + + const addOrMoveItemToTheTopOfTheStack = useRecoilCallback( + ({ set }) => + (focusStackItem: FocusStackItem) => { + set(focusStackState, (currentFocusStack) => [ + ...currentFocusStack.filter( + (currentFocusStackItem) => + currentFocusStackItem.focusId !== focusStackItem.focusId, + ), + focusStackItem, + ]); + }, + [], + ); + + return useRecoilCallback( + () => + ({ + focusId, + component, + hotkeyScope, + memoizeKey = 'global', + globalHotkeysConfig, + }: { + focusId: string; + component: { + type: FocusComponentType; + instanceId: string; + }; + globalHotkeysConfig?: Partial; + // TODO: Remove this once we've migrated hotkey scopes to the new api + hotkeyScope: HotkeyScope; + memoizeKey: string; + }) => { + const focusStackItem: FocusStackItem = { + focusId, + componentInstance: { + componentType: component.type, + componentInstanceId: component.instanceId, + }, + globalHotkeysConfig: { + enableGlobalHotkeysWithModifiers: + globalHotkeysConfig?.enableGlobalHotkeysWithModifiers ?? true, + enableGlobalHotkeysConflictingWithKeyboard: + globalHotkeysConfig?.enableGlobalHotkeysConflictingWithKeyboard ?? + true, + }, + }; + + addOrMoveItemToTheTopOfTheStack(focusStackItem); + + // TODO: Remove this once we've migrated hotkey scopes to the new api + setHotkeyScopeAndMemorizePreviousScope({ + scope: hotkeyScope.scope, + customScopes: hotkeyScope.customScopes, + memoizeKey, + }); + }, + [setHotkeyScopeAndMemorizePreviousScope, addOrMoveItemToTheTopOfTheStack], + ); +}; diff --git a/packages/twenty-front/src/modules/ui/utilities/focus/hooks/useRemoveFocusIdFromFocusStack.ts b/packages/twenty-front/src/modules/ui/utilities/focus/hooks/useRemoveFocusIdFromFocusStack.ts new file mode 100644 index 000000000..c966e92be --- /dev/null +++ b/packages/twenty-front/src/modules/ui/utilities/focus/hooks/useRemoveFocusIdFromFocusStack.ts @@ -0,0 +1,22 @@ +import { focusStackState } from '@/ui/utilities/focus/states/focusStackState'; +import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; +import { useRecoilCallback } from 'recoil'; + +export const useRemoveFocusIdFromFocusStack = () => { + const { goBackToPreviousHotkeyScope } = usePreviousHotkeyScope(); + + return useRecoilCallback( + ({ set }) => + ({ focusId, memoizeKey }: { focusId: string; memoizeKey: string }) => { + set(focusStackState, (previousFocusStack) => + previousFocusStack.filter( + (focusStackItem) => focusStackItem.focusId !== focusId, + ), + ); + + // TODO: Remove this once we've migrated hotkey scopes to the new api + goBackToPreviousHotkeyScope(memoizeKey); + }, + [goBackToPreviousHotkeyScope], + ); +}; diff --git a/packages/twenty-front/src/modules/ui/utilities/focus/hooks/useResetFocusStack.ts b/packages/twenty-front/src/modules/ui/utilities/focus/hooks/useResetFocusStack.ts new file mode 100644 index 000000000..c22529b6a --- /dev/null +++ b/packages/twenty-front/src/modules/ui/utilities/focus/hooks/useResetFocusStack.ts @@ -0,0 +1,18 @@ +import { focusStackState } from '@/ui/utilities/focus/states/focusStackState'; +import { currentHotkeyScopeState } from '@/ui/utilities/hotkey/states/internal/currentHotkeyScopeState'; +import { previousHotkeyScopeFamilyState } from '@/ui/utilities/hotkey/states/internal/previousHotkeyScopeFamilyState'; +import { useRecoilCallback } from 'recoil'; + +export const useResetFocusStack = () => { + return useRecoilCallback( + ({ reset }) => + (memoizeKey = 'global') => { + reset(focusStackState); + + // TODO: Remove this once we've migrated hotkey scopes to the new api + reset(previousHotkeyScopeFamilyState(memoizeKey as string)); + reset(currentHotkeyScopeState); + }, + [], + ); +}; diff --git a/packages/twenty-front/src/modules/ui/utilities/focus/hooks/useResetFocusStackToFocusItem.ts b/packages/twenty-front/src/modules/ui/utilities/focus/hooks/useResetFocusStackToFocusItem.ts new file mode 100644 index 000000000..270a8db4e --- /dev/null +++ b/packages/twenty-front/src/modules/ui/utilities/focus/hooks/useResetFocusStackToFocusItem.ts @@ -0,0 +1,28 @@ +import { focusStackState } from '@/ui/utilities/focus/states/focusStackState'; +import { FocusStackItem } from '@/ui/utilities/focus/types/FocusStackItem'; +import { currentHotkeyScopeState } from '@/ui/utilities/hotkey/states/internal/currentHotkeyScopeState'; +import { previousHotkeyScopeFamilyState } from '@/ui/utilities/hotkey/states/internal/previousHotkeyScopeFamilyState'; +import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; +import { useRecoilCallback } from 'recoil'; + +export const useResetFocusStackToFocusItem = () => { + return useRecoilCallback( + ({ set }) => + ({ + focusStackItem, + hotkeyScope, + memoizeKey, + }: { + focusStackItem: FocusStackItem; + hotkeyScope: HotkeyScope; + memoizeKey: string; + }) => { + set(focusStackState, [focusStackItem]); + + // TODO: Remove this once we've migrated hotkey scopes to the new api + set(previousHotkeyScopeFamilyState(memoizeKey), null); + set(currentHotkeyScopeState, hotkeyScope); + }, + [], + ); +}; diff --git a/packages/twenty-front/src/modules/ui/utilities/focus/states/currentFocusIdSelector.ts b/packages/twenty-front/src/modules/ui/utilities/focus/states/currentFocusIdSelector.ts new file mode 100644 index 000000000..606256ee0 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/utilities/focus/states/currentFocusIdSelector.ts @@ -0,0 +1,11 @@ +import { selector } from 'recoil'; +import { focusStackState } from './focusStackState'; + +export const currentFocusIdSelector = selector({ + key: 'currentFocusIdSelector', + get: ({ get }) => { + const focusStack = get(focusStackState); + + return focusStack.at(-1)?.focusId; + }, +}); diff --git a/packages/twenty-front/src/modules/ui/utilities/focus/states/currentGlobalHotkeysConfigSelector.ts b/packages/twenty-front/src/modules/ui/utilities/focus/states/currentGlobalHotkeysConfigSelector.ts new file mode 100644 index 000000000..f49871a24 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/utilities/focus/states/currentGlobalHotkeysConfigSelector.ts @@ -0,0 +1,21 @@ +import { focusStackState } from '@/ui/utilities/focus/states/focusStackState'; +import { DEFAULT_GLOBAL_HOTKEYS_CONFIG } from '@/ui/utilities/hotkey/constants/DefaultGlobalHotkeysConfig'; +import { GlobalHotkeysConfig } from '@/ui/utilities/hotkey/types/GlobalHotkeysConfig'; +import { selector } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; + +export const currentGlobalHotkeysConfigSelector = selector( + { + key: 'currentGlobalHotkeysConfigSelector', + get: ({ get }) => { + const focusStack = get(focusStackState); + const lastFocusStackItem = focusStack.at(-1); + + if (!isDefined(lastFocusStackItem)) { + return DEFAULT_GLOBAL_HOTKEYS_CONFIG; + } + + return lastFocusStackItem.globalHotkeysConfig; + }, + }, +); diff --git a/packages/twenty-front/src/modules/ui/utilities/focus/states/focusStackState.ts b/packages/twenty-front/src/modules/ui/utilities/focus/states/focusStackState.ts new file mode 100644 index 000000000..9581e78c6 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/utilities/focus/states/focusStackState.ts @@ -0,0 +1,7 @@ +import { FocusStackItem } from '@/ui/utilities/focus/types/FocusStackItem'; +import { createState } from 'twenty-ui/utilities'; + +export const focusStackState = createState({ + key: 'focusStackState', + defaultValue: [], +}); diff --git a/packages/twenty-front/src/modules/ui/utilities/focus/types/FocusComponentInstance.ts b/packages/twenty-front/src/modules/ui/utilities/focus/types/FocusComponentInstance.ts new file mode 100644 index 000000000..9dda63e1e --- /dev/null +++ b/packages/twenty-front/src/modules/ui/utilities/focus/types/FocusComponentInstance.ts @@ -0,0 +1,6 @@ +import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType'; + +export type FocusComponentInstance = { + componentType: FocusComponentType; + componentInstanceId: string; +}; 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 new file mode 100644 index 000000000..7562a2d4e --- /dev/null +++ b/packages/twenty-front/src/modules/ui/utilities/focus/types/FocusComponentType.ts @@ -0,0 +1,3 @@ +export enum FocusComponentType { + MODAL = 'modal', +} diff --git a/packages/twenty-front/src/modules/ui/utilities/focus/types/FocusStackItem.ts b/packages/twenty-front/src/modules/ui/utilities/focus/types/FocusStackItem.ts new file mode 100644 index 000000000..262547f39 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/utilities/focus/types/FocusStackItem.ts @@ -0,0 +1,8 @@ +import { FocusComponentInstance } from '@/ui/utilities/focus/types/FocusComponentInstance'; +import { GlobalHotkeysConfig } from '@/ui/utilities/hotkey/types/GlobalHotkeysConfig'; + +export type FocusStackItem = { + focusId: string; + componentInstance: FocusComponentInstance; + globalHotkeysConfig: GlobalHotkeysConfig; +}; 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 new file mode 100644 index 000000000..0ac5ab524 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/utilities/hotkey/constants/DebugHotkeyScope.ts @@ -0,0 +1 @@ +export const DEBUG_HOTKEY_SCOPE = false; diff --git a/packages/twenty-front/src/modules/ui/utilities/hotkey/constants/DefaultGlobalHotkeysConfig.ts b/packages/twenty-front/src/modules/ui/utilities/hotkey/constants/DefaultGlobalHotkeysConfig.ts new file mode 100644 index 000000000..670f7038e --- /dev/null +++ b/packages/twenty-front/src/modules/ui/utilities/hotkey/constants/DefaultGlobalHotkeysConfig.ts @@ -0,0 +1,6 @@ +import { GlobalHotkeysConfig } from '@/ui/utilities/hotkey/types/GlobalHotkeysConfig'; + +export const DEFAULT_GLOBAL_HOTKEYS_CONFIG: GlobalHotkeysConfig = { + enableGlobalHotkeysWithModifiers: true, + enableGlobalHotkeysConflictingWithKeyboard: true, +}; diff --git a/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useGlobalHotkeys.ts b/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useGlobalHotkeys.ts new file mode 100644 index 000000000..4d97c51a5 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useGlobalHotkeys.ts @@ -0,0 +1,74 @@ +import { useGlobalHotkeysCallback } from '@/ui/utilities/hotkey/hooks/useGlobalHotkeysCallback'; +import { pendingHotkeyState } from '@/ui/utilities/hotkey/states/internal/pendingHotkeysState'; +import { useHotkeys } from 'react-hotkeys-hook'; +import { HotkeyCallback, Keys, Options } from 'react-hotkeys-hook/dist/types'; +import { useRecoilCallback } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; + +type UseHotkeysOptionsWithoutBuggyOptions = Omit; + +export const useGlobalHotkeys = ( + keys: Keys, + callback: HotkeyCallback, + containsModifier: boolean, + // TODO: Remove this once we've migrated hotkey scopes to the new api + scope: string, + dependencies?: unknown[], + options?: UseHotkeysOptionsWithoutBuggyOptions, +) => { + const callGlobalHotkeysCallback = useGlobalHotkeysCallback(dependencies); + + const enableOnContentEditable = isDefined(options?.enableOnContentEditable) + ? options.enableOnContentEditable + : true; + + const enableOnFormTags = isDefined(options?.enableOnFormTags) + ? options.enableOnFormTags + : true; + + const preventDefault = isDefined(options?.preventDefault) + ? options.preventDefault === true + : true; + + const ignoreModifiers = isDefined(options?.ignoreModifiers) + ? options.ignoreModifiers === true + : false; + + const handleCallback = useRecoilCallback( + ({ snapshot, set }) => + async (keyboardEvent: KeyboardEvent, hotkeysEvent: any) => { + const pendingHotkey = snapshot + .getLoadable(pendingHotkeyState) + .getValue(); + + if (!pendingHotkey) { + callback(keyboardEvent, hotkeysEvent); + } + + set(pendingHotkeyState, null); + }, + [callback], + ); + + return useHotkeys( + keys, + (keyboardEvent, hotkeysEvent) => { + callGlobalHotkeysCallback({ + keyboardEvent, + hotkeysEvent, + callback: () => { + handleCallback(keyboardEvent, hotkeysEvent); + }, + scope, + preventDefault, + containsModifier, + }); + }, + { + enableOnContentEditable, + enableOnFormTags, + ignoreModifiers, + }, + dependencies, + ); +}; diff --git a/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useGlobalHotkeysCallback.ts b/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useGlobalHotkeysCallback.ts new file mode 100644 index 000000000..23c9fbb5b --- /dev/null +++ b/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useGlobalHotkeysCallback.ts @@ -0,0 +1,119 @@ +import { currentGlobalHotkeysConfigSelector } from '@/ui/utilities/focus/states/currentGlobalHotkeysConfigSelector'; +import { internalHotkeysEnabledScopesState } from '@/ui/utilities/hotkey/states/internal/internalHotkeysEnabledScopesState'; +import { + Hotkey, + OptionsOrDependencyArray, +} from 'react-hotkeys-hook/dist/types'; +import { useRecoilCallback } from 'recoil'; +import { logDebug } from '~/utils/logDebug'; +import { DEBUG_HOTKEY_SCOPE } from '../constants/DebugHotkeyScope'; + +export const useGlobalHotkeysCallback = ( + dependencies?: OptionsOrDependencyArray, +) => { + const dependencyArray = Array.isArray(dependencies) ? dependencies : []; + + return useRecoilCallback( + ({ snapshot }) => + ({ + callback, + containsModifier, + hotkeysEvent, + keyboardEvent, + preventDefault, + scope, + }: { + keyboardEvent: KeyboardEvent; + hotkeysEvent: Hotkey; + containsModifier: boolean; + callback: (keyboardEvent: KeyboardEvent, hotkeysEvent: Hotkey) => void; + preventDefault?: boolean; + scope: string; + }) => { + // TODO: Remove this once we've migrated hotkey scopes to the new api + const currentHotkeyScopes = snapshot + .getLoadable(internalHotkeysEnabledScopesState) + .getValue(); + + const currentGlobalHotkeysConfig = snapshot + .getLoadable(currentGlobalHotkeysConfigSelector) + .getValue(); + + if ( + containsModifier && + !currentGlobalHotkeysConfig.enableGlobalHotkeysWithModifiers + ) { + if (DEBUG_HOTKEY_SCOPE) { + logDebug( + `DEBUG: %cI can't call hotkey (${ + hotkeysEvent.keys + }) because global hotkeys with modifiers are disabled`, + 'color: gray; ', + ); + } + + return; + } + + if ( + !containsModifier && + !currentGlobalHotkeysConfig.enableGlobalHotkeysConflictingWithKeyboard + ) { + if (DEBUG_HOTKEY_SCOPE) { + logDebug( + `DEBUG: %cI can't call hotkey (${ + hotkeysEvent.keys + }) because global hotkeys conflicting with keyboard are disabled`, + 'color: gray; ', + ); + } + return; + } + + // TODO: Remove this once we've migrated hotkey scopes to the new api + if (!currentHotkeyScopes.includes(scope)) { + if (DEBUG_HOTKEY_SCOPE) { + logDebug( + `DEBUG: %cI can't call hotkey (${ + hotkeysEvent.keys + }) because I'm in scope [${scope}] and the active scopes are : [${currentHotkeyScopes.join( + ', ', + )}]`, + 'color: gray; ', + ); + } + + return; + } + + // TODO: Remove this once we've migrated hotkey scopes to the new api + if (DEBUG_HOTKEY_SCOPE) { + logDebug( + `DEBUG: %cI can call hotkey (${ + hotkeysEvent.keys + }) because I'm in scope [${scope}] and the active scopes are : [${currentHotkeyScopes.join( + ', ', + )}]`, + 'color: green;', + ); + } + + if (preventDefault === true) { + if (DEBUG_HOTKEY_SCOPE) { + logDebug( + `DEBUG: %cI prevent default for hotkey (${hotkeysEvent.keys})`, + 'color: gray;', + ); + } + + keyboardEvent.stopPropagation(); + keyboardEvent.preventDefault(); + keyboardEvent.stopImmediatePropagation(); + } + + return callback(keyboardEvent, hotkeysEvent); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + dependencyArray, + ); +}; diff --git a/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useSequenceScopedHotkeys.ts b/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useGlobalHotkeysSequence.ts similarity index 84% rename from packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useSequenceScopedHotkeys.ts rename to packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useGlobalHotkeysSequence.ts index 19d587921..10565ce04 100644 --- a/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useSequenceScopedHotkeys.ts +++ b/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useGlobalHotkeysSequence.ts @@ -4,10 +4,10 @@ import { useRecoilState } from 'recoil'; import { pendingHotkeyState } from '../states/internal/pendingHotkeysState'; -import { useScopedHotkeyCallback } from './useScopedHotkeyCallback'; +import { useGlobalHotkeysCallback } from '@/ui/utilities/hotkey/hooks/useGlobalHotkeysCallback'; import { isDefined } from 'twenty-shared/utils'; -export const useSequenceHotkeys = ( +export const useGlobalHotkeysSequence = ( firstKey: Keys, secondKey: Keys, sequenceCallback: () => void, @@ -21,14 +21,15 @@ export const useSequenceHotkeys = ( ) => { const [pendingHotkey, setPendingHotkey] = useRecoilState(pendingHotkeyState); - const callScopedHotkeyCallback = useScopedHotkeyCallback(); + const callGlobalHotkeysCallback = useGlobalHotkeysCallback(); useHotkeys( firstKey, (keyboardEvent, hotkeysEvent) => { - callScopedHotkeyCallback({ + callGlobalHotkeysCallback({ keyboardEvent, hotkeysEvent, + containsModifier: false, callback: () => { setPendingHotkey(firstKey); }, @@ -46,9 +47,10 @@ export const useSequenceHotkeys = ( useHotkeys( secondKey, (keyboardEvent, hotkeysEvent) => { - callScopedHotkeyCallback({ + callGlobalHotkeysCallback({ keyboardEvent, hotkeysEvent, + containsModifier: false, callback: () => { if (pendingHotkey !== firstKey) { return; diff --git a/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useGoToHotkeys.ts b/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useGoToHotkeys.ts index d8e62312c..46ca5b869 100644 --- a/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useGoToHotkeys.ts +++ b/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useGoToHotkeys.ts @@ -1,10 +1,9 @@ import { Keys } from 'react-hotkeys-hook/dist/types'; import { useNavigate } from 'react-router-dom'; +import { useGlobalHotkeysSequence } from '@/ui/utilities/hotkey/hooks/useGlobalHotkeysSequence'; import { AppHotkeyScope } from '../types/AppHotkeyScope'; -import { useSequenceHotkeys } from './useSequenceScopedHotkeys'; - type GoToHotkeysProps = { key: Keys; location: string; @@ -18,7 +17,7 @@ export const useGoToHotkeys = ({ }: GoToHotkeysProps) => { const navigate = useNavigate(); - useSequenceHotkeys( + useGlobalHotkeysSequence( 'g', key, () => { diff --git a/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement.ts b/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement.ts new file mode 100644 index 000000000..562de2617 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement.ts @@ -0,0 +1,73 @@ +import { useHotkeysOnFocusedElementCallback } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElementCallback'; +import { pendingHotkeyState } from '@/ui/utilities/hotkey/states/internal/pendingHotkeysState'; +import { useHotkeys } from 'react-hotkeys-hook'; +import { HotkeyCallback, Keys, Options } from 'react-hotkeys-hook/dist/types'; +import { useRecoilState } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; + +type UseHotkeysOptionsWithoutBuggyOptions = Omit; + +export const useHotkeysOnFocusedElement = ({ + keys, + callback, + focusId, + // TODO: Remove this once we've migrated hotkey scopes to the new api + scope, + dependencies, + options, +}: { + keys: Keys; + callback: HotkeyCallback; + focusId: string; + // TODO: Remove this once we've migrated hotkey scopes to the new api + scope: string; + dependencies?: unknown[]; + options?: UseHotkeysOptionsWithoutBuggyOptions; +}) => { + const [pendingHotkey, setPendingHotkey] = useRecoilState(pendingHotkeyState); + + const callScopedHotkeyCallback = + useHotkeysOnFocusedElementCallback(dependencies); + + const enableOnContentEditable = isDefined(options?.enableOnContentEditable) + ? options.enableOnContentEditable + : true; + + const enableOnFormTags = isDefined(options?.enableOnFormTags) + ? options.enableOnFormTags + : true; + + const preventDefault = isDefined(options?.preventDefault) + ? options.preventDefault === true + : true; + + const ignoreModifiers = isDefined(options?.ignoreModifiers) + ? options.ignoreModifiers === true + : false; + + return useHotkeys( + keys, + (keyboardEvent, hotkeysEvent) => { + callScopedHotkeyCallback({ + keyboardEvent, + hotkeysEvent, + callback: () => { + if (!pendingHotkey) { + callback(keyboardEvent, hotkeysEvent); + return; + } + setPendingHotkey(null); + }, + focusId, + scope, + preventDefault, + }); + }, + { + enableOnContentEditable, + enableOnFormTags, + ignoreModifiers, + }, + dependencies, + ); +}; diff --git a/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElementCallback.ts b/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElementCallback.ts new file mode 100644 index 000000000..1f4023799 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElementCallback.ts @@ -0,0 +1,89 @@ +import { internalHotkeysEnabledScopesState } from '@/ui/utilities/hotkey/states/internal/internalHotkeysEnabledScopesState'; +import { + Hotkey, + OptionsOrDependencyArray, +} from 'react-hotkeys-hook/dist/types'; +import { useRecoilCallback } from 'recoil'; +import { logDebug } from '~/utils/logDebug'; +import { currentFocusIdSelector } from '../../focus/states/currentFocusIdSelector'; +import { DEBUG_HOTKEY_SCOPE } from '../constants/DebugHotkeyScope'; + +export const useHotkeysOnFocusedElementCallback = ( + dependencies?: OptionsOrDependencyArray, +) => { + const dependencyArray = Array.isArray(dependencies) ? dependencies : []; + + return useRecoilCallback( + ({ snapshot }) => + ({ + callback, + hotkeysEvent, + keyboardEvent, + focusId, + scope, + preventDefault, + }: { + keyboardEvent: KeyboardEvent; + hotkeysEvent: Hotkey; + callback: (keyboardEvent: KeyboardEvent, hotkeysEvent: Hotkey) => void; + focusId: string; + scope: string; + preventDefault?: boolean; + }) => { + const currentFocusId = snapshot + .getLoadable(currentFocusIdSelector) + .getValue(); + + // TODO: Remove this once we've migrated hotkey scopes to the new api + const currentHotkeyScopes = snapshot + .getLoadable(internalHotkeysEnabledScopesState) + .getValue(); + + if ( + currentFocusId !== focusId || + !currentHotkeyScopes.includes(scope) + ) { + if (DEBUG_HOTKEY_SCOPE) { + logDebug( + `DEBUG: %cI can't call hotkey (${ + hotkeysEvent.keys + }) because I'm in scope [${scope}] and the active scopes are : [${currentHotkeyScopes.join( + ', ', + )}] and the current focus identifier is [${focusId}]`, + 'color: gray; ', + ); + } + + return; + } + + if (DEBUG_HOTKEY_SCOPE) { + logDebug( + `DEBUG: %cI can call hotkey (${ + hotkeysEvent.keys + }) because I'm in scope [${scope}] and the active scopes are : [${currentHotkeyScopes.join( + ', ', + )}], and the current focus identifier is [${focusId}]`, + 'color: green;', + ); + } + + if (preventDefault === true) { + if (DEBUG_HOTKEY_SCOPE) { + logDebug( + `DEBUG: %cI prevent default for hotkey (${hotkeysEvent.keys})`, + 'color: gray;', + ); + } + + keyboardEvent.stopPropagation(); + keyboardEvent.preventDefault(); + keyboardEvent.stopImmediatePropagation(); + } + + return callback(keyboardEvent, hotkeysEvent); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + dependencyArray, + ); +}; diff --git a/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/usePreviousHotkeyScope.ts b/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/usePreviousHotkeyScope.ts index d98e9ab12..6268fa260 100644 --- a/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/usePreviousHotkeyScope.ts +++ b/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/usePreviousHotkeyScope.ts @@ -1,7 +1,7 @@ import { useRecoilCallback } from 'recoil'; -import { DEBUG_HOTKEY_SCOPE } from '@/ui/utilities/hotkey/hooks/useScopedHotkeyCallback'; import { logDebug } from '~/utils/logDebug'; +import { DEBUG_HOTKEY_SCOPE } from '../constants/DebugHotkeyScope'; import { currentHotkeyScopeState } from '../states/internal/currentHotkeyScopeState'; import { previousHotkeyScopeFamilyState } from '../states/internal/previousHotkeyScopeFamilyState'; @@ -9,14 +9,14 @@ import { CustomHotkeyScopes } from '../types/CustomHotkeyScope'; import { useSetHotkeyScope } from './useSetHotkeyScope'; -export const usePreviousHotkeyScope = (memoizeKey = 'global') => { +export const usePreviousHotkeyScope = () => { const setHotkeyScope = useSetHotkeyScope(); const goBackToPreviousHotkeyScope = useRecoilCallback( ({ snapshot, set }) => - () => { + (memoizeKey = 'global') => { const previousHotkeyScope = snapshot - .getLoadable(previousHotkeyScopeFamilyState(memoizeKey)) + .getLoadable(previousHotkeyScopeFamilyState(memoizeKey as string)) .getValue(); if (!previousHotkeyScope) { @@ -39,14 +39,22 @@ export const usePreviousHotkeyScope = (memoizeKey = 'global') => { previousHotkeyScope.customScopes, ); - set(previousHotkeyScopeFamilyState(memoizeKey), null); + set(previousHotkeyScopeFamilyState(memoizeKey as string), null); }, - [setHotkeyScope, memoizeKey], + [setHotkeyScope], ); const setHotkeyScopeAndMemorizePreviousScope = useRecoilCallback( ({ snapshot, set }) => - (scope: string, customScopes?: CustomHotkeyScopes) => { + ({ + scope, + customScopes, + memoizeKey = 'global', + }: { + scope: string; + customScopes?: CustomHotkeyScopes; + memoizeKey?: string; + }) => { const currentHotkeyScope = snapshot .getLoadable(currentHotkeyScopeState) .getValue(); @@ -63,7 +71,7 @@ export const usePreviousHotkeyScope = (memoizeKey = 'global') => { set(previousHotkeyScopeFamilyState(memoizeKey), currentHotkeyScope); }, - [setHotkeyScope, memoizeKey], + [setHotkeyScope], ); return { 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 index bc8d7026c..f9c98ddd5 100644 --- a/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useScopedHotkeyCallback.ts +++ b/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useScopedHotkeyCallback.ts @@ -6,10 +6,9 @@ import { useRecoilCallback } from 'recoil'; import { logDebug } from '~/utils/logDebug'; +import { DEBUG_HOTKEY_SCOPE } from '../constants/DebugHotkeyScope'; import { internalHotkeysEnabledScopesState } from '../states/internal/internalHotkeysEnabledScopesState'; -export const DEBUG_HOTKEY_SCOPE = false; - export const useScopedHotkeyCallback = ( dependencies?: OptionsOrDependencyArray, ) => { 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 index 816e7a0a2..9d186d0a2 100644 --- a/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useScopedHotkeys.ts +++ b/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useScopedHotkeys.ts @@ -2,9 +2,9 @@ import { useHotkeys } from 'react-hotkeys-hook'; import { HotkeyCallback, Keys, Options } from 'react-hotkeys-hook/dist/types'; import { useRecoilState } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; import { pendingHotkeyState } from '../states/internal/pendingHotkeysState'; import { useScopedHotkeyCallback } from './useScopedHotkeyCallback'; -import { isDefined } from 'twenty-shared/utils'; type UseHotkeysOptionsWithoutBuggyOptions = Omit; diff --git a/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useSetHotkeyScope.ts b/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useSetHotkeyScope.ts index adcf1cb28..f1392bd0e 100644 --- a/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useSetHotkeyScope.ts +++ b/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useSetHotkeyScope.ts @@ -1,6 +1,6 @@ import { useRecoilCallback } from 'recoil'; -import { DEBUG_HOTKEY_SCOPE } from '@/ui/utilities/hotkey/hooks/useScopedHotkeyCallback'; +import { DEBUG_HOTKEY_SCOPE } from '../constants/DebugHotkeyScope'; import { isDefined } from 'twenty-shared/utils'; import { logDebug } from '~/utils/logDebug'; @@ -11,7 +11,7 @@ import { AppHotkeyScope } from '../types/AppHotkeyScope'; import { CustomHotkeyScopes } from '../types/CustomHotkeyScope'; import { HotkeyScope } from '../types/HotkeyScope'; -const isCustomScopesEqual = ( +const areCustomScopesEqual = ( customScopesA: CustomHotkeyScopes | undefined, customScopesB: CustomHotkeyScopes | undefined, ) => { @@ -34,7 +34,7 @@ export const useSetHotkeyScope = () => if (currentHotkeyScope.scope === hotkeyScopeToSet) { if (!isDefined(customScopes)) { if ( - isCustomScopesEqual( + areCustomScopesEqual( currentHotkeyScope?.customScopes, DEFAULT_HOTKEYS_SCOPE_CUSTOM_SCOPES, ) @@ -43,7 +43,7 @@ export const useSetHotkeyScope = () => } } else { if ( - isCustomScopesEqual( + areCustomScopesEqual( currentHotkeyScope?.customScopes, customScopes, ) diff --git a/packages/twenty-front/src/modules/ui/utilities/hotkey/types/GlobalHotkeysConfig.ts b/packages/twenty-front/src/modules/ui/utilities/hotkey/types/GlobalHotkeysConfig.ts new file mode 100644 index 000000000..05f619113 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/utilities/hotkey/types/GlobalHotkeysConfig.ts @@ -0,0 +1,4 @@ +export type GlobalHotkeysConfig = { + enableGlobalHotkeysWithModifiers: boolean; + enableGlobalHotkeysConflictingWithKeyboard: boolean; +};