Introduce focus stack to handle hotkeys (#12166)
# Introduce focus stack to handle hotkeys This PR introduces a focus stack to track the order in which the elements are focused: - Each focused element has a unique focus id - When an element is focused, it is pushed on top of the stack - When an element loses focus, we remove it from the stack This focus stack is then used to determine which hotkeys are available. The previous implementation lead to many regressions because of race conditions, of wrong order of open and close operations and by overwriting previous states. This implementation should be way more robust than the previous one. The new api can be incrementally implemented since it preserves backwards compatibility by writing to the old hotkey scopes states. For now, it has been implemented on the modal components. To test this PR, verify that the shortcuts still work correctly, especially for the modal components.
This commit is contained in:
@ -12,7 +12,9 @@ export const useHotkeyScopeOnMount = (hotkeyScope: string) => {
|
|||||||
} = usePreviousHotkeyScope();
|
} = usePreviousHotkeyScope();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setHotkeyScopeAndMemorizePreviousScope(hotkeyScope);
|
setHotkeyScopeAndMemorizePreviousScope({
|
||||||
|
scope: hotkeyScope,
|
||||||
|
});
|
||||||
return () => {
|
return () => {
|
||||||
goBackToPreviousHotkeyScope();
|
goBackToPreviousHotkeyScope();
|
||||||
};
|
};
|
||||||
|
|||||||
@ -353,9 +353,9 @@ export const ActivityRichTextEditor = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleBlockEditorFocus = () => {
|
const handleBlockEditorFocus = () => {
|
||||||
setHotkeyScopeAndMemorizePreviousScope(
|
setHotkeyScopeAndMemorizePreviousScope({
|
||||||
ActivityEditorHotkeyScope.ActivityBody,
|
scope: ActivityEditorHotkeyScope.ActivityBody,
|
||||||
);
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlerBlockEditorBlur = () => {
|
const handlerBlockEditorBlur = () => {
|
||||||
|
|||||||
@ -84,9 +84,9 @@ export const useOpenActivityTargetCellEditMode = () => {
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
setHotkeyScopeAndMemorizePreviousScope(
|
setHotkeyScopeAndMemorizePreviousScope({
|
||||||
MultipleRecordPickerHotkeyScope.MultipleRecordPicker,
|
scope: MultipleRecordPickerHotkeyScope.MultipleRecordPicker,
|
||||||
);
|
});
|
||||||
},
|
},
|
||||||
[multipleRecordPickerPerformSearch, setHotkeyScopeAndMemorizePreviousScope],
|
[multipleRecordPickerPerformSearch, setHotkeyScopeAndMemorizePreviousScope],
|
||||||
);
|
);
|
||||||
|
|||||||
@ -71,12 +71,13 @@ describe('useCommandMenu', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(result.current.isCommandMenuOpened).toBe(true);
|
expect(result.current.isCommandMenuOpened).toBe(true);
|
||||||
expect(mockSetHotkeyScopeAndMemorizePreviousScope).toHaveBeenCalledWith(
|
expect(mockSetHotkeyScopeAndMemorizePreviousScope).toHaveBeenCalledWith({
|
||||||
CommandMenuHotkeyScope.CommandMenuFocused,
|
scope: CommandMenuHotkeyScope.CommandMenuFocused,
|
||||||
{
|
memoizeKey: 'command-menu',
|
||||||
|
customScopes: {
|
||||||
commandMenuOpen: true,
|
commandMenuOpen: true,
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
result.current.commandMenu.closeCommandMenu();
|
result.current.commandMenu.closeCommandMenu();
|
||||||
@ -95,12 +96,13 @@ describe('useCommandMenu', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(result.current.isCommandMenuOpened).toBe(true);
|
expect(result.current.isCommandMenuOpened).toBe(true);
|
||||||
expect(mockSetHotkeyScopeAndMemorizePreviousScope).toHaveBeenCalledWith(
|
expect(mockSetHotkeyScopeAndMemorizePreviousScope).toHaveBeenCalledWith({
|
||||||
CommandMenuHotkeyScope.CommandMenuFocused,
|
scope: CommandMenuHotkeyScope.CommandMenuFocused,
|
||||||
{
|
memoizeKey: 'command-menu',
|
||||||
|
customScopes: {
|
||||||
commandMenuOpen: true,
|
commandMenuOpen: true,
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
result.current.commandMenu.toggleCommandMenu();
|
result.current.commandMenu.toggleCommandMenu();
|
||||||
|
|||||||
@ -16,9 +16,7 @@ import { isCommandMenuOpenedState } from '../states/isCommandMenuOpenedState';
|
|||||||
export const useCommandMenu = () => {
|
export const useCommandMenu = () => {
|
||||||
const { navigateCommandMenu } = useNavigateCommandMenu();
|
const { navigateCommandMenu } = useNavigateCommandMenu();
|
||||||
const { closeAnyOpenDropdown } = useCloseAnyOpenDropdown();
|
const { closeAnyOpenDropdown } = useCloseAnyOpenDropdown();
|
||||||
const { goBackToPreviousHotkeyScope } = usePreviousHotkeyScope(
|
const { goBackToPreviousHotkeyScope } = usePreviousHotkeyScope();
|
||||||
COMMAND_MENU_COMPONENT_INSTANCE_ID,
|
|
||||||
);
|
|
||||||
|
|
||||||
const closeCommandMenu = useRecoilCallback(
|
const closeCommandMenu = useRecoilCallback(
|
||||||
({ set, snapshot }) =>
|
({ set, snapshot }) =>
|
||||||
@ -32,7 +30,7 @@ export const useCommandMenu = () => {
|
|||||||
set(isCommandMenuClosingState, true);
|
set(isCommandMenuClosingState, true);
|
||||||
set(isDragSelectionStartEnabledState, true);
|
set(isDragSelectionStartEnabledState, true);
|
||||||
closeAnyOpenDropdown();
|
closeAnyOpenDropdown();
|
||||||
goBackToPreviousHotkeyScope();
|
goBackToPreviousHotkeyScope(COMMAND_MENU_COMPONENT_INSTANCE_ID);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[closeAnyOpenDropdown, goBackToPreviousHotkeyScope],
|
[closeAnyOpenDropdown, goBackToPreviousHotkeyScope],
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import { CommandMenuHotkeyScope } from '@/command-menu/types/CommandMenuHotkeySc
|
|||||||
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||||
import { useKeyboardShortcutMenu } from '@/keyboard-shortcut-menu/hooks/useKeyboardShortcutMenu';
|
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 { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
import { isNonEmptyString } from '@sniptt/guards';
|
import { isNonEmptyString } from '@sniptt/guards';
|
||||||
@ -36,21 +36,23 @@ export const useCommandMenuHotKeys = () => {
|
|||||||
COMMAND_MENU_COMPONENT_INSTANCE_ID,
|
COMMAND_MENU_COMPONENT_INSTANCE_ID,
|
||||||
);
|
);
|
||||||
|
|
||||||
useScopedHotkeys(
|
useGlobalHotkeys(
|
||||||
'ctrl+k,meta+k',
|
'ctrl+k,meta+k',
|
||||||
() => {
|
() => {
|
||||||
closeKeyboardShortcutMenu();
|
closeKeyboardShortcutMenu();
|
||||||
toggleCommandMenu();
|
toggleCommandMenu();
|
||||||
},
|
},
|
||||||
|
true,
|
||||||
AppHotkeyScope.CommandMenu,
|
AppHotkeyScope.CommandMenu,
|
||||||
[closeKeyboardShortcutMenu, toggleCommandMenu],
|
[closeKeyboardShortcutMenu, toggleCommandMenu],
|
||||||
);
|
);
|
||||||
|
|
||||||
useScopedHotkeys(
|
useGlobalHotkeys(
|
||||||
['/'],
|
['/'],
|
||||||
() => {
|
() => {
|
||||||
openRecordsSearchPage();
|
openRecordsSearchPage();
|
||||||
},
|
},
|
||||||
|
false,
|
||||||
AppHotkeyScope.KeyboardShortcutMenu,
|
AppHotkeyScope.KeyboardShortcutMenu,
|
||||||
[openRecordsSearchPage],
|
[openRecordsSearchPage],
|
||||||
{
|
{
|
||||||
@ -58,16 +60,17 @@ export const useCommandMenuHotKeys = () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
useScopedHotkeys(
|
useGlobalHotkeys(
|
||||||
[Key.Escape],
|
[Key.Escape],
|
||||||
() => {
|
() => {
|
||||||
goBackFromCommandMenu();
|
goBackFromCommandMenu();
|
||||||
},
|
},
|
||||||
|
true,
|
||||||
CommandMenuHotkeyScope.CommandMenuFocused,
|
CommandMenuHotkeyScope.CommandMenuFocused,
|
||||||
[goBackFromCommandMenu],
|
[goBackFromCommandMenu],
|
||||||
);
|
);
|
||||||
|
|
||||||
useScopedHotkeys(
|
useGlobalHotkeys(
|
||||||
[Key.Backspace, Key.Delete],
|
[Key.Backspace, Key.Delete],
|
||||||
() => {
|
() => {
|
||||||
if (isNonEmptyString(commandMenuSearch)) {
|
if (isNonEmptyString(commandMenuSearch)) {
|
||||||
@ -88,6 +91,7 @@ export const useCommandMenuHotKeys = () => {
|
|||||||
goBackFromCommandMenu();
|
goBackFromCommandMenu();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
true,
|
||||||
CommandMenuHotkeyScope.CommandMenuFocused,
|
CommandMenuHotkeyScope.CommandMenuFocused,
|
||||||
[
|
[
|
||||||
commandMenuPage,
|
commandMenuPage,
|
||||||
|
|||||||
@ -27,9 +27,7 @@ export type CommandMenuNavigationStackItem = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useNavigateCommandMenu = () => {
|
export const useNavigateCommandMenu = () => {
|
||||||
const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope(
|
const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope();
|
||||||
COMMAND_MENU_COMPONENT_INSTANCE_ID,
|
|
||||||
);
|
|
||||||
|
|
||||||
const { copyContextStoreStates } = useCopyContextStoreStates();
|
const { copyContextStoreStates } = useCopyContextStoreStates();
|
||||||
|
|
||||||
@ -55,12 +53,13 @@ export const useNavigateCommandMenu = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setHotkeyScopeAndMemorizePreviousScope(
|
setHotkeyScopeAndMemorizePreviousScope({
|
||||||
CommandMenuHotkeyScope.CommandMenuFocused,
|
scope: CommandMenuHotkeyScope.CommandMenuFocused,
|
||||||
{
|
customScopes: {
|
||||||
commandMenuOpen: true,
|
commandMenuOpen: true,
|
||||||
},
|
},
|
||||||
);
|
memoizeKey: COMMAND_MENU_COMPONENT_INSTANCE_ID,
|
||||||
|
});
|
||||||
|
|
||||||
copyContextStoreStates({
|
copyContextStoreStates({
|
||||||
instanceIdToCopyFrom: MAIN_CONTEXT_STORE_INSTANCE_ID,
|
instanceIdToCopyFrom: MAIN_CONTEXT_STORE_INSTANCE_ID,
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
|
||||||
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
|
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
|
||||||
|
|
||||||
import { useKeyboardShortcutMenu } from '../hooks/useKeyboardShortcutMenu';
|
import { useKeyboardShortcutMenu } from '../hooks/useKeyboardShortcutMenu';
|
||||||
import { isKeyboardShortcutMenuOpenedState } from '../states/isKeyboardShortcutMenuOpenedState';
|
import { isKeyboardShortcutMenuOpenedState } from '../states/isKeyboardShortcutMenuOpenedState';
|
||||||
|
|
||||||
import { KeyboardShortcutMenuOpenContent } from '@/keyboard-shortcut-menu/components/KeyboardShortcutMenuOpenContent';
|
import { KeyboardShortcutMenuOpenContent } from '@/keyboard-shortcut-menu/components/KeyboardShortcutMenuOpenContent';
|
||||||
|
import { useGlobalHotkeys } from '@/ui/utilities/hotkey/hooks/useGlobalHotkeys';
|
||||||
|
|
||||||
export const KeyboardShortcutMenu = () => {
|
export const KeyboardShortcutMenu = () => {
|
||||||
const { toggleKeyboardShortcutMenu } = useKeyboardShortcutMenu();
|
const { toggleKeyboardShortcutMenu } = useKeyboardShortcutMenu();
|
||||||
@ -16,12 +16,13 @@ export const KeyboardShortcutMenu = () => {
|
|||||||
);
|
);
|
||||||
const { closeCommandMenu } = useCommandMenu();
|
const { closeCommandMenu } = useCommandMenu();
|
||||||
|
|
||||||
useScopedHotkeys(
|
useGlobalHotkeys(
|
||||||
'shift+?,meta+?',
|
'shift+?,meta+?',
|
||||||
() => {
|
() => {
|
||||||
closeCommandMenu();
|
closeCommandMenu();
|
||||||
toggleKeyboardShortcutMenu();
|
toggleKeyboardShortcutMenu();
|
||||||
},
|
},
|
||||||
|
true,
|
||||||
AppHotkeyScope.KeyboardShortcutMenu,
|
AppHotkeyScope.KeyboardShortcutMenu,
|
||||||
[toggleKeyboardShortcutMenu],
|
[toggleKeyboardShortcutMenu],
|
||||||
);
|
);
|
||||||
|
|||||||
@ -2,11 +2,11 @@ import { Key } from 'ts-key-enum';
|
|||||||
|
|
||||||
import { KEYBOARD_SHORTCUTS_GENERAL } from '@/keyboard-shortcut-menu/constants/KeyboardShortcutsGeneral';
|
import { KEYBOARD_SHORTCUTS_GENERAL } from '@/keyboard-shortcut-menu/constants/KeyboardShortcutsGeneral';
|
||||||
import { KEYBOARD_SHORTCUTS_TABLE } from '@/keyboard-shortcut-menu/constants/KeyboardShortcutsTable';
|
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 { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
|
||||||
|
|
||||||
import { useKeyboardShortcutMenu } from '../hooks/useKeyboardShortcutMenu';
|
import { useKeyboardShortcutMenu } from '../hooks/useKeyboardShortcutMenu';
|
||||||
|
|
||||||
|
import { useGlobalHotkeys } from '@/ui/utilities/hotkey/hooks/useGlobalHotkeys';
|
||||||
import { KeyboardMenuDialog } from './KeyboardShortcutMenuDialog';
|
import { KeyboardMenuDialog } from './KeyboardShortcutMenuDialog';
|
||||||
import { KeyboardMenuGroup } from './KeyboardShortcutMenuGroup';
|
import { KeyboardMenuGroup } from './KeyboardShortcutMenuGroup';
|
||||||
import { KeyboardMenuItem } from './KeyboardShortcutMenuItem';
|
import { KeyboardMenuItem } from './KeyboardShortcutMenuItem';
|
||||||
@ -15,11 +15,12 @@ export const KeyboardShortcutMenuOpenContent = () => {
|
|||||||
const { toggleKeyboardShortcutMenu, closeKeyboardShortcutMenu } =
|
const { toggleKeyboardShortcutMenu, closeKeyboardShortcutMenu } =
|
||||||
useKeyboardShortcutMenu();
|
useKeyboardShortcutMenu();
|
||||||
|
|
||||||
useScopedHotkeys(
|
useGlobalHotkeys(
|
||||||
[Key.Escape],
|
[Key.Escape],
|
||||||
() => {
|
() => {
|
||||||
closeKeyboardShortcutMenu();
|
closeKeyboardShortcutMenu();
|
||||||
},
|
},
|
||||||
|
false,
|
||||||
AppHotkeyScope.KeyboardShortcutMenuOpen,
|
AppHotkeyScope.KeyboardShortcutMenuOpen,
|
||||||
[closeKeyboardShortcutMenu],
|
[closeKeyboardShortcutMenu],
|
||||||
);
|
);
|
||||||
|
|||||||
@ -48,18 +48,18 @@ describe('useKeyboardShortcutMenu', () => {
|
|||||||
result.current.toggleKeyboardShortcutMenu();
|
result.current.toggleKeyboardShortcutMenu();
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockSetHotkeyScopeAndMemorizePreviousScope).toHaveBeenCalledWith(
|
expect(mockSetHotkeyScopeAndMemorizePreviousScope).toHaveBeenCalledWith({
|
||||||
AppHotkeyScope.KeyboardShortcutMenu,
|
scope: AppHotkeyScope.KeyboardShortcutMenu,
|
||||||
);
|
});
|
||||||
expect(result.current.isKeyboardShortcutMenuOpened).toBe(true);
|
expect(result.current.isKeyboardShortcutMenuOpened).toBe(true);
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
result.current.toggleKeyboardShortcutMenu();
|
result.current.toggleKeyboardShortcutMenu();
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockSetHotkeyScopeAndMemorizePreviousScope).toHaveBeenCalledWith(
|
expect(mockSetHotkeyScopeAndMemorizePreviousScope).toHaveBeenCalledWith({
|
||||||
AppHotkeyScope.KeyboardShortcutMenu,
|
scope: AppHotkeyScope.KeyboardShortcutMenu,
|
||||||
);
|
});
|
||||||
expect(result.current.isKeyboardShortcutMenuOpened).toBe(false);
|
expect(result.current.isKeyboardShortcutMenuOpened).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -69,9 +69,9 @@ describe('useKeyboardShortcutMenu', () => {
|
|||||||
result.current.openKeyboardShortcutMenu();
|
result.current.openKeyboardShortcutMenu();
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockSetHotkeyScopeAndMemorizePreviousScope).toHaveBeenCalledWith(
|
expect(mockSetHotkeyScopeAndMemorizePreviousScope).toHaveBeenCalledWith({
|
||||||
AppHotkeyScope.KeyboardShortcutMenu,
|
scope: AppHotkeyScope.KeyboardShortcutMenu,
|
||||||
);
|
});
|
||||||
expect(result.current.isKeyboardShortcutMenuOpened).toBe(true);
|
expect(result.current.isKeyboardShortcutMenuOpened).toBe(true);
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
|
|||||||
@ -15,9 +15,9 @@ export const useKeyboardShortcutMenu = () => {
|
|||||||
({ set }) =>
|
({ set }) =>
|
||||||
() => {
|
() => {
|
||||||
set(isKeyboardShortcutMenuOpenedState, true);
|
set(isKeyboardShortcutMenuOpenedState, true);
|
||||||
setHotkeyScopeAndMemorizePreviousScope(
|
setHotkeyScopeAndMemorizePreviousScope({
|
||||||
AppHotkeyScope.KeyboardShortcutMenu,
|
scope: AppHotkeyScope.KeyboardShortcutMenu,
|
||||||
);
|
});
|
||||||
},
|
},
|
||||||
[setHotkeyScopeAndMemorizePreviousScope],
|
[setHotkeyScopeAndMemorizePreviousScope],
|
||||||
);
|
);
|
||||||
|
|||||||
@ -52,7 +52,9 @@ export const RecordBoardHotkeyEffect = () => {
|
|||||||
useScopedHotkeys(
|
useScopedHotkeys(
|
||||||
Key.ArrowLeft,
|
Key.ArrowLeft,
|
||||||
() => {
|
() => {
|
||||||
setHotkeyScopeAndMemorizePreviousScope(BoardHotkeyScope.BoardFocus);
|
setHotkeyScopeAndMemorizePreviousScope({
|
||||||
|
scope: BoardHotkeyScope.BoardFocus,
|
||||||
|
});
|
||||||
move('left');
|
move('left');
|
||||||
},
|
},
|
||||||
RecordIndexHotkeyScope.RecordIndex,
|
RecordIndexHotkeyScope.RecordIndex,
|
||||||
@ -61,7 +63,9 @@ export const RecordBoardHotkeyEffect = () => {
|
|||||||
useScopedHotkeys(
|
useScopedHotkeys(
|
||||||
Key.ArrowRight,
|
Key.ArrowRight,
|
||||||
() => {
|
() => {
|
||||||
setHotkeyScopeAndMemorizePreviousScope(BoardHotkeyScope.BoardFocus);
|
setHotkeyScopeAndMemorizePreviousScope({
|
||||||
|
scope: BoardHotkeyScope.BoardFocus,
|
||||||
|
});
|
||||||
move('right');
|
move('right');
|
||||||
},
|
},
|
||||||
RecordIndexHotkeyScope.RecordIndex,
|
RecordIndexHotkeyScope.RecordIndex,
|
||||||
@ -70,7 +74,9 @@ export const RecordBoardHotkeyEffect = () => {
|
|||||||
useScopedHotkeys(
|
useScopedHotkeys(
|
||||||
Key.ArrowUp,
|
Key.ArrowUp,
|
||||||
() => {
|
() => {
|
||||||
setHotkeyScopeAndMemorizePreviousScope(BoardHotkeyScope.BoardFocus);
|
setHotkeyScopeAndMemorizePreviousScope({
|
||||||
|
scope: BoardHotkeyScope.BoardFocus,
|
||||||
|
});
|
||||||
move('up');
|
move('up');
|
||||||
},
|
},
|
||||||
RecordIndexHotkeyScope.RecordIndex,
|
RecordIndexHotkeyScope.RecordIndex,
|
||||||
@ -79,7 +85,9 @@ export const RecordBoardHotkeyEffect = () => {
|
|||||||
useScopedHotkeys(
|
useScopedHotkeys(
|
||||||
Key.ArrowDown,
|
Key.ArrowDown,
|
||||||
() => {
|
() => {
|
||||||
setHotkeyScopeAndMemorizePreviousScope(BoardHotkeyScope.BoardFocus);
|
setHotkeyScopeAndMemorizePreviousScope({
|
||||||
|
scope: BoardHotkeyScope.BoardFocus,
|
||||||
|
});
|
||||||
move('down');
|
move('down');
|
||||||
},
|
},
|
||||||
RecordIndexHotkeyScope.RecordIndex,
|
RecordIndexHotkeyScope.RecordIndex,
|
||||||
|
|||||||
@ -79,12 +79,12 @@ export const RecordBoardColumnHeader = () => {
|
|||||||
|
|
||||||
const handleBoardColumnMenuOpen = () => {
|
const handleBoardColumnMenuOpen = () => {
|
||||||
setIsBoardColumnMenuOpen(true);
|
setIsBoardColumnMenuOpen(true);
|
||||||
setHotkeyScopeAndMemorizePreviousScope(
|
setHotkeyScopeAndMemorizePreviousScope({
|
||||||
RecordBoardColumnHotkeyScope.BoardColumn,
|
scope: RecordBoardColumnHotkeyScope.BoardColumn,
|
||||||
{
|
customScopes: {
|
||||||
goto: false,
|
goto: false,
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBoardColumnMenuClose = () => {
|
const handleBoardColumnMenuClose = () => {
|
||||||
|
|||||||
@ -60,9 +60,9 @@ export const FormFieldInputInnerContainer = forwardRef(
|
|||||||
onFocus?.(e);
|
onFocus?.(e);
|
||||||
|
|
||||||
if (!preventSetHotkeyScope) {
|
if (!preventSetHotkeyScope) {
|
||||||
setHotkeyScopeAndMemorizePreviousScope(
|
setHotkeyScopeAndMemorizePreviousScope({
|
||||||
FormFieldInputHotKeyScope.FormFieldInput,
|
scope: FormFieldInputHotKeyScope.FormFieldInput,
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -119,7 +119,9 @@ export const FormMultiSelectFieldInput = ({
|
|||||||
editingMode: 'edit',
|
editingMode: 'edit',
|
||||||
});
|
});
|
||||||
|
|
||||||
setHotkeyScopeAndMemorizePreviousScope(hotkeyScope);
|
setHotkeyScopeAndMemorizePreviousScope({
|
||||||
|
scope: hotkeyScope,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onOptionSelected = (value: FieldMultiSelectValue) => {
|
const onOptionSelected = (value: FieldMultiSelectValue) => {
|
||||||
|
|||||||
@ -31,9 +31,7 @@ export const useOpenFieldInputEditMode = () => {
|
|||||||
const { openActivityTargetCellEditMode } =
|
const { openActivityTargetCellEditMode } =
|
||||||
useOpenActivityTargetCellEditMode();
|
useOpenActivityTargetCellEditMode();
|
||||||
|
|
||||||
const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope(
|
const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope();
|
||||||
INLINE_CELL_HOTKEY_SCOPE_MEMOIZE_KEY,
|
|
||||||
);
|
|
||||||
|
|
||||||
const openFieldInput = useRecoilCallback(
|
const openFieldInput = useRecoilCallback(
|
||||||
({ snapshot }) =>
|
({ snapshot }) =>
|
||||||
@ -105,10 +103,11 @@ export const useOpenFieldInputEditMode = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setHotkeyScopeAndMemorizePreviousScope(
|
setHotkeyScopeAndMemorizePreviousScope({
|
||||||
DEFAULT_CELL_SCOPE.scope,
|
scope: DEFAULT_CELL_SCOPE.scope,
|
||||||
DEFAULT_CELL_SCOPE.customScopes,
|
customScopes: DEFAULT_CELL_SCOPE.customScopes,
|
||||||
);
|
memoizeKey: INLINE_CELL_HOTKEY_SCOPE_MEMOIZE_KEY,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
openActivityTargetCellEditMode,
|
openActivityTargetCellEditMode,
|
||||||
|
|||||||
@ -88,9 +88,9 @@ export const useOpenRelationFromManyFieldInput = () => {
|
|||||||
forcePickableMorphItems: pickableMorphItems,
|
forcePickableMorphItems: pickableMorphItems,
|
||||||
});
|
});
|
||||||
|
|
||||||
setHotkeyScopeAndMemorizePreviousScope(
|
setHotkeyScopeAndMemorizePreviousScope({
|
||||||
MultipleRecordPickerHotkeyScope.MultipleRecordPicker,
|
scope: MultipleRecordPickerHotkeyScope.MultipleRecordPicker,
|
||||||
);
|
});
|
||||||
},
|
},
|
||||||
[performSearch, setHotkeyScopeAndMemorizePreviousScope],
|
[performSearch, setHotkeyScopeAndMemorizePreviousScope],
|
||||||
);
|
);
|
||||||
|
|||||||
@ -34,9 +34,9 @@ export const useOpenRelationToOneFieldInput = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
setHotkeyScopeAndMemorizePreviousScope(
|
setHotkeyScopeAndMemorizePreviousScope({
|
||||||
SingleRecordPickerHotkeyScope.SingleRecordPicker,
|
scope: SingleRecordPickerHotkeyScope.SingleRecordPicker,
|
||||||
);
|
});
|
||||||
},
|
},
|
||||||
[setHotkeyScopeAndMemorizePreviousScope],
|
[setHotkeyScopeAndMemorizePreviousScope],
|
||||||
);
|
);
|
||||||
|
|||||||
@ -35,9 +35,7 @@ export const useInlineCell = (
|
|||||||
const { goBackToPreviousDropdownFocusId } =
|
const { goBackToPreviousDropdownFocusId } =
|
||||||
useGoBackToPreviousDropdownFocusId();
|
useGoBackToPreviousDropdownFocusId();
|
||||||
|
|
||||||
const { goBackToPreviousHotkeyScope } = usePreviousHotkeyScope(
|
const { goBackToPreviousHotkeyScope } = usePreviousHotkeyScope();
|
||||||
INLINE_CELL_HOTKEY_SCOPE_MEMOIZE_KEY,
|
|
||||||
);
|
|
||||||
|
|
||||||
const initFieldInputDraftValue = useInitDraftValueV2();
|
const initFieldInputDraftValue = useInitDraftValueV2();
|
||||||
|
|
||||||
@ -45,7 +43,7 @@ export const useInlineCell = (
|
|||||||
onCloseEditMode?.();
|
onCloseEditMode?.();
|
||||||
setIsInlineCellInEditMode(false);
|
setIsInlineCellInEditMode(false);
|
||||||
|
|
||||||
goBackToPreviousHotkeyScope();
|
goBackToPreviousHotkeyScope(INLINE_CELL_HOTKEY_SCOPE_MEMOIZE_KEY);
|
||||||
|
|
||||||
goBackToPreviousDropdownFocusId();
|
goBackToPreviousDropdownFocusId();
|
||||||
};
|
};
|
||||||
|
|||||||
@ -31,7 +31,9 @@ export const useMapKeyboardToFocus = (recordTableId?: string) => {
|
|||||||
useScopedHotkeys(
|
useScopedHotkeys(
|
||||||
[Key.ArrowUp],
|
[Key.ArrowUp],
|
||||||
() => {
|
() => {
|
||||||
setHotkeyScopeAndMemorizePreviousScope(TableHotkeyScope.TableFocus);
|
setHotkeyScopeAndMemorizePreviousScope({
|
||||||
|
scope: TableHotkeyScope.TableFocus,
|
||||||
|
});
|
||||||
move('up');
|
move('up');
|
||||||
},
|
},
|
||||||
RecordIndexHotkeyScope.RecordIndex,
|
RecordIndexHotkeyScope.RecordIndex,
|
||||||
@ -41,7 +43,9 @@ export const useMapKeyboardToFocus = (recordTableId?: string) => {
|
|||||||
useScopedHotkeys(
|
useScopedHotkeys(
|
||||||
[Key.ArrowDown],
|
[Key.ArrowDown],
|
||||||
() => {
|
() => {
|
||||||
setHotkeyScopeAndMemorizePreviousScope(TableHotkeyScope.TableFocus);
|
setHotkeyScopeAndMemorizePreviousScope({
|
||||||
|
scope: TableHotkeyScope.TableFocus,
|
||||||
|
});
|
||||||
move('down');
|
move('down');
|
||||||
},
|
},
|
||||||
RecordIndexHotkeyScope.RecordIndex,
|
RecordIndexHotkeyScope.RecordIndex,
|
||||||
|
|||||||
@ -42,16 +42,15 @@ export const RecordTitleCellSingleTextDisplayMode = () => {
|
|||||||
|
|
||||||
const { openInlineCell } = useInlineCell();
|
const { openInlineCell } = useInlineCell();
|
||||||
|
|
||||||
const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope(
|
const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope();
|
||||||
INLINE_CELL_HOTKEY_SCOPE_MEMOIZE_KEY,
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledDiv
|
<StyledDiv
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setHotkeyScopeAndMemorizePreviousScope(
|
setHotkeyScopeAndMemorizePreviousScope({
|
||||||
TitleInputHotkeyScope.TitleInput,
|
scope: TitleInputHotkeyScope.TitleInput,
|
||||||
);
|
memoizeKey: INLINE_CELL_HOTKEY_SCOPE_MEMOIZE_KEY,
|
||||||
|
});
|
||||||
openInlineCell();
|
openInlineCell();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -45,15 +45,14 @@ export const RecordTitleFullNameFieldDisplay = () => {
|
|||||||
.join(' ')
|
.join(' ')
|
||||||
.trim();
|
.trim();
|
||||||
|
|
||||||
const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope(
|
const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope();
|
||||||
INLINE_CELL_HOTKEY_SCOPE_MEMOIZE_KEY,
|
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
<StyledDiv
|
<StyledDiv
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setHotkeyScopeAndMemorizePreviousScope(
|
setHotkeyScopeAndMemorizePreviousScope({
|
||||||
TitleInputHotkeyScope.TitleInput,
|
scope: TitleInputHotkeyScope.TitleInput,
|
||||||
);
|
memoizeKey: INLINE_CELL_HOTKEY_SCOPE_MEMOIZE_KEY,
|
||||||
|
});
|
||||||
openInlineCell();
|
openInlineCell();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -18,7 +18,7 @@ export const useRecordTitleCell = () => {
|
|||||||
const {
|
const {
|
||||||
setHotkeyScopeAndMemorizePreviousScope,
|
setHotkeyScopeAndMemorizePreviousScope,
|
||||||
goBackToPreviousHotkeyScope,
|
goBackToPreviousHotkeyScope,
|
||||||
} = usePreviousHotkeyScope(INLINE_CELL_HOTKEY_SCOPE_MEMOIZE_KEY);
|
} = usePreviousHotkeyScope();
|
||||||
|
|
||||||
const closeRecordTitleCell = useRecoilCallback(
|
const closeRecordTitleCell = useRecoilCallback(
|
||||||
({ set }) =>
|
({ set }) =>
|
||||||
@ -38,7 +38,7 @@ export const useRecordTitleCell = () => {
|
|||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
goBackToPreviousHotkeyScope();
|
goBackToPreviousHotkeyScope(INLINE_CELL_HOTKEY_SCOPE_MEMOIZE_KEY);
|
||||||
|
|
||||||
goBackToPreviousDropdownFocusId();
|
goBackToPreviousDropdownFocusId();
|
||||||
},
|
},
|
||||||
@ -61,14 +61,16 @@ export const useRecordTitleCell = () => {
|
|||||||
customEditHotkeyScopeForField?: HotkeyScope;
|
customEditHotkeyScopeForField?: HotkeyScope;
|
||||||
}) => {
|
}) => {
|
||||||
if (isDefined(customEditHotkeyScopeForField)) {
|
if (isDefined(customEditHotkeyScopeForField)) {
|
||||||
setHotkeyScopeAndMemorizePreviousScope(
|
setHotkeyScopeAndMemorizePreviousScope({
|
||||||
customEditHotkeyScopeForField.scope,
|
scope: customEditHotkeyScopeForField.scope,
|
||||||
customEditHotkeyScopeForField.customScopes,
|
customScopes: customEditHotkeyScopeForField.customScopes,
|
||||||
);
|
memoizeKey: INLINE_CELL_HOTKEY_SCOPE_MEMOIZE_KEY,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
setHotkeyScopeAndMemorizePreviousScope(
|
setHotkeyScopeAndMemorizePreviousScope({
|
||||||
TitleInputHotkeyScope.TitleInput,
|
scope: TitleInputHotkeyScope.TitleInput,
|
||||||
);
|
memoizeKey: INLINE_CELL_HOTKEY_SCOPE_MEMOIZE_KEY,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const recordTitleCellId = getRecordTitleCellId(
|
const recordTitleCellId = getRecordTitleCellId(
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { useCallback } from 'react';
|
|
||||||
import { Key } from 'ts-key-enum';
|
import { Key } from 'ts-key-enum';
|
||||||
|
|
||||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
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 { 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 { isDefined } from 'twenty-shared/utils';
|
||||||
import { Button } from 'twenty-ui/input';
|
import { Button } from 'twenty-ui/input';
|
||||||
import { DialogHotkeyScope } from '../types/DialogHotkeyScope';
|
import { DialogHotkeyScope } from '../types/DialogHotkeyScope';
|
||||||
@ -69,7 +71,6 @@ export type DialogProps = React.ComponentPropsWithoutRef<typeof motion.div> & {
|
|||||||
title?: string;
|
title?: string;
|
||||||
message?: string;
|
message?: string;
|
||||||
buttons?: DialogButtonOptions[];
|
buttons?: DialogButtonOptions[];
|
||||||
allowDismiss?: boolean;
|
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
@ -79,16 +80,11 @@ export const Dialog = ({
|
|||||||
title,
|
title,
|
||||||
message,
|
message,
|
||||||
buttons = [],
|
buttons = [],
|
||||||
allowDismiss = true,
|
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
onClose,
|
onClose,
|
||||||
id,
|
id,
|
||||||
}: DialogProps) => {
|
}: DialogProps) => {
|
||||||
const closeSnackbar = useCallback(() => {
|
|
||||||
onClose && onClose();
|
|
||||||
}, [onClose]);
|
|
||||||
|
|
||||||
const dialogVariants = {
|
const dialogVariants = {
|
||||||
open: { opacity: 1 },
|
open: { opacity: 1 },
|
||||||
closed: { opacity: 0 },
|
closed: { opacity: 0 },
|
||||||
@ -108,7 +104,7 @@ export const Dialog = ({
|
|||||||
|
|
||||||
if (isDefined(confirmButton)) {
|
if (isDefined(confirmButton)) {
|
||||||
confirmButton?.onClick?.(event);
|
confirmButton?.onClick?.(event);
|
||||||
closeSnackbar();
|
onClose?.();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
DialogHotkeyScope.Dialog,
|
DialogHotkeyScope.Dialog,
|
||||||
@ -119,30 +115,35 @@ export const Dialog = ({
|
|||||||
Key.Escape,
|
Key.Escape,
|
||||||
(event: KeyboardEvent) => {
|
(event: KeyboardEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
closeSnackbar();
|
onClose?.();
|
||||||
},
|
},
|
||||||
DialogHotkeyScope.Dialog,
|
DialogHotkeyScope.Dialog,
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const dialogRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useListenClickOutside({
|
||||||
|
refs: [dialogRef],
|
||||||
|
callback: () => {
|
||||||
|
onClose?.();
|
||||||
|
},
|
||||||
|
listenerId: DIALOG_LISTENER_ID,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledDialogOverlay
|
<StyledDialogOverlay
|
||||||
variants={dialogVariants}
|
variants={dialogVariants}
|
||||||
initial="closed"
|
initial="closed"
|
||||||
animate="open"
|
animate="open"
|
||||||
exit="closed"
|
exit="closed"
|
||||||
onClick={(e) => {
|
|
||||||
if (allowDismiss) {
|
|
||||||
e.stopPropagation();
|
|
||||||
closeSnackbar();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className={className}
|
className={className}
|
||||||
>
|
>
|
||||||
<StyledDialogContainer
|
<StyledDialogContainer
|
||||||
variants={containerVariants}
|
variants={containerVariants}
|
||||||
transition={{ damping: 15, stiffness: 100 }}
|
transition={{ damping: 15, stiffness: 100 }}
|
||||||
id={id}
|
id={id}
|
||||||
|
ref={dialogRef}
|
||||||
>
|
>
|
||||||
{title && <StyledDialogTitle>{title}</StyledDialogTitle>}
|
{title && <StyledDialogTitle>{title}</StyledDialogTitle>}
|
||||||
{message && <StyledDialogMessage>{message}</StyledDialogMessage>}
|
{message && <StyledDialogMessage>{message}</StyledDialogMessage>}
|
||||||
@ -150,8 +151,8 @@ export const Dialog = ({
|
|||||||
{buttons.map(({ accent, onClick, role, title: key, variant }) => (
|
{buttons.map(({ accent, onClick, role, title: key, variant }) => (
|
||||||
<StyledDialogButton
|
<StyledDialogButton
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
|
onClose?.();
|
||||||
onClick?.(event);
|
onClick?.(event);
|
||||||
closeSnackbar();
|
|
||||||
}}
|
}}
|
||||||
fullWidth={true}
|
fullWidth={true}
|
||||||
variant={variant ?? 'secondary'}
|
variant={variant ?? 'secondary'}
|
||||||
|
|||||||
@ -15,6 +15,7 @@ export const DialogManager = ({ children }: React.PropsWithChildren) => {
|
|||||||
{dialogInternal.queue.map(({ buttons, children, id, message, title }) => (
|
{dialogInternal.queue.map(({ buttons, children, id, message, title }) => (
|
||||||
<Dialog
|
<Dialog
|
||||||
key={id}
|
key={id}
|
||||||
|
className="dialog-manager-dialog"
|
||||||
{...{ title, message, buttons, id, children }}
|
{...{ title, message, buttons, id, children }}
|
||||||
onClose={() => closeDialog(id)}
|
onClose={() => closeDialog(id)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { useEffect } from 'react';
|
|||||||
|
|
||||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
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 { useDialogManagerScopedStates } from '../hooks/internal/useDialogManagerScopedStates';
|
||||||
import { DialogHotkeyScope } from '../types/DialogHotkeyScope';
|
import { DialogHotkeyScope } from '../types/DialogHotkeyScope';
|
||||||
|
|
||||||
@ -15,7 +16,10 @@ export const DialogManagerEffect = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setHotkeyScopeAndMemorizePreviousScope(DialogHotkeyScope.Dialog);
|
setHotkeyScopeAndMemorizePreviousScope({
|
||||||
|
scope: DialogHotkeyScope.Dialog,
|
||||||
|
memoizeKey: DIALOG_MANAGER_HOTKEY_SCOPE_MEMOIZE_KEY,
|
||||||
|
});
|
||||||
}, [dialogInternal.queue, setHotkeyScopeAndMemorizePreviousScope]);
|
}, [dialogInternal.queue, setHotkeyScopeAndMemorizePreviousScope]);
|
||||||
|
|
||||||
return <></>;
|
return <></>;
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
export const DIALOG_LISTENER_ID = 'DIALOG_LISTENER_ID';
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export const DIALOG_MANAGER_HOTKEY_SCOPE_MEMOIZE_KEY = 'dialog-manager';
|
||||||
@ -4,6 +4,7 @@ import { v4 } from 'uuid';
|
|||||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||||
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
|
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 { DialogManagerScopeInternalContext } from '../scopes/scope-internal-context/DialogManagerScopeInternalContext';
|
||||||
import { dialogInternalScopedState } from '../states/dialogInternalScopedState';
|
import { dialogInternalScopedState } from '../states/dialogInternalScopedState';
|
||||||
import { DialogOptions } from '../types/DialogOptions';
|
import { DialogOptions } from '../types/DialogOptions';
|
||||||
@ -25,9 +26,10 @@ export const useDialogManager = (props?: useDialogManagerProps) => {
|
|||||||
(id: string) => {
|
(id: string) => {
|
||||||
set(dialogInternalScopedState({ scopeId: scopeId }), (prevState) => ({
|
set(dialogInternalScopedState({ scopeId: scopeId }), (prevState) => ({
|
||||||
...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],
|
[goBackToPreviousHotkeyScope, scopeId],
|
||||||
);
|
);
|
||||||
|
|||||||
@ -7,9 +7,9 @@ import { Key } from 'ts-key-enum';
|
|||||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
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 { IconArrowRight } from 'twenty-ui/display';
|
||||||
|
import { Button, RoundedIconButton } from 'twenty-ui/input';
|
||||||
|
import { InputHotkeyScope } from '../types/InputHotkeyScope';
|
||||||
|
|
||||||
const MAX_ROWS = 5;
|
const MAX_ROWS = 5;
|
||||||
|
|
||||||
@ -197,7 +197,9 @@ export const AutosizeTextInput = ({
|
|||||||
const handleFocus = () => {
|
const handleFocus = () => {
|
||||||
onFocus?.();
|
onFocus?.();
|
||||||
setIsFocused(true);
|
setIsFocused(true);
|
||||||
setHotkeyScopeAndMemorizePreviousScope(InputHotkeyScope.TextInput);
|
setHotkeyScopeAndMemorizePreviousScope({
|
||||||
|
scope: InputHotkeyScope.TextInput,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBlur = () => {
|
const handleBlur = () => {
|
||||||
|
|||||||
@ -197,9 +197,9 @@ export const IconPicker = ({
|
|||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<div
|
<div
|
||||||
onMouseEnter={() => {
|
onMouseEnter={() => {
|
||||||
setHotkeyScopeAndMemorizePreviousScope(
|
setHotkeyScopeAndMemorizePreviousScope({
|
||||||
IconPickerHotkeyScope.IconPicker,
|
scope: IconPickerHotkeyScope.IconPicker,
|
||||||
);
|
});
|
||||||
}}
|
}}
|
||||||
onMouseLeave={goBackToPreviousHotkeyScope}
|
onMouseLeave={goBackToPreviousHotkeyScope}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -3,9 +3,9 @@ import { FocusEventHandler, useId } from 'react';
|
|||||||
import TextareaAutosize from 'react-textarea-autosize';
|
import TextareaAutosize from 'react-textarea-autosize';
|
||||||
|
|
||||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||||
|
import { RGBA } from 'twenty-ui/theme';
|
||||||
import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly';
|
import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly';
|
||||||
import { InputHotkeyScope } from '../types/InputHotkeyScope';
|
import { InputHotkeyScope } from '../types/InputHotkeyScope';
|
||||||
import { RGBA } from 'twenty-ui/theme';
|
|
||||||
|
|
||||||
const MAX_ROWS = 5;
|
const MAX_ROWS = 5;
|
||||||
|
|
||||||
@ -89,7 +89,9 @@ export const TextArea = ({
|
|||||||
} = usePreviousHotkeyScope();
|
} = usePreviousHotkeyScope();
|
||||||
|
|
||||||
const handleFocus: FocusEventHandler<HTMLTextAreaElement> = () => {
|
const handleFocus: FocusEventHandler<HTMLTextAreaElement> = () => {
|
||||||
setHotkeyScopeAndMemorizePreviousScope(InputHotkeyScope.TextInput);
|
setHotkeyScopeAndMemorizePreviousScope({
|
||||||
|
scope: InputHotkeyScope.TextInput,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBlur: FocusEventHandler<HTMLTextAreaElement> = () => {
|
const handleBlur: FocusEventHandler<HTMLTextAreaElement> = () => {
|
||||||
|
|||||||
@ -55,7 +55,9 @@ export const TextInput = ({
|
|||||||
setIsFocused(true);
|
setIsFocused(true);
|
||||||
|
|
||||||
if (!disableHotkeys) {
|
if (!disableHotkeys) {
|
||||||
setHotkeyScopeAndMemorizePreviousScope(InputHotkeyScope.TextInput);
|
setHotkeyScopeAndMemorizePreviousScope({
|
||||||
|
scope: InputHotkeyScope.TextInput,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -170,7 +170,9 @@ export const TitleInput = ({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!disabled) {
|
if (!disabled) {
|
||||||
setIsOpened(true);
|
setIsOpened(true);
|
||||||
setHotkeyScopeAndMemorizePreviousScope(hotkeyScope);
|
setHotkeyScopeAndMemorizePreviousScope({
|
||||||
|
scope: hotkeyScope,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -65,10 +65,10 @@ export const useDropdown = (dropdownId?: string) => {
|
|||||||
dropdownHotkeyScopeFromProps ?? dropdownHotkeyScope;
|
dropdownHotkeyScopeFromProps ?? dropdownHotkeyScope;
|
||||||
|
|
||||||
if (isDefined(dropdownHotkeyScopeForOpening)) {
|
if (isDefined(dropdownHotkeyScopeForOpening)) {
|
||||||
setHotkeyScopeAndMemorizePreviousScope(
|
setHotkeyScopeAndMemorizePreviousScope({
|
||||||
dropdownHotkeyScopeForOpening.scope,
|
scope: dropdownHotkeyScopeForOpening.scope,
|
||||||
dropdownHotkeyScopeForOpening.customScopes,
|
customScopes: dropdownHotkeyScopeForOpening.customScopes,
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -56,15 +56,15 @@ export const useDropdownV2 = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (isDefined(customHotkeyScope)) {
|
if (isDefined(customHotkeyScope)) {
|
||||||
setHotkeyScopeAndMemorizePreviousScope(
|
setHotkeyScopeAndMemorizePreviousScope({
|
||||||
customHotkeyScope.scope,
|
scope: customHotkeyScope.scope,
|
||||||
customHotkeyScope.customScopes,
|
customScopes: customHotkeyScope.customScopes,
|
||||||
);
|
});
|
||||||
} else if (isDefined(dropdownHotkeyScope)) {
|
} else if (isDefined(dropdownHotkeyScope)) {
|
||||||
setHotkeyScopeAndMemorizePreviousScope(
|
setHotkeyScopeAndMemorizePreviousScope({
|
||||||
dropdownHotkeyScope.scope,
|
scope: dropdownHotkeyScope.scope,
|
||||||
dropdownHotkeyScope.customScopes,
|
customScopes: dropdownHotkeyScope.customScopes,
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[setHotkeyScopeAndMemorizePreviousScope],
|
[setHotkeyScopeAndMemorizePreviousScope],
|
||||||
|
|||||||
@ -19,7 +19,9 @@ export const useOpenDropdownFromOutside = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
setActiveDropdownFocusIdAndMemorizePrevious(dropdownId);
|
setActiveDropdownFocusIdAndMemorizePrevious(dropdownId);
|
||||||
setHotkeyScopeAndMemorizePreviousScope(dropdownId);
|
setHotkeyScopeAndMemorizePreviousScope({
|
||||||
|
scope: dropdownId,
|
||||||
|
});
|
||||||
|
|
||||||
set(dropdownOpenState, true);
|
set(dropdownOpenState, true);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { ModalHotkeyScope } from '@/ui/layout/modal/components/types/ModalHotkeyScope';
|
import { ModalHotkeyScope } from '@/ui/layout/modal/components/types/ModalHotkeyScope';
|
||||||
import { MODAL_CLICK_OUTSIDE_LISTENER_EXCLUDED_CLASS_NAME } from '@/ui/layout/modal/constants/ModalClickOutsideListenerExcludedClassName';
|
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 { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||||
import { Key } from 'ts-key-enum';
|
import { Key } from 'ts-key-enum';
|
||||||
|
|
||||||
@ -19,27 +19,36 @@ export const ModalHotkeysAndClickOutsideEffect = ({
|
|||||||
onClose,
|
onClose,
|
||||||
modalId,
|
modalId,
|
||||||
}: ModalHotkeysAndClickOutsideEffectProps) => {
|
}: ModalHotkeysAndClickOutsideEffectProps) => {
|
||||||
useScopedHotkeys(
|
useHotkeysOnFocusedElement({
|
||||||
[Key.Enter],
|
keys: [Key.Enter],
|
||||||
() => {
|
callback: () => {
|
||||||
onEnter?.();
|
onEnter?.();
|
||||||
},
|
},
|
||||||
ModalHotkeyScope.ModalFocus,
|
focusId: modalId,
|
||||||
);
|
// TODO: Remove this once we've migrated hotkey scopes to the new api
|
||||||
|
scope: ModalHotkeyScope.ModalFocus,
|
||||||
|
dependencies: [onEnter],
|
||||||
|
});
|
||||||
|
|
||||||
useScopedHotkeys(
|
useHotkeysOnFocusedElement({
|
||||||
[Key.Escape],
|
keys: [Key.Escape],
|
||||||
() => {
|
callback: () => {
|
||||||
if (isClosable && onClose !== undefined) {
|
if (isClosable && onClose !== undefined) {
|
||||||
onClose();
|
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({
|
useListenClickOutside({
|
||||||
refs: [modalRef],
|
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}`,
|
listenerId: `MODAL_CLICK_OUTSIDE_LISTENER_ID_${modalId}`,
|
||||||
callback: () => {
|
callback: () => {
|
||||||
if (isClosable && onClose !== undefined) {
|
if (isClosable && onClose !== undefined) {
|
||||||
|
|||||||
@ -2,6 +2,8 @@ import { Meta, StoryObj } from '@storybook/react';
|
|||||||
import { expect, fn, userEvent, waitFor, within } from '@storybook/test';
|
import { expect, fn, userEvent, waitFor, within } from '@storybook/test';
|
||||||
|
|
||||||
import { ModalHotkeyScope } from '@/ui/layout/modal/components/types/ModalHotkeyScope';
|
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 { currentHotkeyScopeState } from '@/ui/utilities/hotkey/states/internal/currentHotkeyScopeState';
|
||||||
import { internalHotkeysEnabledScopesState } from '@/ui/utilities/hotkey/states/internal/internalHotkeysEnabledScopesState';
|
import { internalHotkeysEnabledScopesState } from '@/ui/utilities/hotkey/states/internal/internalHotkeysEnabledScopesState';
|
||||||
import { ComponentDecorator } from 'twenty-ui/testing';
|
import { ComponentDecorator } from 'twenty-ui/testing';
|
||||||
@ -29,6 +31,20 @@ const initializeState = ({ set }: { set: (atom: any, value: any) => void }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
set(internalHotkeysEnabledScopesState, [ModalHotkeyScope.ModalFocus]);
|
set(internalHotkeysEnabledScopesState, [ModalHotkeyScope.ModalFocus]);
|
||||||
|
|
||||||
|
set(focusStackState, [
|
||||||
|
{
|
||||||
|
focusId: 'confirmation-modal',
|
||||||
|
componentInstance: {
|
||||||
|
componentType: FocusComponentType.MODAL,
|
||||||
|
componentInstanceId: 'confirmation-modal',
|
||||||
|
},
|
||||||
|
globalHotkeysConfig: {
|
||||||
|
enableGlobalHotkeysWithModifiers: true,
|
||||||
|
enableGlobalHotkeysConflictingWithKeyboard: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const meta: Meta<typeof ConfirmationModal> = {
|
const meta: Meta<typeof ConfirmationModal> = {
|
||||||
|
|||||||
@ -2,6 +2,8 @@ import { Meta, StoryObj } from '@storybook/react';
|
|||||||
import { expect, fn, userEvent, waitFor, within } from '@storybook/test';
|
import { expect, fn, userEvent, waitFor, within } from '@storybook/test';
|
||||||
|
|
||||||
import { ModalHotkeyScope } from '@/ui/layout/modal/components/types/ModalHotkeyScope';
|
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 { currentHotkeyScopeState } from '@/ui/utilities/hotkey/states/internal/currentHotkeyScopeState';
|
||||||
import { internalHotkeysEnabledScopesState } from '@/ui/utilities/hotkey/states/internal/internalHotkeysEnabledScopesState';
|
import { internalHotkeysEnabledScopesState } from '@/ui/utilities/hotkey/states/internal/internalHotkeysEnabledScopesState';
|
||||||
import { ComponentDecorator } from 'twenty-ui/testing';
|
import { ComponentDecorator } from 'twenty-ui/testing';
|
||||||
@ -29,6 +31,20 @@ const initializeState = ({ set }: { set: (atom: any, value: any) => void }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
set(internalHotkeysEnabledScopesState, [ModalHotkeyScope.ModalFocus]);
|
set(internalHotkeysEnabledScopesState, [ModalHotkeyScope.ModalFocus]);
|
||||||
|
|
||||||
|
set(focusStackState, [
|
||||||
|
{
|
||||||
|
focusId: 'modal-id',
|
||||||
|
componentInstance: {
|
||||||
|
componentType: FocusComponentType.MODAL,
|
||||||
|
componentInstanceId: 'modal-id',
|
||||||
|
},
|
||||||
|
globalHotkeysConfig: {
|
||||||
|
enableGlobalHotkeysWithModifiers: true,
|
||||||
|
enableGlobalHotkeysConflictingWithKeyboard: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const meta: Meta<typeof Modal> = {
|
const meta: Meta<typeof Modal> = {
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import { RecoilRoot, useRecoilValue } from 'recoil';
|
|||||||
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
||||||
import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpenedComponentState';
|
import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpenedComponentState';
|
||||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
|
||||||
import { act } from 'react';
|
import { act } from 'react';
|
||||||
|
|
||||||
jest.mock('@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope');
|
jest.mock('@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope');
|
||||||
@ -13,13 +12,6 @@ const mockSetHotkeyScopeAndMemorizePreviousScope = jest.fn();
|
|||||||
const mockGoBackToPreviousHotkeyScope = jest.fn();
|
const mockGoBackToPreviousHotkeyScope = jest.fn();
|
||||||
|
|
||||||
const modalId = 'test-modal-id';
|
const modalId = 'test-modal-id';
|
||||||
const customHotkeyScope: HotkeyScope = {
|
|
||||||
scope: 'test-scope',
|
|
||||||
customScopes: {
|
|
||||||
goto: true,
|
|
||||||
commandMenu: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('useModal', () => {
|
describe('useModal', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@ -52,31 +44,6 @@ describe('useModal', () => {
|
|||||||
expect(result.current.isModalOpened).toBe(true);
|
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', () => {
|
it('should close a modal', () => {
|
||||||
const { result } = renderHook(
|
const { result } = renderHook(
|
||||||
() => {
|
() => {
|
||||||
@ -153,29 +120,4 @@ describe('useModal', () => {
|
|||||||
expect(result.current.isModalOpened).toBe(false);
|
expect(result.current.isModalOpened).toBe(false);
|
||||||
expect(mockGoBackToPreviousHotkeyScope).toHaveBeenCalled();
|
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,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,16 +1,13 @@
|
|||||||
import { useRecoilCallback } from 'recoil';
|
|
||||||
|
|
||||||
import { ModalHotkeyScope } from '@/ui/layout/modal/components/types/ModalHotkeyScope';
|
import { ModalHotkeyScope } from '@/ui/layout/modal/components/types/ModalHotkeyScope';
|
||||||
import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpenedComponentState';
|
import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpenedComponentState';
|
||||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
|
||||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
import { useRemoveFocusIdFromFocusStack } from '@/ui/utilities/focus/hooks/useRemoveFocusIdFromFocusStack';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
|
||||||
|
import { useRecoilCallback } from 'recoil';
|
||||||
|
|
||||||
export const useModal = () => {
|
export const useModal = () => {
|
||||||
const {
|
const pushFocusItem = usePushFocusItemToFocusStack();
|
||||||
setHotkeyScopeAndMemorizePreviousScope,
|
const removeFocusId = useRemoveFocusIdFromFocusStack();
|
||||||
goBackToPreviousHotkeyScope,
|
|
||||||
} = usePreviousHotkeyScope('modal');
|
|
||||||
|
|
||||||
const closeModal = useRecoilCallback(
|
const closeModal = useRecoilCallback(
|
||||||
({ set, snapshot }) =>
|
({ set, snapshot }) =>
|
||||||
@ -21,20 +18,26 @@ export const useModal = () => {
|
|||||||
)
|
)
|
||||||
.getValue();
|
.getValue();
|
||||||
|
|
||||||
if (isModalOpen) {
|
if (!isModalOpen) {
|
||||||
goBackToPreviousHotkeyScope();
|
return;
|
||||||
set(
|
|
||||||
isModalOpenedComponentState.atomFamily({ instanceId: modalId }),
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
removeFocusId({
|
||||||
|
focusId: modalId,
|
||||||
|
memoizeKey: modalId,
|
||||||
|
});
|
||||||
|
|
||||||
|
set(
|
||||||
|
isModalOpenedComponentState.atomFamily({ instanceId: modalId }),
|
||||||
|
false,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[goBackToPreviousHotkeyScope],
|
[removeFocusId],
|
||||||
);
|
);
|
||||||
|
|
||||||
const openModal = useRecoilCallback(
|
const openModal = useRecoilCallback(
|
||||||
({ set, snapshot }) =>
|
({ set, snapshot }) =>
|
||||||
(modalId: string, customHotkeyScope?: HotkeyScope) => {
|
(modalId: string) => {
|
||||||
const isModalOpened = snapshot
|
const isModalOpened = snapshot
|
||||||
.getLoadable(
|
.getLoadable(
|
||||||
isModalOpenedComponentState.atomFamily({ instanceId: modalId }),
|
isModalOpenedComponentState.atomFamily({ instanceId: modalId }),
|
||||||
@ -50,26 +53,35 @@ export const useModal = () => {
|
|||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isDefined(customHotkeyScope)) {
|
pushFocusItem({
|
||||||
setHotkeyScopeAndMemorizePreviousScope(
|
focusId: modalId,
|
||||||
customHotkeyScope.scope,
|
component: {
|
||||||
customHotkeyScope.customScopes,
|
type: FocusComponentType.MODAL,
|
||||||
);
|
instanceId: modalId,
|
||||||
} else {
|
},
|
||||||
setHotkeyScopeAndMemorizePreviousScope(ModalHotkeyScope.ModalFocus, {
|
globalHotkeysConfig: {
|
||||||
goto: false,
|
enableGlobalHotkeysWithModifiers: false,
|
||||||
commandMenu: false,
|
enableGlobalHotkeysConflictingWithKeyboard: false,
|
||||||
commandMenuOpen: false,
|
},
|
||||||
keyboardShortcutMenu: 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(
|
const toggleModal = useRecoilCallback(
|
||||||
({ snapshot }) =>
|
({ snapshot }) =>
|
||||||
(modalId: string, customHotkeyScope?: HotkeyScope) => {
|
(modalId: string) => {
|
||||||
const isModalOpen = snapshot
|
const isModalOpen = snapshot
|
||||||
.getLoadable(
|
.getLoadable(
|
||||||
isModalOpenedComponentState.atomFamily({ instanceId: modalId }),
|
isModalOpenedComponentState.atomFamily({ instanceId: modalId }),
|
||||||
@ -79,7 +91,7 @@ export const useModal = () => {
|
|||||||
if (isModalOpen) {
|
if (isModalOpen) {
|
||||||
closeModal(modalId);
|
closeModal(modalId);
|
||||||
} else {
|
} else {
|
||||||
openModal(modalId, customHotkeyScope);
|
openModal(modalId);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[closeModal, openModal],
|
[closeModal, openModal],
|
||||||
|
|||||||
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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<GlobalHotkeysConfig>;
|
||||||
|
// 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],
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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],
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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);
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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);
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
import { selector } from 'recoil';
|
||||||
|
import { focusStackState } from './focusStackState';
|
||||||
|
|
||||||
|
export const currentFocusIdSelector = selector<string | undefined>({
|
||||||
|
key: 'currentFocusIdSelector',
|
||||||
|
get: ({ get }) => {
|
||||||
|
const focusStack = get(focusStackState);
|
||||||
|
|
||||||
|
return focusStack.at(-1)?.focusId;
|
||||||
|
},
|
||||||
|
});
|
||||||
@ -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<GlobalHotkeysConfig>(
|
||||||
|
{
|
||||||
|
key: 'currentGlobalHotkeysConfigSelector',
|
||||||
|
get: ({ get }) => {
|
||||||
|
const focusStack = get(focusStackState);
|
||||||
|
const lastFocusStackItem = focusStack.at(-1);
|
||||||
|
|
||||||
|
if (!isDefined(lastFocusStackItem)) {
|
||||||
|
return DEFAULT_GLOBAL_HOTKEYS_CONFIG;
|
||||||
|
}
|
||||||
|
|
||||||
|
return lastFocusStackItem.globalHotkeysConfig;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
import { FocusStackItem } from '@/ui/utilities/focus/types/FocusStackItem';
|
||||||
|
import { createState } from 'twenty-ui/utilities';
|
||||||
|
|
||||||
|
export const focusStackState = createState<FocusStackItem[]>({
|
||||||
|
key: 'focusStackState',
|
||||||
|
defaultValue: [],
|
||||||
|
});
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
|
||||||
|
|
||||||
|
export type FocusComponentInstance = {
|
||||||
|
componentType: FocusComponentType;
|
||||||
|
componentInstanceId: string;
|
||||||
|
};
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
export enum FocusComponentType {
|
||||||
|
MODAL = 'modal',
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export const DEBUG_HOTKEY_SCOPE = false;
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
import { GlobalHotkeysConfig } from '@/ui/utilities/hotkey/types/GlobalHotkeysConfig';
|
||||||
|
|
||||||
|
export const DEFAULT_GLOBAL_HOTKEYS_CONFIG: GlobalHotkeysConfig = {
|
||||||
|
enableGlobalHotkeysWithModifiers: true,
|
||||||
|
enableGlobalHotkeysConflictingWithKeyboard: true,
|
||||||
|
};
|
||||||
@ -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<Options, 'enabled'>;
|
||||||
|
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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,
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -4,10 +4,10 @@ import { useRecoilState } from 'recoil';
|
|||||||
|
|
||||||
import { pendingHotkeyState } from '../states/internal/pendingHotkeysState';
|
import { pendingHotkeyState } from '../states/internal/pendingHotkeysState';
|
||||||
|
|
||||||
import { useScopedHotkeyCallback } from './useScopedHotkeyCallback';
|
import { useGlobalHotkeysCallback } from '@/ui/utilities/hotkey/hooks/useGlobalHotkeysCallback';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
|
||||||
export const useSequenceHotkeys = (
|
export const useGlobalHotkeysSequence = (
|
||||||
firstKey: Keys,
|
firstKey: Keys,
|
||||||
secondKey: Keys,
|
secondKey: Keys,
|
||||||
sequenceCallback: () => void,
|
sequenceCallback: () => void,
|
||||||
@ -21,14 +21,15 @@ export const useSequenceHotkeys = (
|
|||||||
) => {
|
) => {
|
||||||
const [pendingHotkey, setPendingHotkey] = useRecoilState(pendingHotkeyState);
|
const [pendingHotkey, setPendingHotkey] = useRecoilState(pendingHotkeyState);
|
||||||
|
|
||||||
const callScopedHotkeyCallback = useScopedHotkeyCallback();
|
const callGlobalHotkeysCallback = useGlobalHotkeysCallback();
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
firstKey,
|
firstKey,
|
||||||
(keyboardEvent, hotkeysEvent) => {
|
(keyboardEvent, hotkeysEvent) => {
|
||||||
callScopedHotkeyCallback({
|
callGlobalHotkeysCallback({
|
||||||
keyboardEvent,
|
keyboardEvent,
|
||||||
hotkeysEvent,
|
hotkeysEvent,
|
||||||
|
containsModifier: false,
|
||||||
callback: () => {
|
callback: () => {
|
||||||
setPendingHotkey(firstKey);
|
setPendingHotkey(firstKey);
|
||||||
},
|
},
|
||||||
@ -46,9 +47,10 @@ export const useSequenceHotkeys = (
|
|||||||
useHotkeys(
|
useHotkeys(
|
||||||
secondKey,
|
secondKey,
|
||||||
(keyboardEvent, hotkeysEvent) => {
|
(keyboardEvent, hotkeysEvent) => {
|
||||||
callScopedHotkeyCallback({
|
callGlobalHotkeysCallback({
|
||||||
keyboardEvent,
|
keyboardEvent,
|
||||||
hotkeysEvent,
|
hotkeysEvent,
|
||||||
|
containsModifier: false,
|
||||||
callback: () => {
|
callback: () => {
|
||||||
if (pendingHotkey !== firstKey) {
|
if (pendingHotkey !== firstKey) {
|
||||||
return;
|
return;
|
||||||
@ -1,10 +1,9 @@
|
|||||||
import { Keys } from 'react-hotkeys-hook/dist/types';
|
import { Keys } from 'react-hotkeys-hook/dist/types';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { useGlobalHotkeysSequence } from '@/ui/utilities/hotkey/hooks/useGlobalHotkeysSequence';
|
||||||
import { AppHotkeyScope } from '../types/AppHotkeyScope';
|
import { AppHotkeyScope } from '../types/AppHotkeyScope';
|
||||||
|
|
||||||
import { useSequenceHotkeys } from './useSequenceScopedHotkeys';
|
|
||||||
|
|
||||||
type GoToHotkeysProps = {
|
type GoToHotkeysProps = {
|
||||||
key: Keys;
|
key: Keys;
|
||||||
location: string;
|
location: string;
|
||||||
@ -18,7 +17,7 @@ export const useGoToHotkeys = ({
|
|||||||
}: GoToHotkeysProps) => {
|
}: GoToHotkeysProps) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useSequenceHotkeys(
|
useGlobalHotkeysSequence(
|
||||||
'g',
|
'g',
|
||||||
key,
|
key,
|
||||||
() => {
|
() => {
|
||||||
|
|||||||
@ -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<Options, 'enabled'>;
|
||||||
|
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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,
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { useRecoilCallback } from 'recoil';
|
import { useRecoilCallback } from 'recoil';
|
||||||
|
|
||||||
import { DEBUG_HOTKEY_SCOPE } from '@/ui/utilities/hotkey/hooks/useScopedHotkeyCallback';
|
|
||||||
import { logDebug } from '~/utils/logDebug';
|
import { logDebug } from '~/utils/logDebug';
|
||||||
|
import { DEBUG_HOTKEY_SCOPE } from '../constants/DebugHotkeyScope';
|
||||||
|
|
||||||
import { currentHotkeyScopeState } from '../states/internal/currentHotkeyScopeState';
|
import { currentHotkeyScopeState } from '../states/internal/currentHotkeyScopeState';
|
||||||
import { previousHotkeyScopeFamilyState } from '../states/internal/previousHotkeyScopeFamilyState';
|
import { previousHotkeyScopeFamilyState } from '../states/internal/previousHotkeyScopeFamilyState';
|
||||||
@ -9,14 +9,14 @@ import { CustomHotkeyScopes } from '../types/CustomHotkeyScope';
|
|||||||
|
|
||||||
import { useSetHotkeyScope } from './useSetHotkeyScope';
|
import { useSetHotkeyScope } from './useSetHotkeyScope';
|
||||||
|
|
||||||
export const usePreviousHotkeyScope = (memoizeKey = 'global') => {
|
export const usePreviousHotkeyScope = () => {
|
||||||
const setHotkeyScope = useSetHotkeyScope();
|
const setHotkeyScope = useSetHotkeyScope();
|
||||||
|
|
||||||
const goBackToPreviousHotkeyScope = useRecoilCallback(
|
const goBackToPreviousHotkeyScope = useRecoilCallback(
|
||||||
({ snapshot, set }) =>
|
({ snapshot, set }) =>
|
||||||
() => {
|
(memoizeKey = 'global') => {
|
||||||
const previousHotkeyScope = snapshot
|
const previousHotkeyScope = snapshot
|
||||||
.getLoadable(previousHotkeyScopeFamilyState(memoizeKey))
|
.getLoadable(previousHotkeyScopeFamilyState(memoizeKey as string))
|
||||||
.getValue();
|
.getValue();
|
||||||
|
|
||||||
if (!previousHotkeyScope) {
|
if (!previousHotkeyScope) {
|
||||||
@ -39,14 +39,22 @@ export const usePreviousHotkeyScope = (memoizeKey = 'global') => {
|
|||||||
previousHotkeyScope.customScopes,
|
previousHotkeyScope.customScopes,
|
||||||
);
|
);
|
||||||
|
|
||||||
set(previousHotkeyScopeFamilyState(memoizeKey), null);
|
set(previousHotkeyScopeFamilyState(memoizeKey as string), null);
|
||||||
},
|
},
|
||||||
[setHotkeyScope, memoizeKey],
|
[setHotkeyScope],
|
||||||
);
|
);
|
||||||
|
|
||||||
const setHotkeyScopeAndMemorizePreviousScope = useRecoilCallback(
|
const setHotkeyScopeAndMemorizePreviousScope = useRecoilCallback(
|
||||||
({ snapshot, set }) =>
|
({ snapshot, set }) =>
|
||||||
(scope: string, customScopes?: CustomHotkeyScopes) => {
|
({
|
||||||
|
scope,
|
||||||
|
customScopes,
|
||||||
|
memoizeKey = 'global',
|
||||||
|
}: {
|
||||||
|
scope: string;
|
||||||
|
customScopes?: CustomHotkeyScopes;
|
||||||
|
memoizeKey?: string;
|
||||||
|
}) => {
|
||||||
const currentHotkeyScope = snapshot
|
const currentHotkeyScope = snapshot
|
||||||
.getLoadable(currentHotkeyScopeState)
|
.getLoadable(currentHotkeyScopeState)
|
||||||
.getValue();
|
.getValue();
|
||||||
@ -63,7 +71,7 @@ export const usePreviousHotkeyScope = (memoizeKey = 'global') => {
|
|||||||
|
|
||||||
set(previousHotkeyScopeFamilyState(memoizeKey), currentHotkeyScope);
|
set(previousHotkeyScopeFamilyState(memoizeKey), currentHotkeyScope);
|
||||||
},
|
},
|
||||||
[setHotkeyScope, memoizeKey],
|
[setHotkeyScope],
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -6,10 +6,9 @@ import { useRecoilCallback } from 'recoil';
|
|||||||
|
|
||||||
import { logDebug } from '~/utils/logDebug';
|
import { logDebug } from '~/utils/logDebug';
|
||||||
|
|
||||||
|
import { DEBUG_HOTKEY_SCOPE } from '../constants/DebugHotkeyScope';
|
||||||
import { internalHotkeysEnabledScopesState } from '../states/internal/internalHotkeysEnabledScopesState';
|
import { internalHotkeysEnabledScopesState } from '../states/internal/internalHotkeysEnabledScopesState';
|
||||||
|
|
||||||
export const DEBUG_HOTKEY_SCOPE = false;
|
|
||||||
|
|
||||||
export const useScopedHotkeyCallback = (
|
export const useScopedHotkeyCallback = (
|
||||||
dependencies?: OptionsOrDependencyArray,
|
dependencies?: OptionsOrDependencyArray,
|
||||||
) => {
|
) => {
|
||||||
|
|||||||
@ -2,9 +2,9 @@ import { useHotkeys } from 'react-hotkeys-hook';
|
|||||||
import { HotkeyCallback, Keys, Options } from 'react-hotkeys-hook/dist/types';
|
import { HotkeyCallback, Keys, Options } from 'react-hotkeys-hook/dist/types';
|
||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
|
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { pendingHotkeyState } from '../states/internal/pendingHotkeysState';
|
import { pendingHotkeyState } from '../states/internal/pendingHotkeysState';
|
||||||
import { useScopedHotkeyCallback } from './useScopedHotkeyCallback';
|
import { useScopedHotkeyCallback } from './useScopedHotkeyCallback';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
|
||||||
|
|
||||||
type UseHotkeysOptionsWithoutBuggyOptions = Omit<Options, 'enabled'>;
|
type UseHotkeysOptionsWithoutBuggyOptions = Omit<Options, 'enabled'>;
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { useRecoilCallback } from 'recoil';
|
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 { isDefined } from 'twenty-shared/utils';
|
||||||
import { logDebug } from '~/utils/logDebug';
|
import { logDebug } from '~/utils/logDebug';
|
||||||
@ -11,7 +11,7 @@ import { AppHotkeyScope } from '../types/AppHotkeyScope';
|
|||||||
import { CustomHotkeyScopes } from '../types/CustomHotkeyScope';
|
import { CustomHotkeyScopes } from '../types/CustomHotkeyScope';
|
||||||
import { HotkeyScope } from '../types/HotkeyScope';
|
import { HotkeyScope } from '../types/HotkeyScope';
|
||||||
|
|
||||||
const isCustomScopesEqual = (
|
const areCustomScopesEqual = (
|
||||||
customScopesA: CustomHotkeyScopes | undefined,
|
customScopesA: CustomHotkeyScopes | undefined,
|
||||||
customScopesB: CustomHotkeyScopes | undefined,
|
customScopesB: CustomHotkeyScopes | undefined,
|
||||||
) => {
|
) => {
|
||||||
@ -34,7 +34,7 @@ export const useSetHotkeyScope = () =>
|
|||||||
if (currentHotkeyScope.scope === hotkeyScopeToSet) {
|
if (currentHotkeyScope.scope === hotkeyScopeToSet) {
|
||||||
if (!isDefined(customScopes)) {
|
if (!isDefined(customScopes)) {
|
||||||
if (
|
if (
|
||||||
isCustomScopesEqual(
|
areCustomScopesEqual(
|
||||||
currentHotkeyScope?.customScopes,
|
currentHotkeyScope?.customScopes,
|
||||||
DEFAULT_HOTKEYS_SCOPE_CUSTOM_SCOPES,
|
DEFAULT_HOTKEYS_SCOPE_CUSTOM_SCOPES,
|
||||||
)
|
)
|
||||||
@ -43,7 +43,7 @@ export const useSetHotkeyScope = () =>
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (
|
if (
|
||||||
isCustomScopesEqual(
|
areCustomScopesEqual(
|
||||||
currentHotkeyScope?.customScopes,
|
currentHotkeyScope?.customScopes,
|
||||||
customScopes,
|
customScopes,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -0,0 +1,4 @@
|
|||||||
|
export type GlobalHotkeysConfig = {
|
||||||
|
enableGlobalHotkeysWithModifiers: boolean;
|
||||||
|
enableGlobalHotkeysConflictingWithKeyboard: boolean;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user