Add arrow up/down+enter navigation to select relation (#275)

* Add arrow up/down+enter navigation to select relation
This commit is contained in:
Félix Malfait 2023-06-12 17:52:16 +02:00 committed by GitHub
parent be863a22c9
commit 3341539eb2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 133 additions and 1 deletions

View File

@ -1,4 +1,5 @@
import { ChangeEvent, ComponentType, useEffect, useState } from 'react'; import { ChangeEvent, ComponentType, useEffect, useState } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { FaPlus } from 'react-icons/fa'; import { FaPlus } from 'react-icons/fa';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useRecoilState } from 'recoil'; import { useRecoilState } from 'recoil';
@ -58,12 +59,21 @@ const StyledEditModeResults = styled.div`
padding-right: ${(props) => props.theme.spacing(1)}; padding-right: ${(props) => props.theme.spacing(1)};
`; `;
const StyledEditModeResultItem = styled.div` type StyledEditModeResultItemProps = {
isSelected: boolean;
};
const StyledEditModeResultItem = styled.div<StyledEditModeResultItemProps>`
height: 32px; height: 32px;
display: flex; display: flex;
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
${(props) =>
props.isSelected &&
`
background-color: ${props.theme.tertiaryBackground};
`}
`; `;
const StyledCreateButtonIcon = styled.div` const StyledCreateButtonIcon = styled.div`
@ -139,6 +149,72 @@ export function EditableRelation<
setIsSomeInputInEditMode(false); setIsSomeInputInEditMode(false);
} }
const [selectedIndex, setSelectedIndex] = useState(0);
useHotkeys(
'down',
() => {
setSelectedIndex((prevSelectedIndex) =>
Math.min(
prevSelectedIndex + 1,
(filterSearchResults.results?.length ?? 0) - 1,
),
);
},
{
enableOnContentEditable: true,
enableOnFormTags: true,
preventDefault: true,
},
[setSelectedIndex, filterSearchResults.results],
);
useHotkeys(
'up',
() => {
setSelectedIndex((prevSelectedIndex) =>
Math.max(prevSelectedIndex - 1, 0),
);
},
{
enableOnContentEditable: true,
enableOnFormTags: true,
preventDefault: true,
},
[setSelectedIndex],
);
useHotkeys(
'enter',
() => {
if (isEditMode) {
if (
filterSearchResults.results &&
selectedIndex < filterSearchResults.results.length
) {
const selectedResult = filterSearchResults.results[selectedIndex];
onChange(selectedResult.value);
closeEditMode();
} else if (canCreate && isNonEmptyString(searchInput)) {
onCreate(searchInput);
closeEditMode();
}
}
},
{
enableOnContentEditable: true,
enableOnFormTags: true,
},
[
filterSearchResults.results,
selectedIndex,
onChange,
closeEditMode,
canCreate,
searchInput,
onCreate,
],
);
return ( return (
<> <>
<EditableCell <EditableCell
@ -188,6 +264,7 @@ export function EditableRelation<
filterSearchResults.results.map((result, index) => ( filterSearchResults.results.map((result, index) => (
<StyledEditModeResultItem <StyledEditModeResultItem
key={index} key={index}
isSelected={index === selectedIndex}
onClick={() => { onClick={() => {
onChange(result.value); onChange(result.value);
closeEditMode(); closeEditMode();

View File

@ -151,3 +151,58 @@ export const EditRelation: Story = {
], ],
}, },
}; };
export const SelectRelationWithKeys: Story = {
render,
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const thirdRowCompanyCell = await canvas.findByText(
mockedPeopleData[2].company.name,
);
await userEvent.click(thirdRowCompanyCell);
const relationInput = await canvas.findByPlaceholderText('Company');
await userEvent.type(relationInput, 'Air', {
delay: 200,
});
await userEvent.type(relationInput, '{arrowdown}');
await userEvent.type(relationInput, '{arrowdown}');
await userEvent.type(relationInput, '{arrowup}');
await userEvent.type(relationInput, '{arrowdown}');
await userEvent.type(relationInput, '{enter}');
const newThirdRowCompanyCell = await canvas.findByText('Aircall');
await userEvent.click(newThirdRowCompanyCell);
},
parameters: {
actions: {},
msw: [
...graphqlMocks.filter((graphqlMock) => {
return graphqlMock.info.operationName !== 'UpdatePeople';
}),
...[
graphql.mutation('UpdatePeople', (req, res, ctx) => {
return res(
ctx.data({
updateOnePerson: {
...fetchOneFromData(mockedPeopleData, req.variables.id),
...{
company: {
id: req.variables.companyId,
name: 'Aircall',
domainName: 'aircall.io',
__typename: 'Company',
} satisfies GraphqlQueryCompany,
},
},
}),
);
}),
],
],
},
};