Adds KeyBoard Navigation to ObjectFilterDropDownFilterSelect ( #4365 ) (#6613)

fixes #4365

---------

Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
Faisal-imtiyaz123
2024-09-06 23:43:51 +05:30
committed by GitHub
parent c0d0f8d78d
commit 99f8f8fedb
14 changed files with 183 additions and 101 deletions

View File

@ -1,15 +1,18 @@
import styled from '@emotion/styled';
import { useState } from 'react';
import { useRecoilValue } from 'recoil';
import { useIcons } from 'twenty-ui';
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { getOperandsForFilterType } from '../utils/getOperandsForFilterType';
import { ObjectFilterDropdownFilterSelectMenuItem } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem';
import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId';
import { useSelectFilter } from '@/object-record/object-filter-dropdown/hooks/useSelectFilter';
import { FiltersHotkeyScope } from '@/object-record/object-filter-dropdown/types/FiltersHotkeyScope';
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { isDefined } from 'twenty-ui';
export const StyledInput = styled.input`
background: transparent;
@ -39,20 +42,40 @@ export const StyledInput = styled.input`
export const ObjectFilterDropdownFilterSelect = () => {
const [searchText, setSearchText] = useState('');
const {
setFilterDefinitionUsedInDropdown,
setSelectedOperandInDropdown,
setObjectFilterDropdownSearchInput,
availableFilterDefinitionsState,
} = useFilterDropdown();
const { availableFilterDefinitionsState } = useFilterDropdown();
const availableFilterDefinitions = useRecoilValue(
availableFilterDefinitionsState,
);
const { getIcon } = useIcons();
const sortedAvailableFilterDefinitions = [...availableFilterDefinitions]
.sort((a, b) => a.label.localeCompare(b.label))
.filter((item) =>
item.label.toLocaleLowerCase().includes(searchText.toLocaleLowerCase()),
);
const setHotkeyScope = useSetHotkeyScope();
const selectableListItemIds = sortedAvailableFilterDefinitions.map(
(item) => item.fieldMetadataId,
);
const { selectFilter } = useSelectFilter();
const { resetSelectedItem } = useSelectableList(OBJECT_FILTER_DROPDOWN_ID);
const handleEnter = (itemId: string) => {
const selectedFilterDefinition = sortedAvailableFilterDefinitions.find(
(item) => item.fieldMetadataId === itemId,
);
if (!isDefined(selectedFilterDefinition)) {
return;
}
resetSelectedItem();
selectFilter({ filterDefinition: selectedFilterDefinition });
};
return (
<>
@ -64,39 +87,27 @@ export const ObjectFilterDropdownFilterSelect = () => {
setSearchText(event.target.value)
}
/>
<DropdownMenuItemsContainer>
{[...availableFilterDefinitions]
.sort((a, b) => a.label.localeCompare(b.label))
.filter((item) =>
item.label
.toLocaleLowerCase()
.includes(searchText.toLocaleLowerCase()),
)
.map((availableFilterDefinition, index) => (
<MenuItem
key={`select-filter-${index}`}
testId={`select-filter-${index}`}
onClick={() => {
setFilterDefinitionUsedInDropdown(availableFilterDefinition);
if (
availableFilterDefinition.type === 'RELATION' ||
availableFilterDefinition.type === 'SELECT'
) {
setHotkeyScope(RelationPickerHotkeyScope.RelationPicker);
}
setSelectedOperandInDropdown(
getOperandsForFilterType(availableFilterDefinition.type)?.[0],
);
setObjectFilterDropdownSearchInput('');
}}
LeftIcon={getIcon(availableFilterDefinition.iconName)}
text={availableFilterDefinition.label}
/>
))}
</DropdownMenuItemsContainer>
<SelectableList
hotkeyScope={FiltersHotkeyScope.ObjectFilterDropdownButton}
selectableItemIdArray={selectableListItemIds}
selectableListId={OBJECT_FILTER_DROPDOWN_ID}
onEnter={handleEnter}
>
<DropdownMenuItemsContainer>
{sortedAvailableFilterDefinitions.map(
(availableFilterDefinition, index) => (
<SelectableItem
itemId={availableFilterDefinition.fieldMetadataId}
>
<ObjectFilterDropdownFilterSelectMenuItem
key={`select-filter-${index}`}
filterDefinition={availableFilterDefinition}
/>
</SelectableItem>
),
)}
</DropdownMenuItemsContainer>
</SelectableList>
</>
);
};

View File

@ -0,0 +1,43 @@
import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId';
import { useSelectFilter } from '@/object-record/object-filter-dropdown/hooks/useSelectFilter';
import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { MenuItemSelect } from '@/ui/navigation/menu-item/components/MenuItemSelect';
import { useRecoilValue } from 'recoil';
import { useIcons } from 'twenty-ui';
export type ObjectFilterDropdownFilterSelectMenuItemProps = {
filterDefinition: FilterDefinition;
};
export const ObjectFilterDropdownFilterSelectMenuItem = ({
filterDefinition,
}: ObjectFilterDropdownFilterSelectMenuItemProps) => {
const { selectFilter } = useSelectFilter();
const { isSelectedItemIdSelector, resetSelectedItem } = useSelectableList(
OBJECT_FILTER_DROPDOWN_ID,
);
const isSelectedItem = useRecoilValue(
isSelectedItemIdSelector(filterDefinition.fieldMetadataId),
);
const { getIcon } = useIcons();
const handleClick = () => {
resetSelectedItem();
selectFilter({ filterDefinition });
};
return (
<MenuItemSelect
selected={false}
hovered={isSelectedItem}
onClick={handleClick}
LeftIcon={getIcon(filterDefinition.iconName)}
text={filterDefinition.label}
/>
);
};

View File

@ -41,7 +41,7 @@ export const ObjectFilterDropdownOptionSelect = () => {
selectableListScopeId: MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID,
});
const { handleResetSelectedPosition } = useSelectableList(
const { resetSelectedItem } = useSelectableList(
MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID,
);
@ -90,10 +90,10 @@ export const ObjectFilterDropdownOptionSelect = () => {
[Key.Escape],
() => {
closeDropdown();
handleResetSelectedPosition();
resetSelectedItem();
},
RelationPickerHotkeyScope.RelationPicker,
[closeDropdown, handleResetSelectedPosition],
[closeDropdown, resetSelectedItem],
);
const handleMultipleOptionSelectChange = (
@ -137,7 +137,7 @@ export const ObjectFilterDropdownOptionSelect = () => {
value: newFilterValue,
});
}
handleResetSelectedPosition();
resetSelectedItem();
};
const optionsInDropdown = selectableOptions?.filter((option) =>

View File

@ -0,0 +1,40 @@
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition';
import { getOperandsForFilterType } from '@/object-record/object-filter-dropdown/utils/getOperandsForFilterType';
import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
type SelectFilterParams = {
filterDefinition: FilterDefinition;
};
export const useSelectFilter = () => {
const {
setFilterDefinitionUsedInDropdown,
setSelectedOperandInDropdown,
setObjectFilterDropdownSearchInput,
} = useFilterDropdown();
const setHotkeyScope = useSetHotkeyScope();
const selectFilter = ({ filterDefinition }: SelectFilterParams) => {
setFilterDefinitionUsedInDropdown(filterDefinition);
if (
filterDefinition.type === 'RELATION' ||
filterDefinition.type === 'SELECT'
) {
setHotkeyScope(RelationPickerHotkeyScope.RelationPicker);
}
setSelectedOperandInDropdown(
getOperandsForFilterType(filterDefinition.type)?.[0],
);
setObjectFilterDropdownSearchInput('');
};
return {
selectFilter,
};
};

View File

@ -33,7 +33,7 @@ export const MultiSelectFieldInput = ({
const { selectedItemIdState } = useSelectableListStates({
selectableListScopeId: MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID,
});
const { handleResetSelectedPosition } = useSelectableList(
const { resetSelectedItem } = useSelectableList(
MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID,
);
const { persistField, fieldDefinition, fieldValues, hotkeyScope } =
@ -65,10 +65,10 @@ export const MultiSelectFieldInput = ({
Key.Escape,
() => {
onCancel?.();
handleResetSelectedPosition();
resetSelectedItem();
},
hotkeyScope,
[onCancel, handleResetSelectedPosition],
[onCancel, resetSelectedItem],
);
useListenClickOutside({
@ -83,7 +83,7 @@ export const MultiSelectFieldInput = ({
if (weAreNotInAnHTMLInput && isDefined(onCancel)) {
onCancel();
}
handleResetSelectedPosition();
resetSelectedItem();
},
});

View File

@ -27,7 +27,7 @@ export const SelectFieldInput = ({
const [filteredOptions, setFilteredOptions] = useState<SelectOption[]>([]);
const { handleResetSelectedPosition } = useSelectableList(
const { resetSelectedItem } = useSelectableList(
SINGLE_ENTITY_SELECT_BASE_LIST,
);
const clearField = useClearField();
@ -44,17 +44,17 @@ export const SelectFieldInput = ({
const handleSubmit = (option: SelectOption) => {
onSubmit?.(() => persistField(option?.value));
handleResetSelectedPosition();
resetSelectedItem();
};
useScopedHotkeys(
Key.Escape,
() => {
onCancel?.();
handleResetSelectedPosition();
resetSelectedItem();
},
hotkeyScope,
[onCancel, handleResetSelectedPosition],
[onCancel, resetSelectedItem],
);
const optionIds = [
@ -74,7 +74,7 @@ export const SelectFieldInput = ({
);
if (isDefined(option)) {
onSubmit?.(() => persistField(option.value));
handleResetSelectedPosition();
resetSelectedItem();
}
}}
>

View File

@ -48,7 +48,7 @@ export const MultiRecordSelect = ({
const { objectRecordsIdsMultiSelectState, recordMultiSelectIsLoadingState } =
useObjectRecordMultiSelectScopedStates(relationPickerScopedId);
const { handleResetSelectedPosition } = useSelectableList(
const { resetSelectedItem } = useSelectableList(
MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID,
);
const recordMultiSelectIsLoading = useRecoilValue(
@ -79,10 +79,10 @@ export const MultiRecordSelect = ({
() => {
onSubmit?.();
goBackToPreviousHotkeyScope();
handleResetSelectedPosition();
resetSelectedItem();
},
relationPickerScopedId,
[onSubmit, goBackToPreviousHotkeyScope, handleResetSelectedPosition],
[onSubmit, goBackToPreviousHotkeyScope, resetSelectedItem],
);
const debouncedOnCreate = useDebouncedCallback(
@ -123,7 +123,7 @@ export const MultiRecordSelect = ({
hotkeyScope={relationPickerScopedId}
onEnter={(selectedId) => {
onChange?.(selectedId);
handleResetSelectedPosition();
resetSelectedItem();
}}
>
{objectRecordsIdsMultiSelect?.map((recordId) => {
@ -133,7 +133,7 @@ export const MultiRecordSelect = ({
objectRecordId={recordId}
onChange={(recordId) => {
onChange?.(recordId);
handleResetSelectedPosition();
resetSelectedItem();
}}
/>
);

View File

@ -27,7 +27,6 @@ export const SelectableMenuItemSelect = ({
);
const isSelectedItemId = useRecoilValue(isSelectedItemIdSelector(entity.id));
return (
<StyledSelectableItem itemId={entity.id} key={entity.id}>
<MenuItemSelectAvatar

View File

@ -1,5 +1,5 @@
import { useRef } from 'react';
import { isNonEmptyString } from '@sniptt/guards';
import { useRef } from 'react';
import { useRecoilValue } from 'recoil';
import { Key } from 'ts-key-enum';
import { IconComponent, IconPlus } from 'twenty-ui';
@ -92,8 +92,9 @@ export const SingleEntitySelectMenuItems = ({
isDefined(entity) && isNonEmptyString(entity.name),
);
const { isSelectedItemIdSelector, handleResetSelectedPosition } =
useSelectableList(SINGLE_ENTITY_SELECT_BASE_LIST);
const { isSelectedItemIdSelector, resetSelectedItem } = useSelectableList(
SINGLE_ENTITY_SELECT_BASE_LIST,
);
const isSelectedAddNewButton = useRecoilValue(
isSelectedItemIdSelector('add-new'),
@ -110,11 +111,11 @@ export const SingleEntitySelectMenuItems = ({
useScopedHotkeys(
[Key.Escape],
() => {
handleResetSelectedPosition();
resetSelectedItem();
onCancel?.();
},
hotkeyScope,
[onCancel, handleResetSelectedPosition],
[onCancel, resetSelectedItem],
);
const selectableItemIds = entitiesInDropdown.map((entity) => entity.id);
@ -134,7 +135,7 @@ export const SingleEntitySelectMenuItems = ({
);
onEntitySelected(entitiesInDropdown[entityIndex]);
}
handleResetSelectedPosition();
resetSelectedItem();
}}
>
<DropdownMenuItemsContainer hasMaxHeight>

View File

@ -40,7 +40,7 @@ export const MultipleRecordSelectDropdown = ({
selectableListScopeId: selectableListId,
});
const { handleResetSelectedPosition } = useSelectableList(selectableListId);
const { resetSelectedItem } = useSelectableList(selectableListId);
const selectedItemId = useRecoilValue(selectedItemIdState);
@ -75,10 +75,10 @@ export const MultipleRecordSelectDropdown = ({
[Key.Escape],
() => {
closeDropdown();
handleResetSelectedPosition();
resetSelectedItem();
},
hotkeyScope,
[closeDropdown, handleResetSelectedPosition],
[closeDropdown, resetSelectedItem],
);
const showNoResult =
@ -105,7 +105,7 @@ export const MultipleRecordSelectDropdown = ({
recordsInDropdown[record],
!recordIsSelectedInDropwdown,
);
handleResetSelectedPosition();
resetSelectedItem();
}}
>
<DropdownMenuItemsContainer hasMaxHeight>
@ -116,7 +116,7 @@ export const MultipleRecordSelectDropdown = ({
selected={record.isSelected}
isKeySelected={record.id === selectedItemId}
onSelectChange={(newCheckedValue) => {
handleResetSelectedPosition();
resetSelectedItem();
handleRecordSelectChange(record, newCheckedValue);
}}
avatar={

View File

@ -10,9 +10,7 @@ import { MouseEvent, useRef } from 'react';
import { Keys } from 'react-hotkeys-hook';
import { Key } from 'ts-key-enum';
import { SINGLE_ENTITY_SELECT_BASE_LIST } from '@/object-record/relation-picker/constants/SingleEntitySelectBaseList';
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { HotkeyEffect } from '@/ui/utilities/hotkey/components/HotkeyEffect';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
@ -69,9 +67,6 @@ export const Dropdown = ({
const { isDropdownOpen, toggleDropdown, closeDropdown, dropdownWidth } =
useDropdown(dropdownId);
const { handleResetSelectedPosition } = useSelectableList(
SINGLE_ENTITY_SELECT_BASE_LIST,
);
const offsetMiddlewares = [];
if (isDefined(dropdownOffset.x)) {
@ -108,7 +103,6 @@ export const Dropdown = ({
if (isDropdownOpen) {
closeDropdown();
handleResetSelectedPosition();
}
},
});
@ -122,10 +116,9 @@ export const Dropdown = ({
[Key.Escape],
() => {
closeDropdown();
handleResetSelectedPosition();
},
dropdownHotkeyScope.scope,
[closeDropdown, handleResetSelectedPosition],
[closeDropdown],
);
return (

View File

@ -2,6 +2,12 @@ import { ReactNode, useEffect, useRef } from 'react';
import { useRecoilValue } from 'recoil';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import styled from '@emotion/styled';
const StyledContainer = styled.div`
height: 100%;
width: 100%;
`;
export type SelectableItemProps = {
itemId: string;
@ -27,8 +33,8 @@ export const SelectableItem = ({
}, [isSelectedItemId]);
return (
<div className={className} ref={scrollRef}>
<StyledContainer className={className} ref={scrollRef}>
{children}
</div>
</StyledContainer>
);
};

View File

@ -1,8 +1,4 @@
import {
useRecoilCallback,
useResetRecoilState,
useSetRecoilState,
} from 'recoil';
import { useRecoilCallback, useSetRecoilState } from 'recoil';
import { useSelectableListStates } from '@/ui/layout/selectable-list/hooks/internal/useSelectableListStates';
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
@ -24,16 +20,11 @@ export const useSelectableList = (selectableListId?: string) => {
selectableListOnEnterState,
);
const resetSelectedItemIdState = useResetRecoilState(selectedItemIdState);
const resetSelectedItem = () => {
resetSelectedItemIdState();
};
const handleResetSelectedPosition = useRecoilCallback(
const resetSelectedItem = useRecoilCallback(
({ snapshot, set }) =>
() => {
const selectedItemId = getSnapshotValue(snapshot, selectedItemIdState);
if (isDefined(selectedItemId)) {
set(selectedItemIdState, null);
set(isSelectedItemIdSelector(selectedItemId), false);
@ -44,11 +35,9 @@ export const useSelectableList = (selectableListId?: string) => {
return {
selectableListId: scopeId,
setSelectableItemIds,
isSelectedItemIdSelector,
setSelectableListOnEnter,
resetSelectedItem,
handleResetSelectedPosition,
};
};

View File

@ -8,12 +8,12 @@ export type MenuItemBaseProps = {
accent?: MenuItemAccent;
isKeySelected?: boolean;
isHoverBackgroundDisabled?: boolean;
hovered?: boolean;
};
export const StyledMenuItemBase = styled.div<MenuItemBaseProps>`
--horizontal-padding: ${({ theme }) => theme.spacing(1)};
--vertical-padding: ${({ theme }) => theme.spacing(2)};
align-items: center;
border-radius: ${({ theme }) => theme.border.radius.sm};