mirror of
https://github.com/Redocly/redoc.git
synced 2025-07-31 02:19:47 +03:00
Dop 3482 v selector (#8)
This commit is contained in:
parent
50e29d69bb
commit
2d61688fcc
17
src/components/VersionSelector/ArrowSvg.tsx
Normal file
17
src/components/VersionSelector/ArrowSvg.tsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
import * as React from 'react';
|
||||
|
||||
export const ArrowSvg = (): JSX.Element => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<polyline points="6 9 12 15 18 9" />
|
||||
</svg>
|
||||
);
|
24
src/components/VersionSelector/CheckmarkSvg.tsx
Normal file
24
src/components/VersionSelector/CheckmarkSvg.tsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
import * as React from 'react';
|
||||
import { palette } from '@leafygreen-ui/palette';
|
||||
import styled from '../../styled-components';
|
||||
|
||||
const StyledSvg = styled.svg`
|
||||
color: ${palette.blue.base};
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
`;
|
||||
|
||||
const Checkmark = () => {
|
||||
return (
|
||||
<StyledSvg height={16} width={16} role="img" aria-label="Checkmark Icon" viewBox="0 0 16 16">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M6.30583 9.05037L11.7611 3.59509C12.1516 3.20457 12.7848 3.20457 13.1753 3.59509L13.8824 4.3022C14.273 4.69273 14.273 5.32589 13.8824 5.71642L6.81525 12.7836C6.38819 13.2106 5.68292 13.1646 5.31505 12.6856L2.26638 8.71605C1.92998 8.27804 2.01235 7.65025 2.45036 7.31385L3.04518 6.85702C3.59269 6.43652 4.37742 6.53949 4.79792 7.087L6.30583 9.05037Z"
|
||||
fill={'currentColor'}
|
||||
/>
|
||||
</StyledSvg>
|
||||
);
|
||||
};
|
||||
|
||||
export default Checkmark;
|
22
src/components/VersionSelector/Option.tsx
Normal file
22
src/components/VersionSelector/Option.tsx
Normal file
|
@ -0,0 +1,22 @@
|
|||
import * as React from 'react';
|
||||
import { StyledLi, StyledOptionText, StyledPlaceholder } from './styled.elements';
|
||||
import Checkmark from './CheckmarkSvg';
|
||||
import { OptionProps } from './types';
|
||||
|
||||
export const Option = ({ option, selected, onClick }: OptionProps) => {
|
||||
const KEY_ENTER = 'ENTER';
|
||||
const KEY_SPACE = 'SPACE';
|
||||
|
||||
const handleKeyPress = (event: React.KeyboardEvent) => {
|
||||
if (event.key === KEY_ENTER || event.key === KEY_SPACE) {
|
||||
onClick();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledLi onClick={onClick} onKeyPress={handleKeyPress} selected={selected}>
|
||||
{selected ? <Checkmark /> : <StyledPlaceholder />}
|
||||
<StyledOptionText>{option}</StyledOptionText>
|
||||
</StyledLi>
|
||||
);
|
||||
};
|
71
src/components/VersionSelector/VersionSelector.tsx
Normal file
71
src/components/VersionSelector/VersionSelector.tsx
Normal file
|
@ -0,0 +1,71 @@
|
|||
import * as React from 'react';
|
||||
import {
|
||||
ArrowIcon,
|
||||
StyledWrapper,
|
||||
StyledSelectWrapper,
|
||||
StyledButton,
|
||||
StyledLabel,
|
||||
StyledDescription,
|
||||
StyledMenuList,
|
||||
StyledDisplay,
|
||||
StyledDropdown,
|
||||
} from './styled.elements';
|
||||
import { Option } from './Option';
|
||||
import { VersionSelectorProps } from './types';
|
||||
import { useOutsideClick } from './use-outside-click';
|
||||
|
||||
/**
|
||||
* Version Selector Dropdown component based structurally and stylistically off LG Select
|
||||
*/
|
||||
const VersionSelectorComponent = ({
|
||||
resourceVersions,
|
||||
active,
|
||||
description,
|
||||
}: VersionSelectorProps): JSX.Element => {
|
||||
const initialSelectedIdx = resourceVersions.indexOf(active.resourceVersion);
|
||||
const [open, setOpen] = React.useState<boolean>(false);
|
||||
const [selectedIdx, setSelectedIdx] = React.useState<number>(initialSelectedIdx);
|
||||
const menuListRef = React.useRef(null);
|
||||
useOutsideClick(menuListRef, () => {
|
||||
if (open) setOpen(false);
|
||||
});
|
||||
|
||||
const handleClick = (idx: number) => {
|
||||
setSelectedIdx(idx);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper ref={menuListRef}>
|
||||
<StyledSelectWrapper>
|
||||
<StyledLabel>Version Selector: v{active.apiVersion}</StyledLabel>
|
||||
{description && <StyledDescription>{description}</StyledDescription>}
|
||||
<StyledButton onClick={() => setOpen(!open)}>
|
||||
<StyledDisplay>
|
||||
<div>
|
||||
<div>{resourceVersions[selectedIdx]}</div>
|
||||
</div>
|
||||
<ArrowIcon open={open} />
|
||||
</StyledDisplay>
|
||||
</StyledButton>
|
||||
</StyledSelectWrapper>
|
||||
|
||||
<StyledDropdown open={open}>
|
||||
<div>
|
||||
<StyledMenuList>
|
||||
{resourceVersions.map((option, i) => (
|
||||
<Option
|
||||
key={`option-${i}`}
|
||||
selected={i === selectedIdx}
|
||||
option={option}
|
||||
onClick={() => handleClick(i)}
|
||||
/>
|
||||
))}
|
||||
</StyledMenuList>
|
||||
</div>
|
||||
</StyledDropdown>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export const VersionSelector = React.memo<VersionSelectorProps>(VersionSelectorComponent);
|
1
src/components/VersionSelector/index.ts
Normal file
1
src/components/VersionSelector/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './VersionSelector';
|
196
src/components/VersionSelector/styled.elements.ts
Normal file
196
src/components/VersionSelector/styled.elements.ts
Normal file
|
@ -0,0 +1,196 @@
|
|||
import { palette } from '@leafygreen-ui/palette';
|
||||
import { transparentize } from 'polished';
|
||||
import styled, { css } from '../../styled-components';
|
||||
import { ArrowSvg } from './ArrowSvg';
|
||||
import { ArrowIconProps } from './types';
|
||||
|
||||
const transitionDuration = {
|
||||
faster: 100,
|
||||
default: 150,
|
||||
slower: 300,
|
||||
} as const;
|
||||
|
||||
export const ArrowIcon = styled(ArrowSvg)`
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
top: 50%;
|
||||
-webkit-transform: ${(props: ArrowIconProps) =>
|
||||
props.open ? `translateY(-50%) rotate(180deg)` : `translateY(-50%)`};
|
||||
-ms-transform: ${(props: ArrowIconProps) =>
|
||||
props.open ? `translateY(-50%) rotate(180deg)` : `translateY(-50%)`};
|
||||
transform: ${(props: ArrowIconProps) =>
|
||||
props.open ? `translateY(-50%) rotate(180deg)` : `translateY(-50%)`};
|
||||
right: 8px;
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
export const StyledWrapper = styled.div`
|
||||
font-family: 'Euclid Circular A', Akzidenz, 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
font-size: 13px;
|
||||
|
||||
width: 90%;
|
||||
margin: 8px 5% 8px 5%;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
export const StyledSelectWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
> label + button,
|
||||
> p + button {
|
||||
margin-top: 3px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const StyledButton = styled.button.attrs({
|
||||
'aria-labelledby': 'View a different version of documentation.',
|
||||
})`
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
margin: 8px 0;
|
||||
|
||||
background-color: white;
|
||||
color: ${palette.black};
|
||||
border: 1px solid transparent;
|
||||
border-radius: 6px;
|
||||
border-color: ${palette.gray.base};
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
color: ${palette.black};
|
||||
background-color: ${palette.white};
|
||||
box-shadow: 0 0 0 3px ${palette.gray.light2};
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
box-shadow: 0 0 0 3px ${palette.blue.light1};
|
||||
border-color: rgba(255, 255, 255, 0);
|
||||
}
|
||||
`;
|
||||
|
||||
export const StyledLabel = styled.label`
|
||||
pointer-events: none;
|
||||
line-height: 20px;
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
`;
|
||||
|
||||
export const StyledDescription = styled.p`
|
||||
margin: 0;
|
||||
`;
|
||||
|
||||
export const StyledMenuList = styled.ul`
|
||||
position: relative;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
border-radius: 12px;
|
||||
line-height: 16px;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 8px 0px;
|
||||
overflow: auto;
|
||||
box-shadow: 0 4px 7px 0 ${transparentize(0.75, palette.black)};
|
||||
border: ${palette.gray.light2};
|
||||
`;
|
||||
|
||||
export const StyledDisplay = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 16px;
|
||||
gap: 6px;
|
||||
padding: 0 4px 0 12px;
|
||||
`;
|
||||
|
||||
export const disabledOptionStyle = css`
|
||||
cursor: not-allowed;
|
||||
color: ${palette.gray.base};
|
||||
`;
|
||||
|
||||
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 }>`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
overflow-wrap: anywhere;
|
||||
transition: background-color ${transitionDuration.default}ms ease-in-out;
|
||||
position: relative;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
color: ${palette.gray.dark3};
|
||||
|
||||
font-weight: ${props => (props.selected ? `bold` : `normal`)};
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
transform: scaleY(0.3);
|
||||
top: 7px;
|
||||
bottom: 7px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 4px;
|
||||
border-radius: 0px 4px 4px 0px;
|
||||
opacity: 0;
|
||||
transition: all ${transitionDuration.default}ms ease-in-out;
|
||||
}
|
||||
|
||||
${props => (props.disabled ? disabledOptionStyle : enabledOptionStyle)}
|
||||
`;
|
||||
|
||||
export const StyledOptionText = styled.span`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
export const openDropdownStyle = css`
|
||||
position: absolute;
|
||||
top: 70px;
|
||||
left: 1px;
|
||||
display: block;
|
||||
width: 100%;
|
||||
pointer-events: initial;
|
||||
z-index: 2;
|
||||
background-color: ${palette.white};
|
||||
`;
|
||||
|
||||
export const StyledDropdown = styled.div.attrs<{ open: boolean }>({
|
||||
role: 'listbox',
|
||||
'aria-labelledby': 'View a different version of documentation.',
|
||||
tabIndex: '-1',
|
||||
})<{ open: boolean }>`
|
||||
${props => (props.open ? openDropdownStyle : `display: none;`)}
|
||||
`;
|
||||
|
||||
export const StyledPlaceholder = styled.span`
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 6px;
|
||||
`;
|
20
src/components/VersionSelector/types.ts
Normal file
20
src/components/VersionSelector/types.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
export interface ActiveVersionData {
|
||||
resourceVersion: string;
|
||||
apiVersion: string;
|
||||
}
|
||||
export interface VersionSelectorProps {
|
||||
resourceVersions: string[];
|
||||
active: ActiveVersionData;
|
||||
rootUrl: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface OptionProps {
|
||||
option: string;
|
||||
selected: boolean;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export interface ArrowIconProps {
|
||||
open: boolean;
|
||||
}
|
20
src/components/VersionSelector/use-outside-click.tsx
Normal file
20
src/components/VersionSelector/use-outside-click.tsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { useEffect } from 'react';
|
||||
|
||||
/**
|
||||
* Hook that fires event on clicks outside of the passed ref
|
||||
*/
|
||||
export function useOutsideClick(ref, callback) {
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event) {
|
||||
if (ref.current && !ref.current.contains(event.target)) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
// Bind the event listener
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
// Unbind the event listener on clean up
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, [ref, callback]);
|
||||
}
|
16
src/components/__tests__/VersionSelector.test.tsx
Normal file
16
src/components/__tests__/VersionSelector.test.tsx
Normal file
|
@ -0,0 +1,16 @@
|
|||
/* eslint-disable import/no-internal-modules */
|
||||
import * as React from 'react';
|
||||
import { render } from 'enzyme';
|
||||
import { VersionSelector } from '../VersionSelector';
|
||||
import * as versionData from './data/mockVersionData.json';
|
||||
|
||||
describe('VersionSelector', () => {
|
||||
it('should correctly render VersionSelector', () => {
|
||||
const wrapper = render(<VersionSelector {...versionData} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.find('label').text()).toBe(
|
||||
`Version Selector: v${versionData.active.apiVersion}`,
|
||||
);
|
||||
expect(wrapper.find('button').text()).toBe(versionData.resourceVersions.slice(-1)[0]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,117 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`VersionSelector should correctly render VersionSelector 1`] = `
|
||||
<div
|
||||
class="sc-gtsrHT hgSzEu"
|
||||
>
|
||||
<div
|
||||
class="sc-dlnjwi kBpWeg"
|
||||
>
|
||||
<label
|
||||
class="sc-eCApnc kkTJUz"
|
||||
>
|
||||
Version Selector: v2.0
|
||||
</label>
|
||||
<button
|
||||
aria-labelledby="View a different version of documentation."
|
||||
class="sc-hKFxyN hYrDYQ"
|
||||
>
|
||||
<div
|
||||
class="sc-iCoGMd fgtoGu"
|
||||
>
|
||||
<div>
|
||||
<div>
|
||||
2023-01-01
|
||||
</div>
|
||||
</div>
|
||||
<svg
|
||||
fill="none"
|
||||
height="16"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<polyline
|
||||
points="6 9 12 15 18 9"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
aria-labelledby="View a different version of documentation."
|
||||
class="sc-jrsJWt ofjNY"
|
||||
role="listbox"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div>
|
||||
<ul
|
||||
class="sc-gKAaRy iQghMf"
|
||||
>
|
||||
<li
|
||||
aria-selected="false"
|
||||
class="sc-fujyAs iIVhNL"
|
||||
role="option"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="sc-kEqXSa cBTyJc"
|
||||
/>
|
||||
<span
|
||||
class="sc-pNWdM irvinq"
|
||||
>
|
||||
2021-09-09
|
||||
</span>
|
||||
</li>
|
||||
<li
|
||||
aria-selected="false"
|
||||
class="sc-fujyAs iIVhNL"
|
||||
role="option"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="sc-kEqXSa cBTyJc"
|
||||
/>
|
||||
<span
|
||||
class="sc-pNWdM irvinq"
|
||||
>
|
||||
2022-10-18
|
||||
</span>
|
||||
</li>
|
||||
<li
|
||||
aria-selected="true"
|
||||
class="sc-fujyAs Ywdfd"
|
||||
role="option"
|
||||
selected=""
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
aria-label="Checkmark Icon"
|
||||
class="sc-iqAclL ejyrfD"
|
||||
height="16"
|
||||
role="img"
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M6.30583 9.05037L11.7611 3.59509C12.1516 3.20457 12.7848 3.20457 13.1753 3.59509L13.8824 4.3022C14.273 4.69273 14.273 5.32589 13.8824 5.71642L6.81525 12.7836C6.38819 13.2106 5.68292 13.1646 5.31505 12.6856L2.26638 8.71605C1.92998 8.27804 2.01235 7.65025 2.45036 7.31385L3.04518 6.85702C3.59269 6.43652 4.37742 6.53949 4.79792 7.087L6.30583 9.05037Z"
|
||||
fill="currentColor"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="sc-pNWdM irvinq"
|
||||
>
|
||||
2023-01-01
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
8
src/components/__tests__/data/mockVersionData.json
Normal file
8
src/components/__tests__/data/mockVersionData.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"active": {
|
||||
"apiVersion": "2.0",
|
||||
"resourceVersion": "2023-01-01"
|
||||
},
|
||||
"rootUrl": "https://mongodb.com/docs/atlas/reference/api-resources-spec/v2",
|
||||
"resourceVersions": ["2021-09-09", "2022-10-18", "2023-01-01"]
|
||||
}
|
Loading…
Reference in New Issue
Block a user