Skip to content

Instantly share code, notes, and snippets.

@chrisoverstreet
Created June 24, 2020 14:39
Show Gist options
  • Save chrisoverstreet/65774ab578455f5fb526663282c4a4c3 to your computer and use it in GitHub Desktop.
Save chrisoverstreet/65774ab578455f5fb526663282c4a4c3 to your computer and use it in GitHub Desktop.
// @flow
import classnames from 'classnames';
import css from 'styled-jsx/css';
import PropTypes from 'prop-types';
import React from 'react';
import type { Node } from 'react';
import Icon from './Icon';
import Text from './Text';
import { COLOR, SPACING, FONT_SIZE, LINE_HEIGHT, TRANSITION } from './theme';
import type { IconKey } from './Icon';
import type { TextColorName } from './Text';
export type Size = 'large' | 'small';
export type Variant =
| 'primary'
| 'secondary'
| 'tertiary'
| 'positive'
| 'negative'
| 'ghost';
const fontColor: { [TextColorName]: string } = {
default: COLOR.black,
gray: COLOR.darkGray,
selected: COLOR.blue,
success: COLOR.green,
error: COLOR.red,
link: COLOR.blue,
};
const fontColorOnDark: { [TextColorName]: string } = {
default: COLOR.white,
gray: COLOR.softGray,
selected: COLOR.blue,
success: COLOR.green,
error: COLOR.red,
link: COLOR.blue,
};
const { className: glyphClassName, styles: glyphStyles } = css.resolve`
i {
font-size: ${FONT_SIZE.s16};
line-height: ${LINE_HEIGHT.s16};
//margin: 0 ${SPACING.xs}px 0 0 !important;
}
`;
// All of these props are marked as optional because we can't rely on Flow to
// recognize defaultProps for this component (React.forwardRef confuses Flow).
export type ButtonProps = {|
children?: Node,
className?: string,
glyph?: IconKey | Node,
href: string | null,
id?: string,
size?: Size,
style?: { [string]: any },
variant?: Variant,
|};
const LinkButton = React.forwardRef<ButtonProps, HTMLButtonElement>(
(
{
children,
className,
glyph,
href,
id,
size = 'large',
style,
variant = 'primary',
},
ref,
) => {
const color: TextColorName =
{
primary: 'default',
secondary: 'selected',
tertiary: 'gray',
positive: 'success',
negative: 'error',
ghost: 'selected',
}[variant] || 'default';
const opacity = 1;
const iconColor =
variant === 'primary' ? fontColorOnDark[color] : fontColor[color];
return (
<a
aria-busy={undefined}
aria-live="polite"
className={classnames('root', className, size, variant)}
href={href}
id={id}
ref={ref}
style={style}
>
{typeof glyph === 'string' && (
<Icon
className={glyphClassName}
icon={glyph}
style={{
color: iconColor,
}}
/>
)}
{glyph && typeof glyph !== 'string' && (
<i
className={glyphClassName}
style={{
color: iconColor,
}}
>
{glyph}
</i>
)}
<Text
color={color}
element="span"
onDark={variant === 'primary'}
size="large"
weight="semi"
>
{children}
</Text>
<style jsx>
{`
/* Root */
.root {
cursor: pointer;
width: fit-content;
align-items: center;
border-color: ${COLOR.darkGray};
border-style: solid;
border-width: 1px;
border-radius: 2px;
display: flex;
justify-content: center;
overflow: hidden;
padding: 0 ${SPACING.sm}px;
position: relative;
text-transform: capitalize;
transition: background-color ${TRANSITION.duration}
${TRANSITION.timingFunction},
border-color ${TRANSITION.duration} ${TRANSITION.timingFunction};
}
.root:hover,
.root:focus {
background-color: ${COLOR.softGray};
border-color: ${COLOR.black};
}
.root :global(i),
.root :global(span) {
opacity: ${opacity};
transition: color ${TRANSITION.duration}
${TRANSITION.timingFunction};
}
.large {
min-height: 56px;
padding-bottom: ${SPACING.sm}px;
padding-top: ${SPACING.sm}px;
}
.small {
min-height: 40px;
padding-bottom: ${SPACING.xs}px;
padding-top: ${SPACING.xs}px;
}
/* Icon */
.root :global(i + span) {
padding-left: 12px;
}
/* Loading */
.loading-overlay {
align-items: center;
bottom: 0;
display: flex;
height: 100%;
justify-content: center;
left: 0;
position: absolute;
width: 100%;
}
.root.loading {
color: ${COLOR.darkGray};
}
.root.loading :global(i),
.root.loading :global(span) {
opacity: 0;
}
/* Primary */
.root.primary {
background-color: ${COLOR.blue};
border-color: ${COLOR.blue};
}
.root.primary:hover,
.root.primary:focus {
background-color: ${COLOR.blueHover};
border-color: ${COLOR.blueHover};
}
.primary:not(.loading):disabled {
background-color: ${COLOR.lightGray};
border-color: ${COLOR.lightGray};
}
.primary:not(.loading):disabled :global(i),
.primary:not(.loading):disabled :global(span) {
color: ${COLOR.darkGray} !important;
}
.primary.loading {
color: ${COLOR.white};
}
/* Ghost Styles */
.ghost,
.ghost:hover,
.ghost.loading,
.ghost:disabled {
background-color: transparent;
border-color: transparent;
}
.ghost:hover :global(i),
.ghost:hover :global(span) {
color: ${COLOR.blueHover} !important;
}
.ghost:hover :global(span) {
text-decoration: underline !important;
}
/* Other Variants */
.secondary,
.tertiary,
.positive,
.negative {
background-color: ${COLOR.white};
}
.secondary:hover :global(i),
.secondary:hover :global(span) {
color: ${COLOR.blueHover} !important;
}
.tertiary:hover :global(i),
.tertiary:hover :global(span) {
color: ${COLOR.black} !important;
}
.positive:hover :global(i),
.positive:hover :global(span) {
color: ${COLOR.greenHover} !important;
}
.negative:hover :global(i),
.negative:hover :global(span) {
color: ${COLOR.redHover} !important;
}
`}
</style>
{glyphStyles}
</a>
);
},
);
// $FlowFixMe
LinkButton.propTypes = {
children: PropTypes.node,
className: PropTypes.string,
glyph: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
href: PropTypes.string,
id: PropTypes.string,
size: PropTypes.oneOf(['large', 'small']),
style: PropTypes.object,
variant: PropTypes.oneOf([
'primary',
'secondary',
'tertiary',
'positive',
'negative',
'ghost',
]),
};
// $FlowFixMe
LinkButton.defaultProps = {
children: null,
className: null,
glyph: null,
href: null,
id: null,
size: 'large',
style: null,
variant: 'primary',
};
export default LinkButton;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment