Dop 3482 v selector (#8)

This commit is contained in:
mmeigs 2023-02-08 10:43:22 -05:00 committed by GitHub
parent 50e29d69bb
commit 2d61688fcc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 512 additions and 0 deletions

View 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>
);

View 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;

View 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>
);
};

View 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);

View File

@ -0,0 +1 @@
export * from './VersionSelector';

View 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;
`;

View 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;
}

View 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]);
}

View 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]);
});
});

View File

@ -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>
`;

View 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"]
}