mirror of
https://github.com/Redocly/redoc.git
synced 2025-07-31 10:29:47 +03:00
[DOP-3497]: Add arrow key functionality (#32)
This commit is contained in:
parent
1612c0ca89
commit
34a854b765
|
@ -3,9 +3,9 @@ import { StyledLi, StyledOptionText, StyledPlaceholder } from './styled.elements
|
|||
import Checkmark from './CheckmarkSvg';
|
||||
import { OptionProps } from './types';
|
||||
|
||||
export const Option = ({ option, value, selected, onClick }: OptionProps) => {
|
||||
const KEY_ENTER = 'ENTER';
|
||||
const KEY_SPACE = 'SPACE';
|
||||
export const Option = ({ option, value, selected, onClick, focused }: OptionProps) => {
|
||||
const KEY_ENTER = 'Enter';
|
||||
const KEY_SPACE = ' ';
|
||||
|
||||
const handleKeyPress = (event: React.KeyboardEvent) => {
|
||||
if (event.key === KEY_ENTER || event.key === KEY_SPACE) {
|
||||
|
@ -14,7 +14,12 @@ export const Option = ({ option, value, selected, onClick }: OptionProps) => {
|
|||
};
|
||||
|
||||
return (
|
||||
<StyledLi onClick={() => onClick(value)} onKeyPress={handleKeyPress} selected={selected}>
|
||||
<StyledLi
|
||||
onClick={() => onClick(value)}
|
||||
onKeyPress={handleKeyPress}
|
||||
selected={selected}
|
||||
focused={focused}
|
||||
>
|
||||
{selected ? <Checkmark /> : <StyledPlaceholder />}
|
||||
<StyledOptionText>{option}</StyledOptionText>
|
||||
</StyledLi>
|
||||
|
|
|
@ -21,16 +21,32 @@ import { useOutsideClick } from './use-outside-click';
|
|||
const VersionSelectorComponent = ({
|
||||
resourceVersions,
|
||||
active,
|
||||
rootUrl,
|
||||
description,
|
||||
rootUrl,
|
||||
}: VersionSelectorProps): JSX.Element => {
|
||||
const [selectedIdx, setSelectedIdx] = React.useState(
|
||||
resourceVersions.indexOf(active.resourceVersion),
|
||||
);
|
||||
const initialSelectedIdx = resourceVersions.indexOf(active.resourceVersion);
|
||||
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 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, () => {
|
||||
if (open) setOpen(false);
|
||||
setFocusedIdx(0);
|
||||
});
|
||||
|
||||
const handleClick = (idx: number, resourceVersion: string) => {
|
||||
|
@ -47,8 +63,43 @@ const VersionSelectorComponent = ({
|
|||
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 (
|
||||
<StyledWrapper ref={menuListRef}>
|
||||
<StyledWrapper onKeyDown={handleFocusChange} ref={menuListRef}>
|
||||
<StyledSelectWrapper>
|
||||
<StyledLabel>Resource Version:</StyledLabel>
|
||||
{description && <StyledDescription>{description}</StyledDescription>}
|
||||
|
@ -62,17 +113,7 @@ const VersionSelectorComponent = ({
|
|||
|
||||
<StyledDropdown open={open}>
|
||||
<div>
|
||||
<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>
|
||||
<StyledMenuList>{options}</StyledMenuList>
|
||||
</div>
|
||||
</StyledDropdown>
|
||||
</StyledWrapper>
|
||||
|
|
|
@ -121,26 +121,17 @@ export const enabledOptionStyle = css`
|
|||
&:hover {
|
||||
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 }>(
|
||||
({ selected }) => ({
|
||||
role: 'option',
|
||||
'aria-selected': selected,
|
||||
tabIndex: '0',
|
||||
}),
|
||||
)<{ selected: boolean; disabled?: boolean }>`
|
||||
export const StyledLi = styled.li.attrs<{
|
||||
selected: boolean;
|
||||
disabled?: boolean;
|
||||
focused?: boolean;
|
||||
}>(({ selected }) => ({
|
||||
role: 'option',
|
||||
'aria-selected': selected,
|
||||
tabIndex: '0',
|
||||
}))<{ selected: boolean; disabled?: boolean; focused?: boolean }>`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
|
@ -150,9 +141,11 @@ export const StyledLi = styled.li.attrs<{ selected: boolean; disabled?: boolean
|
|||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
color: ${palette.gray.dark3};
|
||||
|
||||
${props =>
|
||||
props.focused &&
|
||||
`color: ${palette.blue.dark2};
|
||||
background-color: ${palette.blue.light3};`}
|
||||
font-weight: ${props => (props.selected ? `bold` : `normal`)};
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
|
@ -165,8 +158,14 @@ export const StyledLi = styled.li.attrs<{ selected: boolean; disabled?: boolean
|
|||
border-radius: 0px 4px 4px 0px;
|
||||
opacity: 0;
|
||||
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)}
|
||||
`;
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ export interface VersionSelectorProps {
|
|||
}
|
||||
|
||||
export interface OptionProps {
|
||||
focused: boolean;
|
||||
option: string;
|
||||
value: string;
|
||||
selected: boolean;
|
||||
|
|
|
@ -54,7 +54,7 @@ exports[`VersionSelector should correctly render VersionSelector 1`] = `
|
|||
>
|
||||
<li
|
||||
aria-selected="false"
|
||||
class="sc-pNWdM glxGCd"
|
||||
class="sc-pNWdM kSTcTW"
|
||||
role="option"
|
||||
tabindex="0"
|
||||
>
|
||||
|
@ -69,7 +69,7 @@ exports[`VersionSelector should correctly render VersionSelector 1`] = `
|
|||
</li>
|
||||
<li
|
||||
aria-selected="false"
|
||||
class="sc-pNWdM glxGCd"
|
||||
class="sc-pNWdM kSTcTW"
|
||||
role="option"
|
||||
tabindex="0"
|
||||
>
|
||||
|
@ -84,7 +84,7 @@ exports[`VersionSelector should correctly render VersionSelector 1`] = `
|
|||
</li>
|
||||
<li
|
||||
aria-selected="true"
|
||||
class="sc-pNWdM jlRKTH"
|
||||
class="sc-pNWdM derVNM"
|
||||
role="option"
|
||||
selected=""
|
||||
tabindex="0"
|
||||
|
|
Loading…
Reference in New Issue
Block a user