From e380d28924c51fb1ff428f1ecae26d16479ab0f1 Mon Sep 17 00:00:00 2001 From: Mateusz Kwasniewski Date: Mon, 18 Dec 2023 14:13:32 +0100 Subject: [PATCH] feat: up and down arrow navigation for filter items (#5673) --- .../filter/FilterItem/FilterItem.test.tsx | 30 ++++++++++- .../filter/FilterItem/FilterItem.tsx | 52 +++++++++++++++++-- 2 files changed, 76 insertions(+), 6 deletions(-) diff --git a/frontend/src/component/filter/FilterItem/FilterItem.test.tsx b/frontend/src/component/filter/FilterItem/FilterItem.test.tsx index d9ddaa2c62..bcb4849784 100644 --- a/frontend/src/component/filter/FilterItem/FilterItem.test.tsx +++ b/frontend/src/component/filter/FilterItem/FilterItem.test.tsx @@ -1,4 +1,4 @@ -import { screen } from '@testing-library/react'; +import { screen, fireEvent } from '@testing-library/react'; import { render } from 'utils/testRenderer'; import { FilterItem, FilterItemParams, IFilterItemProps } from './FilterItem'; @@ -163,4 +163,32 @@ describe('FilterItem Component', () => { }, ]); }); + + it('navigates between items with arrow keys', async () => { + setup(null); + + const searchInput = await screen.findByPlaceholderText('Search'); + fireEvent.keyDown(searchInput, { key: 'ArrowDown' }); + + const firstOption = screen.getByText('Option 1').closest('li')!; + expect(document.activeElement).toBe(firstOption); + + fireEvent.keyDown(firstOption, { key: 'ArrowUp' }); + expect(document.activeElement).toBe(searchInput); + }); + + it('selects an item with the Enter key', async () => { + const recordedChanges = setup(null); + + const searchInput = await screen.findByPlaceholderText('Search'); + fireEvent.keyDown(searchInput, { key: 'ArrowDown' }); + + const firstOption = screen.getByText('Option 1').closest('li')!; + fireEvent.keyDown(firstOption, { key: 'Enter' }); + + expect(recordedChanges).toContainEqual({ + operator: 'IS', + values: ['1'], + }); + }); }); diff --git a/frontend/src/component/filter/FilterItem/FilterItem.tsx b/frontend/src/component/filter/FilterItem/FilterItem.tsx index b98655e07c..639d6526b6 100644 --- a/frontend/src/component/filter/FilterItem/FilterItem.tsx +++ b/frontend/src/component/filter/FilterItem/FilterItem.tsx @@ -9,7 +9,6 @@ import { StyledTextField, } from './FilterItem.styles'; import { FilterItemChip } from './FilterItemChip/FilterItemChip'; -import { onEnter } from '../../common/Search/SearchSuggestions/onEnter'; export interface IFilterItemProps { name: string; @@ -27,6 +26,37 @@ export type FilterItemParams = { values: string[]; }; +interface UseSelectionManagementProps { + options: Array<{ label: string; value: string }>; + handleToggle: (value: string) => () => void; +} + +const useSelectionManagement = ({ + options, + handleToggle, +}: UseSelectionManagementProps) => { + const listRefs = useRef>([]); + + const handleSelection = (event: React.KeyboardEvent, index: number) => { + // we have to be careful not to prevent other keys e.g tab + if (event.key === 'ArrowDown' && index < listRefs.current.length - 1) { + event.preventDefault(); + listRefs.current[index + 1]?.focus(); + } else if (event.key === 'ArrowUp' && index > 0) { + event.preventDefault(); + listRefs.current[index - 1]?.focus(); + } else if (event.key === 'Enter') { + event.preventDefault(); + if (index > 0) { + const listItemIndex = index - 1; + handleToggle(options[listItemIndex].value)(); + } + } + }; + + return { listRefs, handleSelection }; +}; + export const FilterItem: FC = ({ name, label, @@ -92,6 +122,11 @@ export const FilterItem: FC = ({ } }; + const { listRefs, handleSelection } = useSelectionManagement({ + options, + handleToggle, + }); + useEffect(() => { if (state && !currentOperators.includes(state.operator)) { onChange({ @@ -144,6 +179,10 @@ export const FilterItem: FC = ({ ), }} + inputRef={(el) => { + listRefs.current[0] = el; + }} + onKeyDown={(event) => handleSelection(event, 0)} /> {options @@ -152,7 +191,7 @@ export const FilterItem: FC = ({ .toLowerCase() .includes(searchText.toLowerCase()), ) - .map((option) => { + .map((option, index) => { const labelId = `checkbox-list-label-${option.value}`; return ( @@ -161,10 +200,13 @@ export const FilterItem: FC = ({ dense disablePadding tabIndex={0} - onKeyDown={onEnter( - handleToggle(option.value), - )} onClick={handleToggle(option.value)} + ref={(el) => { + listRefs.current[index + 1] = el; + }} + onKeyDown={(event) => + handleSelection(event, index + 1) + } >