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