Skip to content

Commit

Permalink
Add ButtonToggle
Browse files Browse the repository at this point in the history
  • Loading branch information
winkerVSbecks committed Nov 19, 2020
1 parent 30827bf commit 4f64cdf
Show file tree
Hide file tree
Showing 7 changed files with 403 additions and 0 deletions.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
"*.js": [
"yarn lint:js --fix"
],
"*.ts?(x)": [
"yarn lint:js --fix"
],
"package.json": [
"yarn lint:package"
]
Expand Down
120 changes: 120 additions & 0 deletions src/components/ButtonToggle.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { action } from '@storybook/addon-actions';
import React, { useState } from 'react';
import { ButtonToggle } from './ButtonToggle';
// @ts-ignore
import chromeImg from '../images/chrome.svg';
// @ts-ignore
import firefoxImg from '../images/firefox.svg';
// @ts-ignore
import ieImg from '../images/ie.svg';
// @ts-ignore
import safariImg from '../images/safari.svg';

const onSelectIndex = action('onSelectIndex');

export default {
title: 'Design System/ButtonToggle',
};

export const OutlineButtonsSmall = () => {
const [index, setIndex] = useState(1);

return (
<ButtonToggle
titles={[
{ title: '1 up', tooltip: '1 up [1]' },
{ title: '2 up', tooltip: '2 up [2]' },
]}
selectedIndex={index}
onSelectIndex={(idx) => setIndex(idx)}
/>
);
};

OutlineButtonsSmall.story = {
name: 'outline buttons small',
};

export const OutlineThreeButtonsSmall = () => {
const [index, setIndex] = useState(1);

return (
<ButtonToggle
titles={[
{ title: '1 up', tooltip: '1 up [1]' },
{ title: '2 up', tooltip: '2 up [2]' },
{ title: 'Diff', tooltip: 'Diff [3]' },
]}
selectedIndex={index}
onSelectIndex={(idx) => setIndex(idx)}
/>
);
};

OutlineThreeButtonsSmall.story = {
name: 'outline three buttons small',
};

export const PillWText = () => (
<ButtonToggle
titles={[
{ title: '360', tooltip: 'View at 360px' },
{ title: '764', tooltip: 'View at 764px' },
{ title: '1024', tooltip: 'View at 1024px' },
]}
selectedIndex={1}
onSelectIndex={onSelectIndex}
appearance="pill"
/>
);

PillWText.story = {
name: 'pill w/text',
};

export const PillWImage = () => (
<ButtonToggle
titles={[
{
title: <img src={chromeImg} alt="Chrome" />,
tooltip: 'View Chrome',
},
{
title: <img src={firefoxImg} alt="Firefox" />,
tooltip: 'View Firefox',
},
{
title: <img src={ieImg} alt="Safari" />,
tooltip: 'View Safari',
},
{
title: <img src={safariImg} alt="IE" />,
tooltip: 'View IE',
},
]}
selectedIndex={1}
onSelectIndex={onSelectIndex}
appearance="pill"
/>
);

PillWImage.story = {
name: 'pill w/image',
};

export const Tab = () => (
<ButtonToggle
titles={[
{ title: 'Canvas', tooltip: 'View your story' },
{ title: 'Snapshot', tooltip: 'View a screenshot of your story' },
{ title: 'Docs', tooltip: 'View docs for this story' },
]}
selectedIndex={1}
onSelectIndex={onSelectIndex}
appearance="tab"
/>
);

Tab.story = {
name: 'tab',
};
231 changes: 231 additions & 0 deletions src/components/ButtonToggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
import { opacify, rgba } from 'polished';
import React from 'react';
import styled, { css } from 'styled-components';
import { color, typography } from './shared/styles';
import { Link } from './Link';
// @ts-ignore
import { TooltipNote } from './tooltip/TooltipNote';
// @ts-ignore
import WithTooltip from './tooltip/WithTooltip';

const activePadding = 20;
const inActivePadding = 15;

interface StyledButtonProps {
isActive: boolean;
}

const StyledButton = styled.button<StyledButtonProps>`
border: 0;
border-radius: 3em;
cursor: pointer;
display: inline-block;
overflow: hidden;
padding: 8px ${inActivePadding}px;
position: relative;
text-align: center;
text-decoration: none;
transition: background-color 150ms ease-out, color 150ms ease-out;
transform: translate3d(0, 0, 0);
vertical-align: top;
white-space: nowrap;
user-select: none;
opacity: 1;
margin: 0;
font-size: ${typography.size.s1}px;
font-weight: ${typography.weight.extrabold};
line-height: 1;
color: ${color.dark};
background-color: transparent;
${(props) =>
props.isActive &&
css`
background-color: ${color.lightest};
box-shadow: ${opacify(0.05, color.border)} 0 0 0 1px inset;
color: ${color.darkest};
padding-right: ${activePadding}px;
padding-left: ${activePadding}px;
`}
`;

const TooltipWrapper = styled(WithTooltip)`
${(props) =>
props.isActive &&
css`
&:first-child {
margin-right: -${activePadding - inActivePadding}px;
}
:not(:first-child):not(:last-child) {
margin-left: -${activePadding - inActivePadding}px;
margin-right: -${activePadding - inActivePadding}px;
}
&:last-child {
margin-left: -${activePadding - inActivePadding}px;
}
`}
&:first-child ${StyledButton} {
padding-left: ${activePadding}px;
}
&:last-child ${StyledButton} {
padding-right: ${activePadding}px;
}
&:hover ${StyledButton} {
position: relative;
z-index: 1;
}
`;

const PillButton = styled(Link)`
font-size: ${typography.size.s1}px;
font-weight: ${typography.weight.bold};
line-height: 1;
display: inline-block;
vertical-align: top;
padding: 4px 8px;
border-radius: 10px;
&:hover {
background: #e3f3ff;
color: ${color.secondary};
img {
opacity: 1;
}
}
img {
height: 1rem;
width: 1rem;
opacity: 0.3;
transition: all 150ms ease-out;
margin: -3px 0;
}
${(props) =>
props.active &&
css`
background: #e3f3ff;
color: ${color.secondary};
img {
opacity: 1;
}
`};
`;

const TabButton = styled(Link)`
font-size: ${typography.size.s2 - 1}px;
font-weight: ${typography.weight.bold};
line-height: 20px;
display: inline-block;
padding: 10px 15px;
&:hover {
color: ${color.secondary};
}
${(props) =>
props.active &&
css`
color: ${color.secondary};
box-shadow: ${color.secondary} 0 -3px 0 0 inset;
`};
`;

interface WrapperProps {
appearance: 'outline' | 'pill' | 'tab';
}

const Wrapper = styled.div<WrapperProps>`
display: inline-flex;
align-items: center;
white-space: nowrap;
${(props) =>
props.appearance === 'outline' &&
css`
background-color: ${opacify(0.05, color.border)};
border-radius: 3em;
`}
`;

type Title = {
title: string | React.ReactNode;
tooltip: string;
};

interface ButtonToggleProps {
titles: Title[];
onSelectIndex: (index: number) => void;
selectedIndex: number;
appearance?: 'outline' | 'pill' | 'tab';
}

export function ButtonToggle({
titles,
onSelectIndex,
selectedIndex,
appearance,
...props
}: ButtonToggleProps) {
return (
<Wrapper appearance={appearance} {...props}>
{titles.map(({ title, tooltip }, index) => {
switch (appearance) {
case 'pill':
return (
<PillButton
// eslint-disable-next-line react/no-array-index-key
key={index}
active={index === selectedIndex}
onClick={() => onSelectIndex(index)}
title={tooltip}
secondary
isButton
>
{title}
</PillButton>
);
case 'tab':
return (
<TabButton
// eslint-disable-next-line react/no-array-index-key
key={index}
active={index === selectedIndex}
onClick={() => onSelectIndex(index)}
title={tooltip}
secondary
isButton
>
{title}
</TabButton>
);
case 'outline':
return (
<TooltipWrapper
tagName="span"
key={typeof title === 'string' ? title : index}
hasChrome={false}
placement="bottom"
trigger="hover"
tooltip={<TooltipNote note={tooltip} />}
isActive={index === selectedIndex}
>
<StyledButton
isActive={index === selectedIndex}
onClick={() => onSelectIndex(index)}
>
{title}
</StyledButton>
</TooltipWrapper>
);
default:
return 'this appearance is not supported';
}
})}
</Wrapper>
);
}

ButtonToggle.defaultProps = {
appearance: 'outline',
};
20 changes: 20 additions & 0 deletions src/images/chrome.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 4f64cdf

Please sign in to comment.