Fix hook bug (#2995)
* Fix hook bug * Fix --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -228,7 +228,7 @@ export const CommandMenu = () => {
|
|||||||
<StyledInnerList>
|
<StyledInnerList>
|
||||||
<SelectableList
|
<SelectableList
|
||||||
selectableListId="command-menu-list"
|
selectableListId="command-menu-list"
|
||||||
selectableItemIds={[selectableItemIds]}
|
selectableItemIdArray={selectableItemIds}
|
||||||
hotkeyScope={AppHotkeyScope.CommandMenu}
|
hotkeyScope={AppHotkeyScope.CommandMenu}
|
||||||
onEnter={(itemId) => {
|
onEnter={(itemId) => {
|
||||||
const command = [
|
const command = [
|
||||||
|
|||||||
@ -89,7 +89,7 @@ export const MultipleEntitySelect = <
|
|||||||
<DropdownMenuItemsContainer hasMaxHeight>
|
<DropdownMenuItemsContainer hasMaxHeight>
|
||||||
<SelectableList
|
<SelectableList
|
||||||
selectableListId="multiple-entity-select-list"
|
selectableListId="multiple-entity-select-list"
|
||||||
selectableItemIds={[selectableItemIds]}
|
selectableItemIdArray={selectableItemIds}
|
||||||
hotkeyScope={RelationPickerHotkeyScope.RelationPicker}
|
hotkeyScope={RelationPickerHotkeyScope.RelationPicker}
|
||||||
onEnter={(_itemId) => {
|
onEnter={(_itemId) => {
|
||||||
if (_itemId in value === false || value[_itemId] === false) {
|
if (_itemId in value === false || value[_itemId] === false) {
|
||||||
|
|||||||
@ -0,0 +1,50 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
|
||||||
|
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
|
||||||
|
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
||||||
|
import { MenuItemSelectAvatar } from '@/ui/navigation/menu-item/components/MenuItemSelectAvatar';
|
||||||
|
import { Avatar } from '@/users/components/Avatar';
|
||||||
|
|
||||||
|
type SelectableMenuItemSelectProps = {
|
||||||
|
entity: EntityForSelect;
|
||||||
|
onEntitySelected: (entitySelected?: EntityForSelect) => void;
|
||||||
|
selectedEntity?: EntityForSelect;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledSelectableItem = styled(SelectableItem)`
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const SelectableMenuItemSelect = ({
|
||||||
|
entity,
|
||||||
|
onEntitySelected,
|
||||||
|
selectedEntity,
|
||||||
|
}: SelectableMenuItemSelectProps) => {
|
||||||
|
const { isSelectedItemId } = useSelectableList({
|
||||||
|
selectableListId: 'single-entity-select-base-list',
|
||||||
|
itemId: entity.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledSelectableItem itemId={entity.id} key={entity.id}>
|
||||||
|
<MenuItemSelectAvatar
|
||||||
|
key={entity.id}
|
||||||
|
testId="menu-item"
|
||||||
|
onClick={() => onEntitySelected(entity)}
|
||||||
|
text={entity.name}
|
||||||
|
selected={selectedEntity?.id === entity.id}
|
||||||
|
hovered={isSelectedItemId}
|
||||||
|
avatar={
|
||||||
|
<Avatar
|
||||||
|
avatarUrl={entity.avatarUrl}
|
||||||
|
colorId={entity.id}
|
||||||
|
placeholder={entity.name}
|
||||||
|
size="md"
|
||||||
|
type={entity.avatarType ?? 'rounded'}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StyledSelectableItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -7,21 +7,18 @@ import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useLis
|
|||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
import { useEntitySelectSearch } from '../hooks/useEntitySelectSearch';
|
import { useEntitySelectSearch } from '../hooks/useEntitySelectSearch';
|
||||||
import { EntityForSelect } from '../types/EntityForSelect';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
SingleEntitySelectBase,
|
SingleEntitySelectBase,
|
||||||
SingleEntitySelectBaseProps,
|
SingleEntitySelectBaseProps,
|
||||||
} from './SingleEntitySelectBase';
|
} from './SingleEntitySelectBase';
|
||||||
|
|
||||||
export type SingleEntitySelectProps<
|
export type SingleEntitySelectProps = {
|
||||||
CustomEntityForSelect extends EntityForSelect,
|
|
||||||
> = {
|
|
||||||
disableBackgroundBlur?: boolean;
|
disableBackgroundBlur?: boolean;
|
||||||
onCreate?: () => void;
|
onCreate?: () => void;
|
||||||
width?: number;
|
width?: number;
|
||||||
} & Pick<
|
} & Pick<
|
||||||
SingleEntitySelectBaseProps<CustomEntityForSelect>,
|
SingleEntitySelectBaseProps,
|
||||||
| 'EmptyIcon'
|
| 'EmptyIcon'
|
||||||
| 'emptyLabel'
|
| 'emptyLabel'
|
||||||
| 'entitiesToSelect'
|
| 'entitiesToSelect'
|
||||||
@ -31,9 +28,7 @@ export type SingleEntitySelectProps<
|
|||||||
| 'selectedEntity'
|
| 'selectedEntity'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export const SingleEntitySelect = <
|
export const SingleEntitySelect = ({
|
||||||
CustomEntityForSelect extends EntityForSelect,
|
|
||||||
>({
|
|
||||||
EmptyIcon,
|
EmptyIcon,
|
||||||
disableBackgroundBlur = false,
|
disableBackgroundBlur = false,
|
||||||
emptyLabel,
|
emptyLabel,
|
||||||
@ -44,7 +39,7 @@ export const SingleEntitySelect = <
|
|||||||
onEntitySelected,
|
onEntitySelected,
|
||||||
selectedEntity,
|
selectedEntity,
|
||||||
width = 200,
|
width = 200,
|
||||||
}: SingleEntitySelectProps<CustomEntityForSelect>) => {
|
}: SingleEntitySelectProps) => {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const { searchFilter, handleSearchFilterChange } = useEntitySelectSearch();
|
const { searchFilter, handleSearchFilterChange } = useEntitySelectSearch();
|
||||||
|
|||||||
@ -1,39 +1,31 @@
|
|||||||
/* eslint-disable react-hooks/rules-of-hooks */
|
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import { isNonEmptyString } from '@sniptt/guards';
|
import { isNonEmptyString } from '@sniptt/guards';
|
||||||
import { Key } from 'ts-key-enum';
|
import { Key } from 'ts-key-enum';
|
||||||
|
|
||||||
|
import { SelectableMenuItemSelect } from '@/object-record/relation-picker/components/SelectableMenuItemSelect';
|
||||||
import { IconPlus } from '@/ui/display/icon';
|
import { IconPlus } from '@/ui/display/icon';
|
||||||
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
|
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
|
||||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||||
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
|
|
||||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||||
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
|
||||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||||
import { MenuItemSelect } from '@/ui/navigation/menu-item/components/MenuItemSelect';
|
import { MenuItemSelect } from '@/ui/navigation/menu-item/components/MenuItemSelect';
|
||||||
import { MenuItemSelectAvatar } from '@/ui/navigation/menu-item/components/MenuItemSelectAvatar';
|
|
||||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
import { Avatar } from '@/users/components/Avatar';
|
|
||||||
import { assertNotNull } from '~/utils/assert';
|
import { assertNotNull } from '~/utils/assert';
|
||||||
|
|
||||||
import { CreateNewButton } from '../../../ui/input/relation-picker/components/CreateNewButton';
|
import { CreateNewButton } from '../../../ui/input/relation-picker/components/CreateNewButton';
|
||||||
import { DropdownMenuSkeletonItem } from '../../../ui/input/relation-picker/components/skeletons/DropdownMenuSkeletonItem';
|
import { DropdownMenuSkeletonItem } from '../../../ui/input/relation-picker/components/skeletons/DropdownMenuSkeletonItem';
|
||||||
import { CreateButtonId, EmptyButtonId } from '../constants';
|
|
||||||
import { useEntitySelectScroll } from '../hooks/useEntitySelectScroll';
|
|
||||||
import { EntityForSelect } from '../types/EntityForSelect';
|
import { EntityForSelect } from '../types/EntityForSelect';
|
||||||
import { RelationPickerHotkeyScope } from '../types/RelationPickerHotkeyScope';
|
import { RelationPickerHotkeyScope } from '../types/RelationPickerHotkeyScope';
|
||||||
|
|
||||||
export type SingleEntitySelectBaseProps<
|
export type SingleEntitySelectBaseProps = {
|
||||||
CustomEntityForSelect extends EntityForSelect,
|
|
||||||
> = {
|
|
||||||
EmptyIcon?: IconComponent;
|
EmptyIcon?: IconComponent;
|
||||||
emptyLabel?: string;
|
emptyLabel?: string;
|
||||||
entitiesToSelect: CustomEntityForSelect[];
|
entitiesToSelect: EntityForSelect[];
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
onEntitySelected: (entity?: CustomEntityForSelect) => void;
|
onEntitySelected: (entity?: EntityForSelect) => void;
|
||||||
selectedEntity?: CustomEntityForSelect;
|
selectedEntity?: EntityForSelect;
|
||||||
onCreate?: () => void;
|
onCreate?: () => void;
|
||||||
showCreateButton?: boolean;
|
showCreateButton?: boolean;
|
||||||
SelectAllIcon?: IconComponent;
|
SelectAllIcon?: IconComponent;
|
||||||
@ -43,9 +35,7 @@ export type SingleEntitySelectBaseProps<
|
|||||||
onAllEntitySelected?: () => void;
|
onAllEntitySelected?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SingleEntitySelectBase = <
|
export const SingleEntitySelectBase = ({
|
||||||
CustomEntityForSelect extends EntityForSelect,
|
|
||||||
>({
|
|
||||||
EmptyIcon,
|
EmptyIcon,
|
||||||
emptyLabel,
|
emptyLabel,
|
||||||
entitiesToSelect,
|
entitiesToSelect,
|
||||||
@ -60,23 +50,14 @@ export const SingleEntitySelectBase = <
|
|||||||
isAllEntitySelected,
|
isAllEntitySelected,
|
||||||
isAllEntitySelectShown,
|
isAllEntitySelectShown,
|
||||||
onAllEntitySelected,
|
onAllEntitySelected,
|
||||||
}: SingleEntitySelectBaseProps<CustomEntityForSelect>) => {
|
}: SingleEntitySelectBaseProps) => {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const entitiesInDropdown = [selectedEntity, ...entitiesToSelect].filter(
|
const entitiesInDropdown = [selectedEntity, ...entitiesToSelect].filter(
|
||||||
(entity): entity is CustomEntityForSelect =>
|
(entity): entity is EntityForSelect =>
|
||||||
assertNotNull(entity) && isNonEmptyString(entity.name),
|
assertNotNull(entity) && isNonEmptyString(entity.name),
|
||||||
);
|
);
|
||||||
|
|
||||||
const { preselectedOptionId } = useEntitySelectScroll({
|
|
||||||
selectableOptionIds: [
|
|
||||||
EmptyButtonId,
|
|
||||||
...entitiesInDropdown.map((item) => item.id),
|
|
||||||
...(showCreateButton ? [CreateButtonId] : []),
|
|
||||||
],
|
|
||||||
containerRef,
|
|
||||||
});
|
|
||||||
|
|
||||||
useScopedHotkeys(
|
useScopedHotkeys(
|
||||||
Key.Escape,
|
Key.Escape,
|
||||||
() => {
|
() => {
|
||||||
@ -90,94 +71,72 @@ export const SingleEntitySelectBase = <
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef}>
|
<div ref={containerRef}>
|
||||||
<DropdownMenuItemsContainer hasMaxHeight>
|
<SelectableList
|
||||||
{loading ? (
|
selectableListId="single-entity-select-base-list"
|
||||||
<DropdownMenuSkeletonItem />
|
selectableItemIdArray={selectableItemIds}
|
||||||
) : entitiesInDropdown.length === 0 && !isAllEntitySelectShown ? (
|
hotkeyScope={RelationPickerHotkeyScope.RelationPicker}
|
||||||
<MenuItem text="No result" />
|
onEnter={(_itemId) => {
|
||||||
) : (
|
if (showCreateButton) {
|
||||||
<>
|
onCreate?.();
|
||||||
{isAllEntitySelectShown &&
|
} else {
|
||||||
selectAllLabel &&
|
const entity = entitiesInDropdown.findIndex(
|
||||||
onAllEntitySelected && (
|
(entity) => entity.id === _itemId,
|
||||||
|
);
|
||||||
|
onEntitySelected(entitiesInDropdown[entity]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DropdownMenuItemsContainer hasMaxHeight>
|
||||||
|
{loading ? (
|
||||||
|
<DropdownMenuSkeletonItem />
|
||||||
|
) : entitiesInDropdown.length === 0 && !isAllEntitySelectShown ? (
|
||||||
|
<MenuItem text="No result" />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{isAllEntitySelectShown &&
|
||||||
|
selectAllLabel &&
|
||||||
|
onAllEntitySelected && (
|
||||||
|
<MenuItemSelect
|
||||||
|
key="select-all"
|
||||||
|
onClick={() => onAllEntitySelected()}
|
||||||
|
LeftIcon={SelectAllIcon}
|
||||||
|
text={selectAllLabel}
|
||||||
|
selected={!!isAllEntitySelected}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{emptyLabel && (
|
||||||
<MenuItemSelect
|
<MenuItemSelect
|
||||||
onClick={() => onAllEntitySelected()}
|
key="select-none"
|
||||||
LeftIcon={SelectAllIcon}
|
onClick={() => onEntitySelected()}
|
||||||
text={selectAllLabel}
|
LeftIcon={EmptyIcon}
|
||||||
hovered={preselectedOptionId === EmptyButtonId}
|
text={emptyLabel}
|
||||||
selected={!!isAllEntitySelected}
|
selected={!selectedEntity}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{emptyLabel && (
|
{entitiesInDropdown?.map((entity) => (
|
||||||
<MenuItemSelect
|
<SelectableMenuItemSelect
|
||||||
onClick={() => onEntitySelected()}
|
key={entity.id}
|
||||||
LeftIcon={EmptyIcon}
|
entity={entity}
|
||||||
text={emptyLabel}
|
onEntitySelected={onEntitySelected}
|
||||||
hovered={preselectedOptionId === EmptyButtonId}
|
selectedEntity={selectedEntity}
|
||||||
selected={!selectedEntity}
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DropdownMenuItemsContainer>
|
||||||
|
{showCreateButton && (
|
||||||
|
<>
|
||||||
|
<DropdownMenuItemsContainer hasMaxHeight>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<CreateNewButton
|
||||||
|
onClick={onCreate}
|
||||||
|
LeftIcon={IconPlus}
|
||||||
|
text="Add New"
|
||||||
/>
|
/>
|
||||||
)}
|
</DropdownMenuItemsContainer>
|
||||||
{entitiesInDropdown?.map((entity) => (
|
|
||||||
<SelectableList
|
|
||||||
selectableListId="single-entity-select-base-list"
|
|
||||||
selectableItemIds={[selectableItemIds]}
|
|
||||||
hotkeyScope={RelationPickerHotkeyScope.RelationPicker}
|
|
||||||
onEnter={(_itemId) => {
|
|
||||||
if (
|
|
||||||
showCreateButton &&
|
|
||||||
preselectedOptionId === CreateButtonId
|
|
||||||
) {
|
|
||||||
onCreate?.();
|
|
||||||
} else {
|
|
||||||
const entity = entitiesInDropdown.findIndex(
|
|
||||||
(entity) => entity.id === _itemId,
|
|
||||||
);
|
|
||||||
onEntitySelected(entitiesInDropdown[entity]);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectableItem itemId={entity.id} key={entity.id}>
|
|
||||||
<MenuItemSelectAvatar
|
|
||||||
key={entity.id}
|
|
||||||
testId="menu-item"
|
|
||||||
onClick={() => onEntitySelected(entity)}
|
|
||||||
text={entity.name}
|
|
||||||
selected={selectedEntity?.id === entity.id}
|
|
||||||
hovered={
|
|
||||||
useSelectableList({
|
|
||||||
selectableListId: 'single-entity-select-base-list',
|
|
||||||
itemId: entity.id,
|
|
||||||
}).isSelectedItemId
|
|
||||||
}
|
|
||||||
avatar={
|
|
||||||
<Avatar
|
|
||||||
avatarUrl={entity.avatarUrl}
|
|
||||||
colorId={entity.id}
|
|
||||||
placeholder={entity.name}
|
|
||||||
size="md"
|
|
||||||
type={entity.avatarType ?? 'rounded'}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</SelectableItem>
|
|
||||||
</SelectableList>
|
|
||||||
))}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</DropdownMenuItemsContainer>
|
</SelectableList>
|
||||||
{showCreateButton && (
|
|
||||||
<>
|
|
||||||
<DropdownMenuItemsContainer hasMaxHeight>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<CreateNewButton
|
|
||||||
onClick={onCreate}
|
|
||||||
LeftIcon={IconPlus}
|
|
||||||
text="Add New"
|
|
||||||
hovered={preselectedOptionId === CreateButtonId}
|
|
||||||
/>
|
|
||||||
</DropdownMenuItemsContainer>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,101 +0,0 @@
|
|||||||
import scrollIntoView from 'scroll-into-view';
|
|
||||||
import { Key } from 'ts-key-enum';
|
|
||||||
|
|
||||||
import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker';
|
|
||||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
|
||||||
|
|
||||||
import { CreateButtonId } from '../constants';
|
|
||||||
import { RelationPickerHotkeyScope } from '../types/RelationPickerHotkeyScope';
|
|
||||||
import { getPreselectedIdIndex } from '../utils/getPreselectedIdIndex';
|
|
||||||
|
|
||||||
export const useEntitySelectScroll = ({
|
|
||||||
containerRef,
|
|
||||||
selectableOptionIds,
|
|
||||||
}: {
|
|
||||||
selectableOptionIds: string[];
|
|
||||||
containerRef: React.RefObject<HTMLDivElement>;
|
|
||||||
}) => {
|
|
||||||
const { relationPickerPreselectedId, setRelationPickerPreselectedId } =
|
|
||||||
useRelationPicker();
|
|
||||||
|
|
||||||
const preselectedIdIndex = getPreselectedIdIndex(
|
|
||||||
selectableOptionIds,
|
|
||||||
relationPickerPreselectedId ?? '',
|
|
||||||
);
|
|
||||||
|
|
||||||
const resetScroll = () => {
|
|
||||||
setRelationPickerPreselectedId('');
|
|
||||||
|
|
||||||
const preselectedRef = containerRef.current?.children[0] as HTMLElement;
|
|
||||||
|
|
||||||
scrollIntoView(preselectedRef, {
|
|
||||||
align: {
|
|
||||||
top: 0,
|
|
||||||
},
|
|
||||||
isScrollable: (target) => {
|
|
||||||
return target === containerRef.current;
|
|
||||||
},
|
|
||||||
time: 0,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
useScopedHotkeys(
|
|
||||||
Key.ArrowUp,
|
|
||||||
() => {
|
|
||||||
const previousSelectableIndex = Math.max(preselectedIdIndex - 1, 0);
|
|
||||||
const previousSelectableId = selectableOptionIds[previousSelectableIndex];
|
|
||||||
setRelationPickerPreselectedId(previousSelectableId);
|
|
||||||
const preselectedRef = containerRef.current?.children[
|
|
||||||
previousSelectableIndex
|
|
||||||
] as HTMLElement;
|
|
||||||
|
|
||||||
if (preselectedRef) {
|
|
||||||
scrollIntoView(preselectedRef, {
|
|
||||||
align: {
|
|
||||||
top: 0.5,
|
|
||||||
},
|
|
||||||
isScrollable: (target) => {
|
|
||||||
return target === containerRef.current;
|
|
||||||
},
|
|
||||||
time: 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
RelationPickerHotkeyScope.RelationPicker,
|
|
||||||
[selectableOptionIds],
|
|
||||||
);
|
|
||||||
|
|
||||||
useScopedHotkeys(
|
|
||||||
Key.ArrowDown,
|
|
||||||
() => {
|
|
||||||
const nextSelectableIndex = Math.min(
|
|
||||||
preselectedIdIndex + 1,
|
|
||||||
selectableOptionIds?.length - 1,
|
|
||||||
);
|
|
||||||
const nextSelectableId = selectableOptionIds[nextSelectableIndex];
|
|
||||||
setRelationPickerPreselectedId(nextSelectableId);
|
|
||||||
if (nextSelectableId !== CreateButtonId) {
|
|
||||||
const preselectedRef = containerRef.current?.children[
|
|
||||||
nextSelectableIndex
|
|
||||||
] as HTMLElement;
|
|
||||||
|
|
||||||
if (preselectedRef) {
|
|
||||||
scrollIntoView(preselectedRef, {
|
|
||||||
align: {
|
|
||||||
top: 0.15,
|
|
||||||
},
|
|
||||||
isScrollable: (target) => target === containerRef.current,
|
|
||||||
time: 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
RelationPickerHotkeyScope.RelationPicker,
|
|
||||||
[selectableOptionIds],
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
preselectedOptionId: relationPickerPreselectedId,
|
|
||||||
resetScroll,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -138,7 +138,7 @@ export const IconPicker = ({
|
|||||||
dropdownComponents={
|
dropdownComponents={
|
||||||
<SelectableList
|
<SelectableList
|
||||||
selectableListId="icon-list"
|
selectableListId="icon-list"
|
||||||
selectableItemIds={iconKeys2d}
|
selectableItemIdMatrix={iconKeys2d}
|
||||||
hotkeyScope={IconPickerHotkeyScope.IconPicker}
|
hotkeyScope={IconPickerHotkeyScope.IconPicker}
|
||||||
onEnter={(iconKey) => {
|
onEnter={(iconKey) => {
|
||||||
onChange({ iconKey, Icon: getIcon(iconKey) });
|
onChange({ iconKey, Icon: getIcon(iconKey) });
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import styled from '@emotion/styled';
|
|||||||
|
|
||||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||||
|
|
||||||
const StyledCreateNewButton = styled(MenuItem)<{ hovered: boolean }>`
|
const StyledCreateNewButton = styled(MenuItem)<{ hovered?: boolean }>`
|
||||||
${({ hovered, theme }) =>
|
${({ hovered, theme }) =>
|
||||||
hovered &&
|
hovered &&
|
||||||
css`
|
css`
|
||||||
|
|||||||
@ -1,14 +1,19 @@
|
|||||||
import React, { useEffect, useRef } from 'react';
|
import { ReactNode, useEffect, useRef } from 'react';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
import { useSelectableListScopedStates } from '@/ui/layout/selectable-list/hooks/internal/useSelectableListScopedStates';
|
import { useSelectableListScopedStates } from '@/ui/layout/selectable-list/hooks/internal/useSelectableListScopedStates';
|
||||||
|
|
||||||
type SelectableItemProps = {
|
export type SelectableItemProps = {
|
||||||
itemId: string;
|
itemId: string;
|
||||||
children: React.ReactElement;
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SelectableItem = ({ itemId, children }: SelectableItemProps) => {
|
export const SelectableItem = ({
|
||||||
|
itemId,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
}: SelectableItemProps) => {
|
||||||
const { isSelectedItemIdSelector } = useSelectableListScopedStates({
|
const { isSelectedItemIdSelector } = useSelectableListScopedStates({
|
||||||
itemId: itemId,
|
itemId: itemId,
|
||||||
});
|
});
|
||||||
@ -23,5 +28,9 @@ export const SelectableItem = ({ itemId, children }: SelectableItemProps) => {
|
|||||||
}
|
}
|
||||||
}, [isSelectedItemId]);
|
}, [isSelectedItemId]);
|
||||||
|
|
||||||
return <div ref={scrollRef}>{children}</div>;
|
return (
|
||||||
|
<div className={className} ref={scrollRef}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,28 +1,26 @@
|
|||||||
import { ReactNode, useEffect } from 'react';
|
import { ReactNode, useEffect } from 'react';
|
||||||
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 { 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';
|
||||||
|
import { arrayToChunks } from '~/utils/array/array-to-chunks';
|
||||||
|
|
||||||
type SelectableListProps = {
|
type SelectableListProps = {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
selectableListId: string;
|
selectableListId: string;
|
||||||
selectableItemIds: string[][];
|
selectableItemIdArray?: string[];
|
||||||
|
selectableItemIdMatrix?: string[][];
|
||||||
onSelect?: (selected: string) => void;
|
onSelect?: (selected: string) => void;
|
||||||
hotkeyScope: string;
|
hotkeyScope: string;
|
||||||
onEnter?: (itemId: string) => void;
|
onEnter?: (itemId: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StyledSelectableItemsContainer = styled.div`
|
|
||||||
width: 100%;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const SelectableList = ({
|
export const SelectableList = ({
|
||||||
children,
|
children,
|
||||||
selectableListId,
|
selectableListId,
|
||||||
hotkeyScope,
|
hotkeyScope,
|
||||||
selectableItemIds,
|
selectableItemIdArray,
|
||||||
|
selectableItemIdMatrix,
|
||||||
onEnter,
|
onEnter,
|
||||||
}: SelectableListProps) => {
|
}: SelectableListProps) => {
|
||||||
useSelectableListHotKeys(selectableListId, hotkeyScope);
|
useSelectableListHotKeys(selectableListId, hotkeyScope);
|
||||||
@ -36,14 +34,24 @@ export const SelectableList = ({
|
|||||||
}, [onEnter, setSelectableListOnEnter]);
|
}, [onEnter, setSelectableListOnEnter]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectableItemIds(selectableItemIds);
|
if (!selectableItemIdArray && !selectableItemIdMatrix) {
|
||||||
}, [selectableItemIds, setSelectableItemIds]);
|
throw new Error(
|
||||||
|
'Either selectableItemIdArray or selectableItemIdsMatrix must be provided',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectableItemIdMatrix) {
|
||||||
|
setSelectableItemIds(selectableItemIdMatrix);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectableItemIdArray) {
|
||||||
|
setSelectableItemIds(arrayToChunks(selectableItemIdArray, 1));
|
||||||
|
}
|
||||||
|
}, [selectableItemIdArray, selectableItemIdMatrix, setSelectableItemIds]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SelectableListScope selectableListScopeId={selectableListId}>
|
<SelectableListScope selectableListScopeId={selectableListId}>
|
||||||
<StyledSelectableItemsContainer>
|
{children}
|
||||||
{children}
|
|
||||||
</StyledSelectableItemsContainer>
|
|
||||||
</SelectableListScope>
|
</SelectableListScope>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -16,7 +16,7 @@ export const useSelectableListHotKeys = (
|
|||||||
selectedItemId?: string | null,
|
selectedItemId?: string | null,
|
||||||
) => {
|
) => {
|
||||||
if (!selectedItemId) {
|
if (!selectedItemId) {
|
||||||
return { row: 0, col: -1 };
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let row = 0; row < selectableItemIds.length; row++) {
|
for (let row = 0; row < selectableItemIds.length; row++) {
|
||||||
@ -25,7 +25,6 @@ export const useSelectableListHotKeys = (
|
|||||||
return { row, col };
|
return { row, col };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { row: 0, col: 0 };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelect = useRecoilCallback(
|
const handleSelect = useRecoilCallback(
|
||||||
@ -41,12 +40,15 @@ export const useSelectableListHotKeys = (
|
|||||||
selectableItemIdsState,
|
selectableItemIdsState,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { row: currentRow, col: currentCol } = findPosition(
|
const currentPosition = findPosition(selectableItemIds, selectedItemId);
|
||||||
selectableItemIds,
|
|
||||||
selectedItemId,
|
|
||||||
);
|
|
||||||
|
|
||||||
const computeNextId = (direction: Direction) => {
|
const computeNextId = (direction: Direction) => {
|
||||||
|
if (!selectedItemId || !currentPosition) {
|
||||||
|
return selectableItemIds[0][0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const { row: currentRow, col: currentCol } = currentPosition;
|
||||||
|
|
||||||
if (selectableItemIds.length === 0) {
|
if (selectableItemIds.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -93,7 +95,7 @@ export const useSelectableListHotKeys = (
|
|||||||
|
|
||||||
const nextId = computeNextId(direction);
|
const nextId = computeNextId(direction);
|
||||||
|
|
||||||
if (!selectedItemId || (selectedItemId && selectedItemId !== nextId)) {
|
if (selectedItemId !== nextId) {
|
||||||
if (nextId) {
|
if (nextId) {
|
||||||
const { isSelectedItemIdSelector } = getSelectableListScopedStates({
|
const { isSelectedItemIdSelector } = getSelectableListScopedStates({
|
||||||
selectableListScopeId: scopeId,
|
selectableListScopeId: scopeId,
|
||||||
|
|||||||
@ -19,7 +19,7 @@ export const StyledMenuItemBase = styled.li<MenuItemBaseProps>`
|
|||||||
background: ${({ isKeySelected, theme }) =>
|
background: ${({ isKeySelected, theme }) =>
|
||||||
isKeySelected
|
isKeySelected
|
||||||
? theme.background.transparent.light
|
? theme.background.transparent.light
|
||||||
: theme.background.primary};
|
: theme.background.secondary};
|
||||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user