diff --git a/front/src/modules/command-menu/components/CommandMenu.tsx b/front/src/modules/command-menu/components/CommandMenu.tsx index 2efb2233e..9817a259a 100644 --- a/front/src/modules/command-menu/components/CommandMenu.tsx +++ b/front/src/modules/command-menu/components/CommandMenu.tsx @@ -4,7 +4,6 @@ import { useRecoilValue } from 'recoil'; import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer'; import { Activity } from '@/activities/types/Activity'; -import { CommandMenuSelectableListEffect } from '@/command-menu/components/CommandMenuSelectableListEffect'; import { Company } from '@/companies/types/Company'; import { useKeyboardShortcutMenu } from '@/keyboard-shortcut-menu/hooks/useKeyboardShortcutMenu'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; @@ -86,7 +85,7 @@ export const StyledEmpty = styled.div` `; export const CommandMenu = () => { - const { toggleCommandMenu, closeCommandMenu } = useCommandMenu(); + const { toggleCommandMenu } = useCommandMenu(); const openActivityRightDrawer = useOpenActivityRightDrawer(); const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState); @@ -109,17 +108,6 @@ export const CommandMenu = () => { [toggleCommandMenu, setSearch], ); - useScopedHotkeys( - 'esc', - () => { - setSearch(''); - closeKeyboardShortcutMenu(); - closeCommandMenu(); - }, - AppHotkeyScope.CommandMenu, - [toggleCommandMenu, setSearch], - ); - const { records: people } = useFindManyRecords({ skip: !isCommandMenuOpened, objectNameSingular: 'person', @@ -198,12 +186,13 @@ export const CommandMenu = () => { - { + console.log(itemId); + }} > {!matchingCreateCommand.length && !matchingNavigateCommand.length && diff --git a/front/src/modules/command-menu/components/CommandMenuItem.tsx b/front/src/modules/command-menu/components/CommandMenuItem.tsx index 88f165175..3a1f6afd3 100644 --- a/front/src/modules/command-menu/components/CommandMenuItem.tsx +++ b/front/src/modules/command-menu/components/CommandMenuItem.tsx @@ -1,5 +1,3 @@ -import { useNavigate } from 'react-router-dom'; - import { IconArrowUpRight } from '@/ui/display/icon'; import { IconComponent } from '@/ui/display/icon/types/IconComponent'; import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList'; @@ -26,8 +24,7 @@ export const CommandMenuItem = ({ firstHotKey, secondHotKey, }: CommandMenuItemProps) => { - const navigate = useNavigate(); - const { toggleCommandMenu } = useCommandMenu(); + const { onItemClick } = useCommandMenu(); if (to && !Icon) { Icon = IconArrowUpRight; @@ -35,26 +32,13 @@ export const CommandMenuItem = ({ const { isSelectedItemId } = useSelectableList({ itemId: id }); - const onItemClick = () => { - toggleCommandMenu(); - - if (onClick) { - onClick(); - return; - } - if (to) { - navigate(to); - return; - } - }; - return ( onItemClick(onClick, to)} isSelected={isSelectedItemId} /> ); diff --git a/front/src/modules/command-menu/components/CommandMenuSelectableListEffect.tsx b/front/src/modules/command-menu/components/CommandMenuSelectableListEffect.tsx deleted file mode 100644 index 200539e1e..000000000 --- a/front/src/modules/command-menu/components/CommandMenuSelectableListEffect.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { useEffect } from 'react'; - -import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList'; - -type CommandMenuSelectableListEffectProps = { - selectableItemIds: string[]; -}; - -export const CommandMenuSelectableListEffect = ({ - selectableItemIds, -}: CommandMenuSelectableListEffectProps) => { - const { setSelectableItemIds } = useSelectableList({ - selectableListId: 'command-menu-list', - }); - - useEffect(() => { - setSelectableItemIds(selectableItemIds); - }, [selectableItemIds, setSelectableItemIds]); - - return <>; -}; diff --git a/front/src/modules/command-menu/hooks/useCommandMenu.ts b/front/src/modules/command-menu/hooks/useCommandMenu.ts index 756415ed1..cf9af6747 100644 --- a/front/src/modules/command-menu/hooks/useCommandMenu.ts +++ b/front/src/modules/command-menu/hooks/useCommandMenu.ts @@ -1,4 +1,5 @@ import { useCallback } from 'react'; +import { useNavigate } from 'react-router-dom'; import { useRecoilCallback, useSetRecoilState } from 'recoil'; import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; @@ -10,6 +11,7 @@ import { isCommandMenuOpenedState } from '../states/isCommandMenuOpenedState'; import { Command } from '../types/Command'; export const useCommandMenu = () => { + const navigate = useNavigate(); const setIsCommandMenuOpened = useSetRecoilState(isCommandMenuOpenedState); const setCommands = useSetRecoilState(commandMenuCommandsState); const { @@ -50,11 +52,28 @@ export const useCommandMenu = () => { setCommands(commandMenuCommands); }; + const onItemClick = useCallback( + (onClick?: () => void, to?: string) => { + toggleCommandMenu(); + + if (onClick) { + onClick(); + return; + } + if (to) { + navigate(to); + return; + } + }, + [navigate, toggleCommandMenu], + ); + return { openCommandMenu, closeCommandMenu, toggleCommandMenu, addToCommandMenu, + onItemClick, setToIntitialCommandMenu, }; }; diff --git a/front/src/modules/ui/input/components/IconPicker.tsx b/front/src/modules/ui/input/components/IconPicker.tsx index 9f707c61d..6f25f637e 100644 --- a/front/src/modules/ui/input/components/IconPicker.tsx +++ b/front/src/modules/ui/input/components/IconPicker.tsx @@ -9,6 +9,10 @@ import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/Dropdow import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope'; +import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList'; +import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList'; +import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; +import { arrayToChunks } from '~/utils/array/array-to-chunks'; import { IconButton, IconButtonVariant } from '../button/components/IconButton'; import { LightIconButton } from '../button/components/LightIconButton'; @@ -44,6 +48,34 @@ const StyledLightIconButton = styled(LightIconButton)<{ isSelected?: boolean }>` const convertIconKeyToLabel = (iconKey: string) => iconKey.replace(/[A-Z]/g, (letter) => ` ${letter}`).trim(); +type IconPickerIconProps = { + iconKey: string; + onClick: () => void; + selectedIconKey?: string; + Icon: IconComponent; +}; + +const IconPickerIcon = ({ + iconKey, + onClick, + selectedIconKey, + Icon, +}: IconPickerIconProps) => { + const { isSelectedItemId } = useSelectableList({ itemId: iconKey }); + + return ( + + ); +}; + export const IconPicker = ({ disabled, dropdownScopeId = 'icon-picker', @@ -56,6 +88,10 @@ export const IconPicker = ({ className, }: IconPickerProps) => { const [searchString, setSearchString] = useState(''); + const { + goBackToPreviousHotkeyScope, + setHotkeyScopeAndMemorizePreviousScope, + } = usePreviousHotkeyScope(); const { closeDropdown } = useDropdown({ dropdownScopeId }); @@ -79,6 +115,11 @@ export const IconPicker = ({ ).slice(0, 25); }, [icons, searchString, selectedIconKey]); + const iconKeys2d = useMemo( + () => arrayToChunks(iconKeys.slice(), 5), + [iconKeys], + ); + return (
@@ -93,36 +134,53 @@ export const IconPicker = ({ } dropdownMenuWidth={176} dropdownComponents={ - - setSearchString(event.target.value)} - /> - - - {isLoading ? ( - - ) : ( - - {iconKeys.map((iconKey) => ( - { - onChange({ iconKey, Icon: icons[iconKey] }); - closeDropdown(); - }} - /> - ))} - - )} - - + { + onChange({ iconKey, Icon: icons[iconKey] }); + closeDropdown(); + }} + > + + setSearchString(event.target.value)} + /> + +
{ + setHotkeyScopeAndMemorizePreviousScope( + IconPickerHotkeyScope.IconPicker, + ); + }} + onMouseLeave={goBackToPreviousHotkeyScope} + > + + {isLoading ? ( + + ) : ( + + {iconKeys.map((iconKey) => ( + { + onChange({ iconKey, Icon: icons[iconKey] }); + closeDropdown(); + }} + selectedIconKey={selectedIconKey} + Icon={icons[iconKey]} + /> + ))} + + )} + +
+
+
} onClickOutside={onClickOutside} onClose={() => { diff --git a/front/src/modules/ui/layout/selectable-list/components/SelectableList.tsx b/front/src/modules/ui/layout/selectable-list/components/SelectableList.tsx index 587644e7f..e65c499c0 100644 --- a/front/src/modules/ui/layout/selectable-list/components/SelectableList.tsx +++ b/front/src/modules/ui/layout/selectable-list/components/SelectableList.tsx @@ -1,13 +1,17 @@ -import { ReactNode } from 'react'; +import { ReactNode, useEffect } from 'react'; import styled from '@emotion/styled'; import { useSelectableListHotKeys } from '@/ui/layout/selectable-list/hooks/internal/useSelectableListHotKeys'; +import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList'; import { SelectableListScope } from '@/ui/layout/selectable-list/scopes/SelectableListScope'; + type SelectableListProps = { children: ReactNode; selectableListId: string; - selectableItemIds: string[]; + selectableItemIds: string[][]; onSelect?: (selected: string) => void; + hotkeyScope: string; + onEnter?: (itemId: string) => void; }; const StyledSelectableItemsContainer = styled.div` @@ -17,8 +21,23 @@ const StyledSelectableItemsContainer = styled.div` export const SelectableList = ({ children, selectableListId, + hotkeyScope, + selectableItemIds, + onEnter, }: SelectableListProps) => { - useSelectableListHotKeys(selectableListId); + useSelectableListHotKeys(selectableListId, hotkeyScope); + + const { setSelectableItemIds, setSelectableListOnEnter } = useSelectableList({ + selectableListId, + }); + + useEffect(() => { + setSelectableListOnEnter(() => onEnter); + }, [onEnter, setSelectableListOnEnter]); + + useEffect(() => { + setSelectableItemIds(selectableItemIds); + }, [selectableItemIds, setSelectableItemIds]); return ( diff --git a/front/src/modules/ui/layout/selectable-list/hooks/internal/useSelectableListHotKeys.tsx b/front/src/modules/ui/layout/selectable-list/hooks/internal/useSelectableListHotKeys.tsx index e491a407f..a8fb33856 100644 --- a/front/src/modules/ui/layout/selectable-list/hooks/internal/useSelectableListHotKeys.tsx +++ b/front/src/modules/ui/layout/selectable-list/hooks/internal/useSelectableListHotKeys.tsx @@ -1,16 +1,37 @@ -import { isNull } from '@sniptt/guards'; import { useRecoilCallback } from 'recoil'; import { Key } from 'ts-key-enum'; import { getSelectableListScopedStates } from '@/ui/layout/selectable-list/utils/internal/getSelectableListScopedStates'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; -import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope'; import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue'; -export const useSelectableListHotKeys = (scopeId: string) => { +type Direction = 'up' | 'down' | 'left' | 'right'; + +export const useSelectableListHotKeys = ( + scopeId: string, + hotkeyScope: string, +) => { + const findPosition = ( + selectableItemIds: string[][], + selectedItemId?: string | null, + ) => { + if (!selectedItemId) { + // If nothing is selected, return the default position + return { row: 0, col: 0 }; + } + + for (let row = 0; row < selectableItemIds.length; row++) { + const col = selectableItemIds[row].indexOf(selectedItemId); + if (col !== -1) { + return { row, col }; + } + } + return { row: 0, col: 0 }; + }; + const handleSelect = useRecoilCallback( ({ snapshot, set }) => - (direction: 'up' | 'down') => { + (direction: Direction) => { const { selectedItemIdState, selectableItemIdsState } = getSelectableListScopedStates({ selectableListScopeId: scopeId, @@ -21,31 +42,54 @@ export const useSelectableListHotKeys = (scopeId: string) => { selectableItemIdsState, ); - const computeNextId = (direction: 'up' | 'down') => { + const { row: currentRow, col: currentCol } = findPosition( + selectableItemIds, + selectedItemId, + ); + + const computeNextId = (direction: Direction) => { if (selectableItemIds.length === 0) { return; } - if (isNull(selectedItemId)) { - return direction === 'up' - ? selectableItemIds[selectableItemIds.length - 1] - : selectableItemIds[0]; + const isSingleRow = selectableItemIds.length === 1; + + let nextRow: number; + let nextCol: number; + + switch (direction) { + case 'up': + nextRow = isSingleRow ? currentRow : Math.max(0, currentRow - 1); + nextCol = isSingleRow ? Math.max(0, currentCol - 1) : currentCol; + break; + case 'down': + nextRow = isSingleRow + ? currentRow + : Math.min(selectableItemIds.length - 1, currentRow + 1); + nextCol = isSingleRow + ? Math.min( + selectableItemIds[currentRow].length - 1, + currentCol + 1, + ) + : currentCol; + break; + case 'left': + nextRow = currentRow; + nextCol = Math.max(0, currentCol - 1); + break; + case 'right': + nextRow = currentRow; + nextCol = Math.min( + selectableItemIds[currentRow].length - 1, + currentCol + 1, + ); + break; + default: + nextRow = currentRow; + nextCol = currentCol; } - const currentIndex = selectableItemIds.indexOf(selectedItemId); - if (currentIndex === -1) { - return direction === 'up' - ? selectableItemIds[selectableItemIds.length - 1] - : selectableItemIds[0]; - } - - return direction === 'up' - ? currentIndex == 0 - ? selectableItemIds[selectableItemIds.length - 1] - : selectableItemIds[currentIndex - 1] - : currentIndex == selectableItemIds.length - 1 - ? selectableItemIds[0] - : selectableItemIds[currentIndex + 1]; + return selectableItemIds[nextRow][nextCol]; }; const nextId = computeNextId(direction); @@ -70,17 +114,45 @@ export const useSelectableListHotKeys = (scopeId: string) => { [scopeId], ); + useScopedHotkeys(Key.ArrowUp, () => handleSelect('up'), hotkeyScope, []); + + useScopedHotkeys(Key.ArrowDown, () => handleSelect('down'), hotkeyScope, []); + + useScopedHotkeys(Key.ArrowLeft, () => handleSelect('left'), hotkeyScope, []); + useScopedHotkeys( - Key.ArrowUp, - () => handleSelect('up'), - AppHotkeyScope.CommandMenu, + Key.ArrowRight, + () => handleSelect('right'), + hotkeyScope, [], ); useScopedHotkeys( - Key.ArrowDown, - () => handleSelect('down'), - AppHotkeyScope.CommandMenu, + Key.Enter, + useRecoilCallback( + ({ snapshot }) => + () => { + const { selectedItemIdState, selectableListOnEnterState } = + getSelectableListScopedStates({ + selectableListScopeId: scopeId, + }); + const selectedItemId = getSnapshotValue( + snapshot, + selectedItemIdState, + ); + + const onEnter = getSnapshotValue( + snapshot, + selectableListOnEnterState, + ); + + if (selectedItemId) { + onEnter?.(selectedItemId); + } + }, + [scopeId], + ), + hotkeyScope, [], ); diff --git a/front/src/modules/ui/layout/selectable-list/hooks/internal/useSelectableListScopedStates.ts b/front/src/modules/ui/layout/selectable-list/hooks/internal/useSelectableListScopedStates.ts index 885fd68e3..6afabd8a6 100644 --- a/front/src/modules/ui/layout/selectable-list/hooks/internal/useSelectableListScopedStates.ts +++ b/front/src/modules/ui/layout/selectable-list/hooks/internal/useSelectableListScopedStates.ts @@ -20,6 +20,7 @@ export const useSelectableListScopedStates = ( selectedItemIdState, selectableItemIdsState, isSelectedItemIdSelector, + selectableListOnEnterState, } = getSelectableListScopedStates({ selectableListScopeId: scopeId, itemId: itemId, @@ -30,5 +31,6 @@ export const useSelectableListScopedStates = ( isSelectedItemIdSelector, selectableItemIdsState, selectedItemIdState, + selectableListOnEnterState, }; }; diff --git a/front/src/modules/ui/layout/selectable-list/hooks/useSelectableList.ts b/front/src/modules/ui/layout/selectable-list/hooks/useSelectableList.ts index b67144190..1f09977b3 100644 --- a/front/src/modules/ui/layout/selectable-list/hooks/useSelectableList.ts +++ b/front/src/modules/ui/layout/selectable-list/hooks/useSelectableList.ts @@ -15,18 +15,25 @@ export const useSelectableList = (props?: UseSelectableListProps) => { props?.selectableListId, ); - const { selectableItemIdsState, isSelectedItemIdSelector } = - useSelectableListScopedStates({ - selectableListScopeId: scopeId, - itemId: props?.itemId, - }); + const { + selectableItemIdsState, + isSelectedItemIdSelector, + selectableListOnEnterState, + } = useSelectableListScopedStates({ + selectableListScopeId: scopeId, + itemId: props?.itemId, + }); const setSelectableItemIds = useSetRecoilState(selectableItemIdsState); + const setSelectableListOnEnter = useSetRecoilState( + selectableListOnEnterState, + ); const isSelectedItemId = useRecoilValue(isSelectedItemIdSelector); return { setSelectableItemIds, isSelectedItemId, + setSelectableListOnEnter, selectableListId: scopeId, isSelectedItemIdSelector, }; diff --git a/front/src/modules/ui/layout/selectable-list/states/selectableItemIdsScopedState.ts b/front/src/modules/ui/layout/selectable-list/states/selectableItemIdsScopedState.ts index 861b9bee9..1e56312e9 100644 --- a/front/src/modules/ui/layout/selectable-list/states/selectableItemIdsScopedState.ts +++ b/front/src/modules/ui/layout/selectable-list/states/selectableItemIdsScopedState.ts @@ -1,6 +1,6 @@ import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState'; -export const selectableItemIdsScopedState = createScopedState({ +export const selectableItemIdsScopedState = createScopedState({ key: 'selectableItemIdsScopedState', - defaultValue: [], + defaultValue: [[]], }); diff --git a/front/src/modules/ui/layout/selectable-list/states/selectableListOnEnterScopedState.ts b/front/src/modules/ui/layout/selectable-list/states/selectableListOnEnterScopedState.ts new file mode 100644 index 000000000..ef6752569 --- /dev/null +++ b/front/src/modules/ui/layout/selectable-list/states/selectableListOnEnterScopedState.ts @@ -0,0 +1,8 @@ +import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState'; + +export const selectableListOnEnterScopedState = createScopedState< + ((itemId: string) => void) | undefined +>({ + key: 'selectableListOnEnterScopedState', + defaultValue: undefined, +}); diff --git a/front/src/modules/ui/layout/selectable-list/utils/internal/getSelectableListScopedStates.ts b/front/src/modules/ui/layout/selectable-list/utils/internal/getSelectableListScopedStates.ts index 8a04dcb14..847485dc5 100644 --- a/front/src/modules/ui/layout/selectable-list/utils/internal/getSelectableListScopedStates.ts +++ b/front/src/modules/ui/layout/selectable-list/utils/internal/getSelectableListScopedStates.ts @@ -1,4 +1,5 @@ import { selectableItemIdsScopedState } from '@/ui/layout/selectable-list/states/selectableItemIdsScopedState'; +import { selectableListOnEnterScopedState } from '@/ui/layout/selectable-list/states/selectableListOnEnterScopedState'; import { selectedItemIdScopedState } from '@/ui/layout/selectable-list/states/selectedItemIdScopedState'; import { isSelectedItemIdScopedFamilySelector } from '@/ui/layout/selectable-list/states/selectors/isSelectedItemIdScopedFamilySelector'; import { getScopedState } from '@/ui/utilities/recoil-scope/utils/getScopedState'; @@ -27,9 +28,15 @@ export const getSelectableListScopedStates = ({ selectableListScopeId, ); + const selectableListOnEnterState = getScopedState( + selectableListOnEnterScopedState, + selectableListScopeId, + ); + return { isSelectedItemIdSelector, selectableItemIdsState, selectedItemIdState, + selectableListOnEnterState, }; }; diff --git a/front/src/modules/ui/utilities/hotkey/hooks/useSetHotkeyScope.ts b/front/src/modules/ui/utilities/hotkey/hooks/useSetHotkeyScope.ts index beaf9715c..d8dc4d370 100644 --- a/front/src/modules/ui/utilities/hotkey/hooks/useSetHotkeyScope.ts +++ b/front/src/modules/ui/utilities/hotkey/hooks/useSetHotkeyScope.ts @@ -74,6 +74,7 @@ export const useSetHotkeyScope = () => } scopesToSet.push(newHotkeyScope.scope); + console.log(scopesToSet); set(internalHotkeysEnabledScopesState, scopesToSet); set(currentHotkeyScopeState, newHotkeyScope); }, diff --git a/front/src/utils/array/array-to-chunks.ts b/front/src/utils/array/array-to-chunks.ts new file mode 100644 index 000000000..a6f5985bc --- /dev/null +++ b/front/src/utils/array/array-to-chunks.ts @@ -0,0 +1,11 @@ +// split an array into subarrays of a given size +export const arrayToChunks = (array: T[], size: number) => { + const arrayCopy = [...array]; + const results = []; + + while (arrayCopy.length) { + results.push(arrayCopy.splice(0, size)); + } + + return results; +};