[DOP-3497]: Add arrow key functionality (#32)

This commit is contained in:
Brandon Ly 2023-04-05 09:03:26 -05:00 committed by GitHub
parent 1612c0ca89
commit 34a854b765
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 90 additions and 44 deletions

View File

@ -3,9 +3,9 @@ import { StyledLi, StyledOptionText, StyledPlaceholder } from './styled.elements
import Checkmark from './CheckmarkSvg'; import Checkmark from './CheckmarkSvg';
import { OptionProps } from './types'; import { OptionProps } from './types';
export const Option = ({ option, value, selected, onClick }: OptionProps) => { export const Option = ({ option, value, selected, onClick, focused }: OptionProps) => {
const KEY_ENTER = 'ENTER'; const KEY_ENTER = 'Enter';
const KEY_SPACE = 'SPACE'; const KEY_SPACE = ' ';
const handleKeyPress = (event: React.KeyboardEvent) => { const handleKeyPress = (event: React.KeyboardEvent) => {
if (event.key === KEY_ENTER || event.key === KEY_SPACE) { if (event.key === KEY_ENTER || event.key === KEY_SPACE) {
@ -14,7 +14,12 @@ export const Option = ({ option, value, selected, onClick }: OptionProps) => {
}; };
return ( return (
<StyledLi onClick={() => onClick(value)} onKeyPress={handleKeyPress} selected={selected}> <StyledLi
onClick={() => onClick(value)}
onKeyPress={handleKeyPress}
selected={selected}
focused={focused}
>
{selected ? <Checkmark /> : <StyledPlaceholder />} {selected ? <Checkmark /> : <StyledPlaceholder />}
<StyledOptionText>{option}</StyledOptionText> <StyledOptionText>{option}</StyledOptionText>
</StyledLi> </StyledLi>

View File

@ -21,16 +21,32 @@ import { useOutsideClick } from './use-outside-click';
const VersionSelectorComponent = ({ const VersionSelectorComponent = ({
resourceVersions, resourceVersions,
active, active,
rootUrl,
description, description,
rootUrl,
}: VersionSelectorProps): JSX.Element => { }: VersionSelectorProps): JSX.Element => {
const [selectedIdx, setSelectedIdx] = React.useState( const initialSelectedIdx = resourceVersions.indexOf(active.resourceVersion);
resourceVersions.indexOf(active.resourceVersion),
);
const [open, setOpen] = React.useState<boolean>(false); const [open, setOpen] = React.useState<boolean>(false);
const [focusedIdx, setFocusedIdx] = React.useState<number>(-1);
const [selectedIdx, setSelectedIdx] = React.useState<number>(initialSelectedIdx);
const menuListRef = React.useRef(null); const menuListRef = React.useRef(null);
const options = resourceVersions.map((option, i) => {
return (
<Option
key={`option-${i}`}
selected={i === selectedIdx}
value={option}
option={`${option}${i === resourceVersions.length - 1 ? ' (latest)' : ''}`}
onClick={option => handleClick(i, option)}
focused={i === focusedIdx}
/>
);
});
useOutsideClick(menuListRef, () => { useOutsideClick(menuListRef, () => {
if (open) setOpen(false); if (open) setOpen(false);
setFocusedIdx(0);
}); });
const handleClick = (idx: number, resourceVersion: string) => { const handleClick = (idx: number, resourceVersion: string) => {
@ -47,8 +63,43 @@ const VersionSelectorComponent = ({
return setOpen(false); return setOpen(false);
}; };
const handleFocusChange = (event: React.KeyboardEvent<HTMLDivElement>) => {
const { key, shiftKey } = event;
if (key === 'ArrowDown' || (key === 'Tab' && !shiftKey)) {
// if we go down when we are already past the end, don't do anything
if (focusedIdx === resourceVersions.length) return;
if (focusedIdx === -1) {
// when first entering the dropdown via the down arrow key or tab,
// we want to open the modal
setOpen(true);
} else if (focusedIdx === resourceVersions.length - 1) {
// if we are at the last element of the dropdown, and attempt to go
// down again, we want to close the dropdown
setOpen(false);
}
setFocusedIdx(focusedIdx + 1);
} else if (key === 'ArrowUp' || (key === 'Tab' && shiftKey)) {
// if we go down when we are already past the end, don't do anything
if (focusedIdx === -1) return;
if (focusedIdx === resourceVersions.length) {
// in this scenario, we are entering the dropdown from below
// we open the dropdown and start from the bottom
setOpen(true);
} else if (focusedIdx === 0) {
// if we reach the first element in the drop down, and we attempt to go up again,
// we want to close the dropdown
setOpen(false);
}
setFocusedIdx(focusedIdx - 1);
}
};
return ( return (
<StyledWrapper ref={menuListRef}> <StyledWrapper onKeyDown={handleFocusChange} ref={menuListRef}>
<StyledSelectWrapper> <StyledSelectWrapper>
<StyledLabel>Resource Version:</StyledLabel> <StyledLabel>Resource Version:</StyledLabel>
{description && <StyledDescription>{description}</StyledDescription>} {description && <StyledDescription>{description}</StyledDescription>}
@ -62,17 +113,7 @@ const VersionSelectorComponent = ({
<StyledDropdown open={open}> <StyledDropdown open={open}>
<div> <div>
<StyledMenuList> <StyledMenuList>{options}</StyledMenuList>
{resourceVersions.map((option, i) => (
<Option
key={`option-${i}`}
selected={i === selectedIdx}
value={option}
option={`${option}${i === resourceVersions.length - 1 ? ' (latest)' : ''}`}
onClick={option => handleClick(i, option)}
/>
))}
</StyledMenuList>
</div> </div>
</StyledDropdown> </StyledDropdown>
</StyledWrapper> </StyledWrapper>

View File

@ -121,26 +121,17 @@ export const enabledOptionStyle = css`
&:hover { &:hover {
background-color: ${palette.gray.light2}; background-color: ${palette.gray.light2};
} }
&:focus-visible {
color: ${palette.blue.dark2};
background-color: ${palette.blue.light3};
&:before {
opacity: 1;
transform: scaleY(1);
background-color: ${palette.blue.base};
}
}
`; `;
export const StyledLi = styled.li.attrs<{ selected: boolean; disabled?: boolean }>( export const StyledLi = styled.li.attrs<{
({ selected }) => ({ selected: boolean;
role: 'option', disabled?: boolean;
'aria-selected': selected, focused?: boolean;
tabIndex: '0', }>(({ selected }) => ({
}), role: 'option',
)<{ selected: boolean; disabled?: boolean }>` 'aria-selected': selected,
tabIndex: '0',
}))<{ selected: boolean; disabled?: boolean; focused?: boolean }>`
display: flex; display: flex;
width: 100%; width: 100%;
outline: none; outline: none;
@ -150,9 +141,11 @@ export const StyledLi = styled.li.attrs<{ selected: boolean; disabled?: boolean
padding: 8px 12px; padding: 8px 12px;
cursor: pointer; cursor: pointer;
color: ${palette.gray.dark3}; color: ${palette.gray.dark3};
${props =>
props.focused &&
`color: ${palette.blue.dark2};
background-color: ${palette.blue.light3};`}
font-weight: ${props => (props.selected ? `bold` : `normal`)}; font-weight: ${props => (props.selected ? `bold` : `normal`)};
&:before { &:before {
content: ''; content: '';
position: absolute; position: absolute;
@ -165,8 +158,14 @@ export const StyledLi = styled.li.attrs<{ selected: boolean; disabled?: boolean
border-radius: 0px 4px 4px 0px; border-radius: 0px 4px 4px 0px;
opacity: 0; opacity: 0;
transition: all ${transitionDuration.default}ms ease-in-out; transition: all ${transitionDuration.default}ms ease-in-out;
${props =>
props.focused &&
`
opacity: 1;
transform: scaleY(1);
background-color: ${palette.blue.base};
`}
} }
${props => (props.disabled ? disabledOptionStyle : enabledOptionStyle)} ${props => (props.disabled ? disabledOptionStyle : enabledOptionStyle)}
`; `;

View File

@ -10,6 +10,7 @@ export interface VersionSelectorProps {
} }
export interface OptionProps { export interface OptionProps {
focused: boolean;
option: string; option: string;
value: string; value: string;
selected: boolean; selected: boolean;

View File

@ -54,7 +54,7 @@ exports[`VersionSelector should correctly render VersionSelector 1`] = `
> >
<li <li
aria-selected="false" aria-selected="false"
class="sc-pNWdM glxGCd" class="sc-pNWdM kSTcTW"
role="option" role="option"
tabindex="0" tabindex="0"
> >
@ -69,7 +69,7 @@ exports[`VersionSelector should correctly render VersionSelector 1`] = `
</li> </li>
<li <li
aria-selected="false" aria-selected="false"
class="sc-pNWdM glxGCd" class="sc-pNWdM kSTcTW"
role="option" role="option"
tabindex="0" tabindex="0"
> >
@ -84,7 +84,7 @@ exports[`VersionSelector should correctly render VersionSelector 1`] = `
</li> </li>
<li <li
aria-selected="true" aria-selected="true"
class="sc-pNWdM jlRKTH" class="sc-pNWdM derVNM"
role="option" role="option"
selected="" selected=""
tabindex="0" tabindex="0"