Add Keyboard navigation on IconPicker (#2778)
* Add Add Keyboard navigation on IconPicker Co-authored-by: Matheus <matheus_benini@hotmail.com> * Add Keyboard navigation on IconPicker Co-authored-by: v1b3m <vibenjamin6@gmail.com> Co-authored-by: Matheus <matheus_benini@hotmail.com> * Add Keyboard navigation on IconPicker Co-authored-by: v1b3m <vibenjamin6@gmail.com> Co-authored-by: Matheus <matheus_benini@hotmail.com> * Add Keyboard navigation on IconPicker Co-authored-by: v1b3m <vibenjamin6@gmail.com> Co-authored-by: Matheus <matheus_benini@hotmail.com> * Refactor according to review Co-authored-by: v1b3m <vibenjamin6@gmail.com> Co-authored-by: Matheus <matheus_benini@hotmail.com> * Implement IconPicker * Remove onEnter clicked --------- Co-authored-by: gitstart-twenty <gitstart-twenty@users.noreply.github.com> Co-authored-by: Matheus <matheus_benini@hotmail.com> Co-authored-by: v1b3m <vibenjamin6@gmail.com> Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -4,7 +4,6 @@ import { useRecoilValue } from 'recoil';
|
|||||||
|
|
||||||
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
|
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
|
||||||
import { Activity } from '@/activities/types/Activity';
|
import { Activity } from '@/activities/types/Activity';
|
||||||
import { CommandMenuSelectableListEffect } from '@/command-menu/components/CommandMenuSelectableListEffect';
|
|
||||||
import { Company } from '@/companies/types/Company';
|
import { Company } from '@/companies/types/Company';
|
||||||
import { useKeyboardShortcutMenu } from '@/keyboard-shortcut-menu/hooks/useKeyboardShortcutMenu';
|
import { useKeyboardShortcutMenu } from '@/keyboard-shortcut-menu/hooks/useKeyboardShortcutMenu';
|
||||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||||
@ -86,7 +85,7 @@ export const StyledEmpty = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const CommandMenu = () => {
|
export const CommandMenu = () => {
|
||||||
const { toggleCommandMenu, closeCommandMenu } = useCommandMenu();
|
const { toggleCommandMenu } = useCommandMenu();
|
||||||
|
|
||||||
const openActivityRightDrawer = useOpenActivityRightDrawer();
|
const openActivityRightDrawer = useOpenActivityRightDrawer();
|
||||||
const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState);
|
const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState);
|
||||||
@ -109,17 +108,6 @@ export const CommandMenu = () => {
|
|||||||
[toggleCommandMenu, setSearch],
|
[toggleCommandMenu, setSearch],
|
||||||
);
|
);
|
||||||
|
|
||||||
useScopedHotkeys(
|
|
||||||
'esc',
|
|
||||||
() => {
|
|
||||||
setSearch('');
|
|
||||||
closeKeyboardShortcutMenu();
|
|
||||||
closeCommandMenu();
|
|
||||||
},
|
|
||||||
AppHotkeyScope.CommandMenu,
|
|
||||||
[toggleCommandMenu, setSearch],
|
|
||||||
);
|
|
||||||
|
|
||||||
const { records: people } = useFindManyRecords<Person>({
|
const { records: people } = useFindManyRecords<Person>({
|
||||||
skip: !isCommandMenuOpened,
|
skip: !isCommandMenuOpened,
|
||||||
objectNameSingular: 'person',
|
objectNameSingular: 'person',
|
||||||
@ -198,12 +186,13 @@ export const CommandMenu = () => {
|
|||||||
<StyledList>
|
<StyledList>
|
||||||
<ScrollWrapper>
|
<ScrollWrapper>
|
||||||
<StyledInnerList>
|
<StyledInnerList>
|
||||||
<CommandMenuSelectableListEffect
|
|
||||||
selectableItemIds={selectableItemIds}
|
|
||||||
/>
|
|
||||||
<SelectableList
|
<SelectableList
|
||||||
selectableListId="command-menu-list"
|
selectableListId="command-menu-list"
|
||||||
selectableItemIds={selectableItemIds}
|
selectableItemIds={[selectableItemIds]}
|
||||||
|
hotkeyScope={AppHotkeyScope.CommandMenu}
|
||||||
|
onEnter={(itemId) => {
|
||||||
|
console.log(itemId);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{!matchingCreateCommand.length &&
|
{!matchingCreateCommand.length &&
|
||||||
!matchingNavigateCommand.length &&
|
!matchingNavigateCommand.length &&
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { IconArrowUpRight } from '@/ui/display/icon';
|
import { IconArrowUpRight } from '@/ui/display/icon';
|
||||||
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
|
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
|
||||||
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
||||||
@ -26,8 +24,7 @@ export const CommandMenuItem = ({
|
|||||||
firstHotKey,
|
firstHotKey,
|
||||||
secondHotKey,
|
secondHotKey,
|
||||||
}: CommandMenuItemProps) => {
|
}: CommandMenuItemProps) => {
|
||||||
const navigate = useNavigate();
|
const { onItemClick } = useCommandMenu();
|
||||||
const { toggleCommandMenu } = useCommandMenu();
|
|
||||||
|
|
||||||
if (to && !Icon) {
|
if (to && !Icon) {
|
||||||
Icon = IconArrowUpRight;
|
Icon = IconArrowUpRight;
|
||||||
@ -35,26 +32,13 @@ export const CommandMenuItem = ({
|
|||||||
|
|
||||||
const { isSelectedItemId } = useSelectableList({ itemId: id });
|
const { isSelectedItemId } = useSelectableList({ itemId: id });
|
||||||
|
|
||||||
const onItemClick = () => {
|
|
||||||
toggleCommandMenu();
|
|
||||||
|
|
||||||
if (onClick) {
|
|
||||||
onClick();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (to) {
|
|
||||||
navigate(to);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MenuItemCommand
|
<MenuItemCommand
|
||||||
LeftIcon={Icon}
|
LeftIcon={Icon}
|
||||||
text={label}
|
text={label}
|
||||||
firstHotKey={firstHotKey}
|
firstHotKey={firstHotKey}
|
||||||
secondHotKey={secondHotKey}
|
secondHotKey={secondHotKey}
|
||||||
onClick={onItemClick}
|
onClick={() => onItemClick(onClick, to)}
|
||||||
isSelected={isSelectedItemId}
|
isSelected={isSelectedItemId}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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 <></>;
|
|
||||||
};
|
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useRecoilCallback, useSetRecoilState } from 'recoil';
|
import { useRecoilCallback, useSetRecoilState } from 'recoil';
|
||||||
|
|
||||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||||
@ -10,6 +11,7 @@ import { isCommandMenuOpenedState } from '../states/isCommandMenuOpenedState';
|
|||||||
import { Command } from '../types/Command';
|
import { Command } from '../types/Command';
|
||||||
|
|
||||||
export const useCommandMenu = () => {
|
export const useCommandMenu = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
const setIsCommandMenuOpened = useSetRecoilState(isCommandMenuOpenedState);
|
const setIsCommandMenuOpened = useSetRecoilState(isCommandMenuOpenedState);
|
||||||
const setCommands = useSetRecoilState(commandMenuCommandsState);
|
const setCommands = useSetRecoilState(commandMenuCommandsState);
|
||||||
const {
|
const {
|
||||||
@ -50,11 +52,28 @@ export const useCommandMenu = () => {
|
|||||||
setCommands(commandMenuCommands);
|
setCommands(commandMenuCommands);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onItemClick = useCallback(
|
||||||
|
(onClick?: () => void, to?: string) => {
|
||||||
|
toggleCommandMenu();
|
||||||
|
|
||||||
|
if (onClick) {
|
||||||
|
onClick();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (to) {
|
||||||
|
navigate(to);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[navigate, toggleCommandMenu],
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
openCommandMenu,
|
openCommandMenu,
|
||||||
closeCommandMenu,
|
closeCommandMenu,
|
||||||
toggleCommandMenu,
|
toggleCommandMenu,
|
||||||
addToCommandMenu,
|
addToCommandMenu,
|
||||||
|
onItemClick,
|
||||||
setToIntitialCommandMenu,
|
setToIntitialCommandMenu,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -9,6 +9,10 @@ import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/Dropdow
|
|||||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||||
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
|
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 { IconButton, IconButtonVariant } from '../button/components/IconButton';
|
||||||
import { LightIconButton } from '../button/components/LightIconButton';
|
import { LightIconButton } from '../button/components/LightIconButton';
|
||||||
@ -44,6 +48,34 @@ const StyledLightIconButton = styled(LightIconButton)<{ isSelected?: boolean }>`
|
|||||||
const convertIconKeyToLabel = (iconKey: string) =>
|
const convertIconKeyToLabel = (iconKey: string) =>
|
||||||
iconKey.replace(/[A-Z]/g, (letter) => ` ${letter}`).trim();
|
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 (
|
||||||
|
<StyledLightIconButton
|
||||||
|
key={iconKey}
|
||||||
|
aria-label={convertIconKeyToLabel(iconKey)}
|
||||||
|
size="medium"
|
||||||
|
title={iconKey}
|
||||||
|
isSelected={iconKey === selectedIconKey || isSelectedItemId}
|
||||||
|
Icon={Icon}
|
||||||
|
onClick={onClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const IconPicker = ({
|
export const IconPicker = ({
|
||||||
disabled,
|
disabled,
|
||||||
dropdownScopeId = 'icon-picker',
|
dropdownScopeId = 'icon-picker',
|
||||||
@ -56,6 +88,10 @@ export const IconPicker = ({
|
|||||||
className,
|
className,
|
||||||
}: IconPickerProps) => {
|
}: IconPickerProps) => {
|
||||||
const [searchString, setSearchString] = useState('');
|
const [searchString, setSearchString] = useState('');
|
||||||
|
const {
|
||||||
|
goBackToPreviousHotkeyScope,
|
||||||
|
setHotkeyScopeAndMemorizePreviousScope,
|
||||||
|
} = usePreviousHotkeyScope();
|
||||||
|
|
||||||
const { closeDropdown } = useDropdown({ dropdownScopeId });
|
const { closeDropdown } = useDropdown({ dropdownScopeId });
|
||||||
|
|
||||||
@ -79,6 +115,11 @@ export const IconPicker = ({
|
|||||||
).slice(0, 25);
|
).slice(0, 25);
|
||||||
}, [icons, searchString, selectedIconKey]);
|
}, [icons, searchString, selectedIconKey]);
|
||||||
|
|
||||||
|
const iconKeys2d = useMemo(
|
||||||
|
() => arrayToChunks(iconKeys.slice(), 5),
|
||||||
|
[iconKeys],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownScope dropdownScopeId={dropdownScopeId}>
|
<DropdownScope dropdownScopeId={dropdownScopeId}>
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
@ -93,36 +134,53 @@ export const IconPicker = ({
|
|||||||
}
|
}
|
||||||
dropdownMenuWidth={176}
|
dropdownMenuWidth={176}
|
||||||
dropdownComponents={
|
dropdownComponents={
|
||||||
<DropdownMenu width={176}>
|
<SelectableList
|
||||||
<DropdownMenuSearchInput
|
selectableListId="icon-list"
|
||||||
placeholder="Search icon"
|
selectableItemIds={iconKeys2d}
|
||||||
autoFocus
|
hotkeyScope={IconPickerHotkeyScope.IconPicker}
|
||||||
onChange={(event) => setSearchString(event.target.value)}
|
onEnter={(iconKey) => {
|
||||||
/>
|
onChange({ iconKey, Icon: icons[iconKey] });
|
||||||
<DropdownMenuSeparator />
|
closeDropdown();
|
||||||
<DropdownMenuItemsContainer>
|
}}
|
||||||
{isLoading ? (
|
>
|
||||||
<DropdownMenuSkeletonItem />
|
<DropdownMenu width={176}>
|
||||||
) : (
|
<DropdownMenuSearchInput
|
||||||
<StyledMenuIconItemsContainer>
|
placeholder="Search icon"
|
||||||
{iconKeys.map((iconKey) => (
|
autoFocus
|
||||||
<StyledLightIconButton
|
onChange={(event) => setSearchString(event.target.value)}
|
||||||
key={iconKey}
|
/>
|
||||||
aria-label={convertIconKeyToLabel(iconKey)}
|
<DropdownMenuSeparator />
|
||||||
isSelected={selectedIconKey === iconKey}
|
<div
|
||||||
size="medium"
|
onMouseEnter={() => {
|
||||||
title={iconKey}
|
setHotkeyScopeAndMemorizePreviousScope(
|
||||||
Icon={icons[iconKey]}
|
IconPickerHotkeyScope.IconPicker,
|
||||||
onClick={() => {
|
);
|
||||||
onChange({ iconKey, Icon: icons[iconKey] });
|
}}
|
||||||
closeDropdown();
|
onMouseLeave={goBackToPreviousHotkeyScope}
|
||||||
}}
|
>
|
||||||
/>
|
<DropdownMenuItemsContainer>
|
||||||
))}
|
{isLoading ? (
|
||||||
</StyledMenuIconItemsContainer>
|
<DropdownMenuSkeletonItem />
|
||||||
)}
|
) : (
|
||||||
</DropdownMenuItemsContainer>
|
<StyledMenuIconItemsContainer>
|
||||||
</DropdownMenu>
|
{iconKeys.map((iconKey) => (
|
||||||
|
<IconPickerIcon
|
||||||
|
key={iconKey}
|
||||||
|
iconKey={iconKey}
|
||||||
|
onClick={() => {
|
||||||
|
onChange({ iconKey, Icon: icons[iconKey] });
|
||||||
|
closeDropdown();
|
||||||
|
}}
|
||||||
|
selectedIconKey={selectedIconKey}
|
||||||
|
Icon={icons[iconKey]}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</StyledMenuIconItemsContainer>
|
||||||
|
)}
|
||||||
|
</DropdownMenuItemsContainer>
|
||||||
|
</div>
|
||||||
|
</DropdownMenu>
|
||||||
|
</SelectableList>
|
||||||
}
|
}
|
||||||
onClickOutside={onClickOutside}
|
onClickOutside={onClickOutside}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
|
|||||||
@ -1,13 +1,17 @@
|
|||||||
import { ReactNode } from 'react';
|
import { ReactNode, useEffect } from 'react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
import { useSelectableListHotKeys } from '@/ui/layout/selectable-list/hooks/internal/useSelectableListHotKeys';
|
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';
|
import { SelectableListScope } from '@/ui/layout/selectable-list/scopes/SelectableListScope';
|
||||||
|
|
||||||
type SelectableListProps = {
|
type SelectableListProps = {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
selectableListId: string;
|
selectableListId: string;
|
||||||
selectableItemIds: string[];
|
selectableItemIds: string[][];
|
||||||
onSelect?: (selected: string) => void;
|
onSelect?: (selected: string) => void;
|
||||||
|
hotkeyScope: string;
|
||||||
|
onEnter?: (itemId: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StyledSelectableItemsContainer = styled.div`
|
const StyledSelectableItemsContainer = styled.div`
|
||||||
@ -17,8 +21,23 @@ const StyledSelectableItemsContainer = styled.div`
|
|||||||
export const SelectableList = ({
|
export const SelectableList = ({
|
||||||
children,
|
children,
|
||||||
selectableListId,
|
selectableListId,
|
||||||
|
hotkeyScope,
|
||||||
|
selectableItemIds,
|
||||||
|
onEnter,
|
||||||
}: SelectableListProps) => {
|
}: SelectableListProps) => {
|
||||||
useSelectableListHotKeys(selectableListId);
|
useSelectableListHotKeys(selectableListId, hotkeyScope);
|
||||||
|
|
||||||
|
const { setSelectableItemIds, setSelectableListOnEnter } = useSelectableList({
|
||||||
|
selectableListId,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSelectableListOnEnter(() => onEnter);
|
||||||
|
}, [onEnter, setSelectableListOnEnter]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSelectableItemIds(selectableItemIds);
|
||||||
|
}, [selectableItemIds, setSelectableItemIds]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SelectableListScope selectableListScopeId={selectableListId}>
|
<SelectableListScope selectableListScopeId={selectableListId}>
|
||||||
|
|||||||
@ -1,16 +1,37 @@
|
|||||||
import { isNull } from '@sniptt/guards';
|
|
||||||
import { useRecoilCallback } from 'recoil';
|
import { useRecoilCallback } from 'recoil';
|
||||||
import { Key } from 'ts-key-enum';
|
import { Key } from 'ts-key-enum';
|
||||||
|
|
||||||
import { getSelectableListScopedStates } from '@/ui/layout/selectable-list/utils/internal/getSelectableListScopedStates';
|
import { getSelectableListScopedStates } from '@/ui/layout/selectable-list/utils/internal/getSelectableListScopedStates';
|
||||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
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';
|
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(
|
const handleSelect = useRecoilCallback(
|
||||||
({ snapshot, set }) =>
|
({ snapshot, set }) =>
|
||||||
(direction: 'up' | 'down') => {
|
(direction: Direction) => {
|
||||||
const { selectedItemIdState, selectableItemIdsState } =
|
const { selectedItemIdState, selectableItemIdsState } =
|
||||||
getSelectableListScopedStates({
|
getSelectableListScopedStates({
|
||||||
selectableListScopeId: scopeId,
|
selectableListScopeId: scopeId,
|
||||||
@ -21,31 +42,54 @@ export const useSelectableListHotKeys = (scopeId: string) => {
|
|||||||
selectableItemIdsState,
|
selectableItemIdsState,
|
||||||
);
|
);
|
||||||
|
|
||||||
const computeNextId = (direction: 'up' | 'down') => {
|
const { row: currentRow, col: currentCol } = findPosition(
|
||||||
|
selectableItemIds,
|
||||||
|
selectedItemId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const computeNextId = (direction: Direction) => {
|
||||||
if (selectableItemIds.length === 0) {
|
if (selectableItemIds.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isNull(selectedItemId)) {
|
const isSingleRow = selectableItemIds.length === 1;
|
||||||
return direction === 'up'
|
|
||||||
? selectableItemIds[selectableItemIds.length - 1]
|
let nextRow: number;
|
||||||
: selectableItemIds[0];
|
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);
|
return selectableItemIds[nextRow][nextCol];
|
||||||
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];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const nextId = computeNextId(direction);
|
const nextId = computeNextId(direction);
|
||||||
@ -70,17 +114,45 @@ export const useSelectableListHotKeys = (scopeId: string) => {
|
|||||||
[scopeId],
|
[scopeId],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useScopedHotkeys(Key.ArrowUp, () => handleSelect('up'), hotkeyScope, []);
|
||||||
|
|
||||||
|
useScopedHotkeys(Key.ArrowDown, () => handleSelect('down'), hotkeyScope, []);
|
||||||
|
|
||||||
|
useScopedHotkeys(Key.ArrowLeft, () => handleSelect('left'), hotkeyScope, []);
|
||||||
|
|
||||||
useScopedHotkeys(
|
useScopedHotkeys(
|
||||||
Key.ArrowUp,
|
Key.ArrowRight,
|
||||||
() => handleSelect('up'),
|
() => handleSelect('right'),
|
||||||
AppHotkeyScope.CommandMenu,
|
hotkeyScope,
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
useScopedHotkeys(
|
useScopedHotkeys(
|
||||||
Key.ArrowDown,
|
Key.Enter,
|
||||||
() => handleSelect('down'),
|
useRecoilCallback(
|
||||||
AppHotkeyScope.CommandMenu,
|
({ snapshot }) =>
|
||||||
|
() => {
|
||||||
|
const { selectedItemIdState, selectableListOnEnterState } =
|
||||||
|
getSelectableListScopedStates({
|
||||||
|
selectableListScopeId: scopeId,
|
||||||
|
});
|
||||||
|
const selectedItemId = getSnapshotValue(
|
||||||
|
snapshot,
|
||||||
|
selectedItemIdState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const onEnter = getSnapshotValue(
|
||||||
|
snapshot,
|
||||||
|
selectableListOnEnterState,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (selectedItemId) {
|
||||||
|
onEnter?.(selectedItemId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[scopeId],
|
||||||
|
),
|
||||||
|
hotkeyScope,
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -20,6 +20,7 @@ export const useSelectableListScopedStates = (
|
|||||||
selectedItemIdState,
|
selectedItemIdState,
|
||||||
selectableItemIdsState,
|
selectableItemIdsState,
|
||||||
isSelectedItemIdSelector,
|
isSelectedItemIdSelector,
|
||||||
|
selectableListOnEnterState,
|
||||||
} = getSelectableListScopedStates({
|
} = getSelectableListScopedStates({
|
||||||
selectableListScopeId: scopeId,
|
selectableListScopeId: scopeId,
|
||||||
itemId: itemId,
|
itemId: itemId,
|
||||||
@ -30,5 +31,6 @@ export const useSelectableListScopedStates = (
|
|||||||
isSelectedItemIdSelector,
|
isSelectedItemIdSelector,
|
||||||
selectableItemIdsState,
|
selectableItemIdsState,
|
||||||
selectedItemIdState,
|
selectedItemIdState,
|
||||||
|
selectableListOnEnterState,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -15,18 +15,25 @@ export const useSelectableList = (props?: UseSelectableListProps) => {
|
|||||||
props?.selectableListId,
|
props?.selectableListId,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { selectableItemIdsState, isSelectedItemIdSelector } =
|
const {
|
||||||
useSelectableListScopedStates({
|
selectableItemIdsState,
|
||||||
selectableListScopeId: scopeId,
|
isSelectedItemIdSelector,
|
||||||
itemId: props?.itemId,
|
selectableListOnEnterState,
|
||||||
});
|
} = useSelectableListScopedStates({
|
||||||
|
selectableListScopeId: scopeId,
|
||||||
|
itemId: props?.itemId,
|
||||||
|
});
|
||||||
|
|
||||||
const setSelectableItemIds = useSetRecoilState(selectableItemIdsState);
|
const setSelectableItemIds = useSetRecoilState(selectableItemIdsState);
|
||||||
|
const setSelectableListOnEnter = useSetRecoilState(
|
||||||
|
selectableListOnEnterState,
|
||||||
|
);
|
||||||
const isSelectedItemId = useRecoilValue(isSelectedItemIdSelector);
|
const isSelectedItemId = useRecoilValue(isSelectedItemIdSelector);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
setSelectableItemIds,
|
setSelectableItemIds,
|
||||||
isSelectedItemId,
|
isSelectedItemId,
|
||||||
|
setSelectableListOnEnter,
|
||||||
selectableListId: scopeId,
|
selectableListId: scopeId,
|
||||||
isSelectedItemIdSelector,
|
isSelectedItemIdSelector,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
|
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
|
||||||
|
|
||||||
export const selectableItemIdsScopedState = createScopedState<string[]>({
|
export const selectableItemIdsScopedState = createScopedState<string[][]>({
|
||||||
key: 'selectableItemIdsScopedState',
|
key: 'selectableItemIdsScopedState',
|
||||||
defaultValue: [],
|
defaultValue: [[]],
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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,
|
||||||
|
});
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import { selectableItemIdsScopedState } from '@/ui/layout/selectable-list/states/selectableItemIdsScopedState';
|
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 { selectedItemIdScopedState } from '@/ui/layout/selectable-list/states/selectedItemIdScopedState';
|
||||||
import { isSelectedItemIdScopedFamilySelector } from '@/ui/layout/selectable-list/states/selectors/isSelectedItemIdScopedFamilySelector';
|
import { isSelectedItemIdScopedFamilySelector } from '@/ui/layout/selectable-list/states/selectors/isSelectedItemIdScopedFamilySelector';
|
||||||
import { getScopedState } from '@/ui/utilities/recoil-scope/utils/getScopedState';
|
import { getScopedState } from '@/ui/utilities/recoil-scope/utils/getScopedState';
|
||||||
@ -27,9 +28,15 @@ export const getSelectableListScopedStates = ({
|
|||||||
selectableListScopeId,
|
selectableListScopeId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const selectableListOnEnterState = getScopedState(
|
||||||
|
selectableListOnEnterScopedState,
|
||||||
|
selectableListScopeId,
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isSelectedItemIdSelector,
|
isSelectedItemIdSelector,
|
||||||
selectableItemIdsState,
|
selectableItemIdsState,
|
||||||
selectedItemIdState,
|
selectedItemIdState,
|
||||||
|
selectableListOnEnterState,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -74,6 +74,7 @@ export const useSetHotkeyScope = () =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
scopesToSet.push(newHotkeyScope.scope);
|
scopesToSet.push(newHotkeyScope.scope);
|
||||||
|
console.log(scopesToSet);
|
||||||
set(internalHotkeysEnabledScopesState, scopesToSet);
|
set(internalHotkeysEnabledScopesState, scopesToSet);
|
||||||
set(currentHotkeyScopeState, newHotkeyScope);
|
set(currentHotkeyScopeState, newHotkeyScope);
|
||||||
},
|
},
|
||||||
|
|||||||
11
front/src/utils/array/array-to-chunks.ts
Normal file
11
front/src/utils/array/array-to-chunks.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
// split an array into subarrays of a given size
|
||||||
|
export const arrayToChunks = <T>(array: T[], size: number) => {
|
||||||
|
const arrayCopy = [...array];
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
while (arrayCopy.length) {
|
||||||
|
results.push(arrayCopy.splice(0, size));
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user