@retailmenot/anchor
Version:
A React UI Library by RetailMeNot
468 lines (453 loc) • 16.9 kB
JavaScript
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
// REACT
import * as React from 'react';
// VENDOR
import classNames from 'classnames';
import styled, { css, ThemeContext, } from '@xstyled/styled-components';
import { variant as createVariant, th, space as spaceStyles, } from '@xstyled/system';
import { transparentize } from 'polished';
// ANCHOR
import { TRANSITION_SPEED, REVEAL_BACKGROUND_COLOR, REVEAL_COLOR, } from './utils';
import { rem } from '../utils/rem/rem';
import { cloneWithProps } from '../utils/cloneWithProps/cloneWithProps';
import { Flip } from './Flip';
import { HitArea } from './HitArea';
// Todo: These should not be hardcoded like this, and needs
// a better solution.
// Basically a copy of 'ash'
const disabledColor = {
dark: '#808080',
light: '#D3D3D3',
};
// Just a copy of 'savvyCyan'
const primaryColor = {
base: '#0998D6',
light: '#66CCFF',
dark: '#0074A6',
};
// Just a copy of 'charcoal'
const grayColor = {
base: '#323232',
light: '#595959',
dark: '#000000',
};
const themeDefaults = {
filled: primaryColor,
outline: {
base: primaryColor.dark,
light: primaryColor.base,
dark: primaryColor.dark,
},
minimal: {
base: primaryColor.dark,
light: primaryColor.base,
dark: primaryColor.dark,
},
};
const reverseDefaults = {
filled: {
base: grayColor.light,
light: grayColor.light,
dark: grayColor.dark,
},
outline: {
base: 'white',
light: 'white',
dark: grayColor.light,
},
minimal: {
base: 'white',
light: 'white',
dark: 'white',
},
};
export const BUTTON_KEY = 'buttons';
export const BUTTON_THEME = {
sizes: {
xs: {
minWidth: 4,
height: 2,
padding: 0.5,
circularPadding: 1,
fontSize: 0.75,
affixSpacing: 0.375,
},
sm: {
minWidth: 5,
height: 2.5,
padding: 1,
circularPadding: 1.5,
fontSize: 0.875,
affixSpacing: 0.375,
},
md: {
minWidth: 12.5,
height: 3,
padding: 1.5,
circularPadding: 2,
fontSize: 1,
affixSpacing: 0.5,
},
lg: {
minWidth: 12.5,
height: 3.5,
padding: 2,
circularPadding: 2.5,
fontSize: 1,
affixSpacing: 0.5,
},
},
variants: {
filled: {
base: ({ reverse, colorTheme }) => reverse
? css `
border: solid thin white;
background-color: white;
color: ${colorTheme.base};
`
: css `
border: solid thin ${colorTheme.base};
background-color: ${colorTheme.base};
color: white;
`,
disabled: ({ reverse, colorTheme }) => reverse
? css `
border: solid thin white;
background-color: white;
color: ${colorTheme.base};
opacity: 0.5;
`
: css `
border: solid thin ${disabledColor.light};
background-color: ${disabledColor.light};
color: ${disabledColor.dark};
`,
hover: ({ reverse, colorTheme }) => reverse
? css `
border: solid thin rgba(255, 255, 255, 0.85);
background-color: rgba(255, 255, 255, 0.85);
color: ${colorTheme.base};
`
: css `
background-color: ${colorTheme.dark};
border: solid thin ${colorTheme.dark};
`,
active: ({ reverse }) => reverse
? css `
border: solid thin white;
background-color: white;
`
: undefined,
focus: ({ reverse }) => reverse
? css `
border: solid thin white;
background-color: white;
`
: undefined,
focusOutline: ({ reverse, colorTheme }) => reverse
? css `
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.4);
`
: css `
box-shadow: 0 0 0 2px
${transparentize(0.6, colorTheme.base)};
`,
},
outline: {
base: ({ colorTheme }) => css `
border: solid thin ${colorTheme.base};
background-color: transparent;
color: ${colorTheme.base};
`,
disabled: ({ reverse, colorTheme }) => reverse
? css `
opacity: 0.5;
border: solid thin ${colorTheme.base};
background-color: transparent;
color: ${colorTheme.base};
`
: css `
border: solid thin ${disabledColor.dark};
background-color: transparent;
color: ${disabledColor.dark};
`,
hover: ({ reverse, colorTheme }) => reverse
? css `
border: solid thin ${colorTheme.base};
background-color: ${colorTheme.base};
color: ${grayColor.light};
`
: css `
background-color: ${colorTheme.dark};
border: solid thin ${colorTheme.dark};
color: white;
`,
focusOutline: ({ reverse, colorTheme }) => reverse
? css `
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.4);
`
: css `
box-shadow: 0 0 0 2px
${transparentize(0.6, colorTheme.light)};
`,
},
minimal: {
base: ({ colorTheme }) => css `
border: solid thin transparent;
background-color: transparent;
color: ${colorTheme.base};
`,
disabled: ({ reverse }) => reverse
? css `
border: solid thin transparent;
background-color: transparent;
color: ${disabledColor.dark};
`
: css `
border: solid thin ${disabledColor.light};
background-color: ${disabledColor.light};
color: ${disabledColor.dark};
`,
hover: ({ reverse, colorTheme }) => reverse
? css `
background: ${transparentize(0.84, colorTheme.base)};
color: ${colorTheme.base};
`
: css `
background: ${transparentize(0.84, disabledColor.dark)};
color: ${colorTheme.dark};
`,
active: ({ reverse, colorTheme }) => reverse
? css `
background: ${transparentize(0.8, colorTheme.base)};
`
: undefined,
focusOutline: ({ reverse }) => reverse
? css `
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.4);
`
: css `
box-shadow: 0 0 0 2px
${transparentize(0.6, disabledColor.dark)};
`,
},
},
};
const sizeStyles = createVariant({
key: `${BUTTON_KEY}.sizes`,
prop: 'size',
default: 'md',
variants: BUTTON_THEME.sizes,
});
const stateStyles = createVariant({
key: `${BUTTON_KEY}.variants`,
prop: 'variant',
default: 'filled',
variants: BUTTON_THEME.variants,
});
const OutlineStyles = ({ buttonStyles, borderRadius }) => css `
&:after {
position: absolute;
content: '';
// overlap border (1px) and extend 2px past
// TODO: determine approach for spacing with larger than 1px borders
top: -3px;
left: -3px;
right: -3px;
bottom: -3px;
border-radius: calc(${th.radius(borderRadius)} + 2px);
// shadow instead of border so that it doesn't contribute to clickable area
${buttonStyles.focusOutline}
}
`;
const StyledButton = styled('button') `
position: relative;
${({ borderRadius }) => css({ borderRadius })};
font-weight: 600;
font-family: base;
text-align: center;
cursor: pointer;
display: inline-flex;
justify-content: center;
align-items: center;
outline: none !important;
transition: ${TRANSITION_SPEED} ease background-color,
${TRANSITION_SPEED} ease border-color,
${TRANSITION_SPEED} ease color,
${TRANSITION_SPEED} ease fill;
// These properties are deprecated but help make white text
// on colored backgrounds look more crisp in Chrome and Firefox.
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
/* Base and Disabled Styles */
${({ disabled, buttonStyles }) => disabled
? css `
${buttonStyles.disabled};
cursor: not-allowed;
`
: buttonStyles.base}
/* Sizing */
${({ $fontSize, $height }) => css `
font-size: ${rem($fontSize)};
height: ${rem($height)};
`}
${({ minWidth, block }) => block
? css `
width: 100%;
`
: css `
min-width: ${rem(minWidth)};
`}
/* State styles */
${({ disabled, revealed, flip, forceHover, forceFocus, forceActive, buttonStyles, ...props }) => !disabled &&
!revealed &&
css `
&:hover,
&:focus,
&:active {
${buttonStyles.hover}
${flip &&
css `
& > .flip-base {
opacity: 0;
}
`}
}
&:active {
${buttonStyles.active}
}
&:focus {
${buttonStyles.focus}
}
${(forceHover || forceFocus || forceActive) &&
css `
${buttonStyles.hover}
${flip &&
css `
& > .flip-base {
opacity: 0;
}
`}
${forceActive && buttonStyles.active}
${forceFocus && buttonStyles.focus}
${forceFocus && OutlineStyles({ buttonStyles, ...props })}
`}
`}
&:focus {
${OutlineStyles}
}
/* Revealed State */
${({ variant, revealed }) => variant === 'filled' &&
revealed &&
css `
background-color: ${REVEAL_BACKGROUND_COLOR};
border: solid thin ${REVEAL_BACKGROUND_COLOR};
color: ${REVEAL_COLOR};
font-weight: bold;
`}
${spaceStyles}
`;
export const Button = React.forwardRef((_a, ref) => {
var { className, flip = false, variant = 'filled', size = 'md', block, disabled, revealed, colorTheme, reverse, circular, children, minWidth, prefix, suffix, onMouseDown, onMouseUp, onFocus } = _a, props = __rest(_a, ["className", "flip", "variant", "size", "block", "disabled", "revealed", "colorTheme", "reverse", "circular", "children", "minWidth", "prefix", "suffix", "onMouseDown", "onMouseUp", "onFocus"]);
const theme = React.useContext(ThemeContext);
const [mouseDown, setMouseDown] = React.useState(false);
// if there are no children and only prefix or only suffix are set
const iconOnly = (prefix ? !suffix : !!suffix) &&
React.Children.count(children) === 0;
if (flip && reverse) {
/* eslint-disable-next-line */
console.warn("Buttons should not have both 'flip' and 'reverse' props.");
}
if (flip && disabled) {
/* eslint-disable-next-line */
console.warn("Buttons with 'flip' are not meant to be 'disabled'. Did you mean to make it 'revealed'?");
}
if (iconOnly && minWidth) {
/* eslint-disable-next-line */
console.warn("Button is icon-only so 'minWidth' prop will be ignored.");
}
if (iconOnly && block) {
/* eslint-disable-next-line */
console.warn("Button is icon-only so 'block' prop will be ignored.");
}
if (block && minWidth) {
/* eslint-disable-next-line */
console.warn("Button has 'block' prop so 'minWidth' prop will be ignored.");
}
const iconScale = iconOnly
? size === 'xs' || (size === 'sm' && variant === 'minimal')
? 'md'
: 'lg'
: 'md';
const dims = sizeStyles(Object.assign(Object.assign({}, props), { size, theme }));
const { height, affixSpacing, fontSize, minWidth: themeWidth } = dims;
// Value just needs to be larger than the height
// to make the ends circular. We're using a very
// large radius so that it doesn't actually have
// to be calculated from the height.
const borderRadius = circular ? 'circular' : 'base';
const width = iconOnly ? height : minWidth || themeWidth;
if (!colorTheme) {
colorTheme = reverse
? reverseDefaults[variant]
: themeDefaults[variant];
}
const padding = iconOnly
? '0'
: `0 ${circular ? dims.circularPadding : dims.padding}rem`;
const buttonStyles = stateStyles(Object.assign(Object.assign({}, props), { colorTheme,
variant,
theme }));
return (React.createElement(StyledButton, Object.assign({ onMouseDown: event => {
setMouseDown(true);
if (onMouseDown) {
onMouseDown(event);
}
}, onMouseUp: event => {
setMouseDown(false);
if (onMouseUp) {
onMouseUp(event);
}
}, onFocus: event => {
if (mouseDown) {
// This keeps the button from being :focused when
// clicked so that it is only applied when tabbed to.
// We want the outline to appear when tabbing for
// accessibility.
event.target.blur();
}
if (onFocus) {
onFocus(event);
}
}, className: classNames('anchor-button', className), ref: ref, flip: flip, block: block, colorTheme: colorTheme, "$fontSize": fontSize, padding: padding, reverse: reverse, minWidth: width, "$height": height, "$size": size, iconOnly: iconOnly, circular: circular, variant: variant, disabled: disabled, revealed: revealed, borderRadius: borderRadius, buttonStyles: buttonStyles }, props),
(height < 3 || width < 3) && (React.createElement(HitArea, { buttonHeight: height, buttonWidth: width })),
prefix &&
cloneWithProps(prefix, {
scale: iconScale,
margin: iconOnly
? undefined
: `0 ${affixSpacing}rem 0 0`,
className: 'anchor-button-prefix',
}),
children,
suffix &&
cloneWithProps(suffix, {
scale: iconScale,
margin: iconOnly
? undefined
: `0 0 0 ${affixSpacing}rem`,
className: 'anchor-button-suffix',
}),
flip && !disabled && !revealed && (React.createElement(Flip, { circular: circular, colorTheme: colorTheme, height: height }))));
});
//# sourceMappingURL=Button.component.js.map