Browse Source

ref(ui): Remove button.less (#7772)

* change fontSizeMedium = 14px, large= 16, extraLarge=18
* fix tooltips + add `aria-label`
Billy Vong 6 years ago
parent
commit
744ddacabe

+ 184 - 81
src/sentry/static/sentry/app/components/buttons/button.jsx

@@ -2,22 +2,11 @@ import {Flex, Box} from 'grid-emotion';
 import {Link} from 'react-router';
 import PropTypes from 'prop-types';
 import React from 'react';
-import classNames from 'classnames';
-import styled from 'react-emotion';
+import styled, {css} from 'react-emotion';
 
 import ExternalLink from 'app/components/externalLink';
 import InlineSvg from 'app/components/inlineSvg';
-
-import 'app/../less/components/button.less';
-
-const Icon = styled(Box)`
-  margin-right: ${p => (p.size === 'small' ? '6px' : '8px')};
-  margin-left: -2px;
-`;
-
-const StyledInlineSvg = styled(InlineSvg)`
-  display: block;
-`;
+import Tooltip from 'app/components/tooltip';
 
 class Button extends React.Component {
   static propTypes = {
@@ -33,6 +22,9 @@ class Button extends React.Component {
      * Use this prop if button should use a normal (non-react-router) link
      */
     href: PropTypes.string,
+    /**
+     * Path to an icon svg that will be displayed to left of button label
+     */
     icon: PropTypes.string,
     /**
      * Tooltip text
@@ -42,7 +34,16 @@ class Button extends React.Component {
      * Is an external link? (Will open in new tab)
      */
     external: PropTypes.bool,
+    /**
+     * Button with a border
+     */
     borderless: PropTypes.bool,
+    /**
+     * Label for screen-readers (`aria-label`).
+     * `children` will be used by default (only if it is a string), but this property takes priority.
+     */
+    label: PropTypes.string,
+
     onClick: PropTypes.func,
   };
 
@@ -62,26 +63,21 @@ class Button extends React.Component {
     onClick(...args);
   };
 
-  getUrl = () => {
-    let {disabled, to, href} = this.props;
+  getUrl = prop => {
+    let {disabled} = this.props;
     if (disabled) return null;
-    return to || href;
+    return prop;
   };
 
   render() {
     let {
-      priority,
       size,
       to,
       href,
-      children,
-      className,
-      disabled,
-      busy,
       title,
-      borderless,
       icon,
-      external,
+      children,
+      label,
 
       // destructure from `buttonProps`
       // not necessary, but just in case someone re-orders props
@@ -90,70 +86,177 @@ class Button extends React.Component {
       ...buttonProps
     } = this.props;
 
-    let isPrimary = priority === 'primary' && !disabled;
-    let isDanger = priority === 'danger' && !disabled;
-    let isSuccess = priority === 'success' && !disabled;
-    let isLink = priority === 'link';
-
-    let cx = classNames(className, 'button', {
-      tip: !!title,
-      'button-no-border': borderless,
-      'button-primary': isPrimary,
-      'button-danger': isDanger,
-      'button-success': isSuccess,
-      'button-link': isLink && !isPrimary && !isDanger,
-      'button-default': !isLink && !isPrimary && !isDanger,
-      'button-zero': size === 'zero',
-      'button-sm': size === 'small',
-      'button-xs': size === 'xsmall',
-      'button-lg': size === 'large',
-      'button-busy': busy,
-      'button-disabled': disabled,
-    });
-
-    let childContainer = (
-      <Flex align="center" className="button-label">
-        {icon && (
-          <Icon size={size}>
-            <StyledInlineSvg src={icon} size={size === 'small' ? '12px' : '16px'} />
-          </Icon>
-        )}
-        {children}
-      </Flex>
+    // For `aria-label`
+    let screenReaderLabel = label || typeof children === 'string' ? children : undefined;
+
+    // Buttons come in 4 flavors: <Link>, <ExternalLink>, <a>, and <button>.
+    // Let's use props to determine which to serve up, so we don't have to think about it.
+    // *Note* you must still handle tabindex manually.
+    let button = (
+      <StyledButton
+        aria-label={screenReaderLabel}
+        to={this.getUrl(to)}
+        href={this.getUrl(href)}
+        size={size}
+        {...buttonProps}
+        onClick={this.handleClick}
+        role="button"
+      >
+        <ButtonLabel size={size}>
+          {icon && (
+            <Icon size={size} hasChildren={!!children}>
+              <StyledInlineSvg src={icon} size={size === 'small' ? '12px' : '16px'} />
+            </Icon>
+          )}
+          {children}
+        </ButtonLabel>
+      </StyledButton>
     );
 
-    // Buttons come in 3 flavors: Link, anchor, and regular buttons. Let's
-    // use props to determine which to serve up, so we don't have to think
-    // about it. As a bonus, let's ensure all buttons appear as a button
-    // control to screen readers. Note: you must still handle tabindex manually.
-
-    // Props common to all elements
-    let componentProps = {
-      disabled,
-      ...buttonProps,
-      onClick: this.handleClick,
-      className: cx,
-      role: 'button',
-      children: childContainer,
-    };
-
-    // Handle react-router Links
-    if (to) {
-      return <Link to={this.getUrl()} {...componentProps} />;
+    // Doing this instead of using `Tooltip`'s `disabled` prop so that we can minimize snapshot nesting
+    if (title) {
+      return <Tooltip title={title}>{button}</Tooltip>;
     }
 
-    if (href && external) {
-      return <ExternalLink href={this.getUrl()} {...componentProps} />;
-    }
+    return button;
+  }
+}
+
+export default Button;
+
+const getFontSize = ({size, theme}) => {
+  switch (size) {
+    case 'xsmall':
+    case 'small':
+      return theme.fontSizeSmall;
+    case 'large':
+      return theme.fontSizeLarge;
+    default:
+      return theme.fontSizeMedium;
+  }
+};
+
+const getFontWeight = ({priority}) => `font-weight: ${priority === 'link' ? 400 : 600};`;
+
+const getBoxShadow = active => ({priority, borderless, disabled}) => {
+  if (disabled || borderless || priority === 'link') {
+    return 'box-shadow: none';
+  }
+
+  return `box-shadow: ${active ? 'inset' : ''} 0 2px rgba(0, 0, 0, 0.05)`;
+};
+
+const getColors = ({priority, disabled, theme}) => {
+  let themeName = disabled ? 'disabled' : priority || 'default';
+  let {
+    color,
+    colorActive,
+    background,
+    backgroundActive,
+    border,
+    borderActive,
+  } = theme.button[themeName];
+
+  return css`
+    color: ${color};
+    background-color: ${background};
+    border: 1px solid ${border || 'transparent'};
 
-    // Handle traditional links
-    if (href) {
-      return <a href={this.getUrl()} {...componentProps} />;
+    &:hover,
+    &:focus,
+    &:active {
+      ${colorActive ? 'color: ${colorActive};' : ''};
+      background: ${backgroundActive};
+      border: 1px solid ${borderActive || border || 'transparent'};
     }
+  `;
+};
 
-    // Otherwise, fall back to basic button element
-    return <button {...componentProps} />;
+const StyledButton = styled(({busy, external, borderless, ...props}) => {
+  // Get component to use based on existance of `to` or `href` properties
+  // Can be react-router `Link`, `a`, or `button`
+  if (props.to) {
+    return <Link {...props} />;
   }
-}
 
-export default Button;
+  if (!props.href) {
+    return <button {...props} />;
+  }
+
+  if (external) {
+    return <ExternalLink {...props} />;
+  }
+
+  return <a {...props} />;
+})`
+  display: inline-block;
+  line-height: 1;
+  border-radius: ${p => p.theme.button.borderRadius};
+  padding: 0;
+  text-transform: none;
+
+  ${getFontWeight};
+  font-size: ${getFontSize};
+  ${getColors};
+  ${getBoxShadow(false)};
+  cursor: ${p => (p.disabled ? 'not-allowed' : 'pointer')};
+  opacity: ${p => (p.busy || p.disabled) && '0.65'};
+
+  &:active {
+    ${getBoxShadow(true)};
+  }
+  &:focus {
+    outline: none;
+  }
+
+  ${p => p.borderless && 'border-color: transparent'};
+`;
+
+/**
+ * Get label padding determined by size
+ */
+const getLabelPadding = ({size, priority}) => {
+  if (priority === 'link') {
+    return '0';
+  }
+
+  switch (size) {
+    case 'zero':
+      return '0';
+    case 'xsmall':
+      return '6px 10px';
+    case 'small':
+      return '8px 12px;';
+    case 'large':
+      return '14px 20px';
+
+    default:
+      return '12px 16px;';
+  }
+};
+
+const ButtonLabel = styled(props => <Flex align="center" {...props} />)`
+  padding: ${getLabelPadding};
+
+  .icon {
+    margin-right: 2px;
+  }
+`;
+
+const getIconMargin = ({size, hasChildren}) => {
+  // If button is only an icon, then it shouldn't have margin
+  if (!hasChildren) {
+    return '0';
+  }
+
+  return size === 'small' ? '6px' : '8px';
+};
+
+const Icon = styled(({hasChildren, ...props}) => <Box {...props} />)`
+  margin-right: ${getIconMargin};
+  margin-left: -2px;
+`;
+
+const StyledInlineSvg = styled(InlineSvg)`
+  display: block;
+`;

+ 1 - 0
src/sentry/static/sentry/app/components/confirm.jsx

@@ -146,6 +146,7 @@ class Confirm extends React.PureComponent {
               {cancelText}
             </Button>
             <Button
+              data-test-id="confirm-modal"
               disabled={this.state.disableConfirmButton}
               priority={priority}
               onClick={this.handleConfirm}

+ 9 - 10
src/sentry/static/sentry/app/components/dropdownButton.jsx

@@ -13,6 +13,7 @@ const DropdownButton = ({isOpen, children, ...props}) => {
   );
 };
 
+DropdownButton.displayName = 'DropdownButton';
 DropdownButton.propTypes = {
   isOpen: PropTypes.bool,
 };
@@ -24,17 +25,15 @@ const StyledChevronDown = styled(props => (
 `;
 
 const StyledButton = styled(({isOpen, ...props}) => <Button {...props} />)`
-  &.button {
-    border-bottom-right-radius: ${p => (p.isOpen ? 0 : p.theme.borderRadius)};
-    border-bottom-left-radius: ${p => (p.isOpen ? 0 : p.theme.borderRadius)};
-    position: relative;
-    z-index: 2;
-    box-shadow: ${p => (p.isOpen ? 'none' : p.theme.dropShadowLight)};
+  border-bottom-right-radius: ${p => (p.isOpen ? 0 : p.theme.borderRadius)};
+  border-bottom-left-radius: ${p => (p.isOpen ? 0 : p.theme.borderRadius)};
+  position: relative;
+  z-index: 2;
+  box-shadow: ${p => (p.isOpen ? 'none' : p.theme.dropShadowLight)};
 
-    &,
-    &:hover {
-      border-bottom-color: ${p => (p.isOpen ? 'transparent' : p.theme.borderDark)};
-    }
+  &,
+  &:hover {
+    border-bottom-color: ${p => (p.isOpen ? 'transparent' : p.theme.borderDark)};
   }
 `;
 

+ 52 - 2
src/sentry/static/sentry/app/utils/theme.jsx

@@ -1,4 +1,5 @@
 const theme = {
+  white: '#FFFFFF',
   offWhite: '#FAF9FB',
   whiteDark: '#fbfbfc',
 
@@ -87,8 +88,9 @@ const theme = {
 
   grid: 8,
   fontSizeSmall: '12px',
-  fontSizeMedium: '16px',
-  fontSizeLarge: '18px',
+  fontSizeMedium: '14px',
+  fontSizeLarge: '16px',
+  fontSizeExtraLarge: '18px',
 
   settings: {
     containerWidth: '1140px',
@@ -124,4 +126,52 @@ theme.alert.error.background = theme.red;
 //alias warn to warning
 theme.alert.warn = theme.alert.warning;
 
+theme.button = {
+  borderRadius: '3px',
+
+  default: {
+    color: '#2f2936',
+    colorActive: '#161319',
+    background: theme.white,
+    backgroundActive: theme.white,
+    border: '#d8d2de',
+    borderActive: '#c9c0d1',
+  },
+  primary: {
+    color: theme.white,
+    background: theme.purple,
+    backgroundActive: '#4e3fb4',
+    border: '#3d328e',
+    borderActive: '#352b7b',
+  },
+  success: {
+    color: theme.white,
+    background: '#3fa372',
+    backgroundActive: theme.green,
+    border: '#7ccca5',
+    borderActive: '#7ccca5',
+  },
+  danger: {
+    color: theme.white,
+    background: theme.red,
+    backgroundActive: '#bf2a1d',
+    border: '#bf2a1d',
+    borderActive: '#7d1c13',
+  },
+  link: {
+    color: theme.blue,
+    background: 'transparent',
+    // border: '#3d328e',
+    backgroundActive: 'transparent',
+    // borderActive: '#352b7b',
+  },
+  disabled: {
+    color: '#ced3d6',
+    border: '#e3e5e6',
+    borderActive: '#e3e5e6',
+    background: theme.white,
+    backgroundActive: theme.white,
+  },
+};
+
 export default theme;

+ 1 - 1
src/sentry/static/sentry/app/views/organizationDashboard/index.jsx

@@ -74,7 +74,7 @@ const TeamTitleBar = styled(Flex)`
 
 const TeamName = styled.h4`
   margin: 0;
-  font-size: ${p => p.theme.fontSizeLarge};
+  font-size: ${p => p.theme.fontSizeExtraLarge};
 `;
 
 const ProjectCards = styled(Flex)`

+ 1 - 1
src/sentry/static/sentry/app/views/settings/components/emptyMessage.jsx

@@ -10,7 +10,7 @@ const Wrapper = styled.div`
   flex-direction: column;
   color: ${p => p.theme.gray4};
   padding: ${p => p.theme.grid * 3}px;
-  font-size: ${p => (p.theme.large ? p.theme.fontSizeLarge : p.theme.fontSizeMedium)};
+  font-size: ${p => (p.theme.large ? p.theme.fontSizeExtraLarge : p.theme.fontSizeLarge)};
   font-weight: bold;
 `;
 

+ 1 - 0
src/sentry/static/sentry/app/views/settings/components/forms/form.jsx

@@ -152,6 +152,7 @@ export default class Form extends React.Component {
             <Observer>
               {() => (
                 <Button
+                  data-test-id="form-submit"
                   priority={submitPriority}
                   disabled={
                     this.model.isError ||

+ 1 - 0
src/sentry/static/sentry/app/views/settings/project/projectServiceHooks.jsx

@@ -157,6 +157,7 @@ export default class ProjectServiceHooks extends AsyncView {
           action={
             access.has('project:write') ? (
               <Button
+                data-test-id="new-service-hook"
                 to={`/settings/${orgId}/${projectId}/hooks/new/`}
                 size="small"
                 priority="primary"

+ 0 - 169
src/sentry/static/sentry/less/components/button.less

@@ -1,169 +0,0 @@
-@import url('../palette.less');
-
-.button {
-  display: inline-block;
-  line-height: 1;
-  font-weight: 600;
-  border-radius: 3px;
-  box-shadow: 0 2px rgba(0, 0, 0, 0.05);
-  cursor: pointer;
-  font-size: 14px;
-  padding: 0;
-  text-transform: none;
-
-  .button-label {
-    padding: 12px 16px;
-    display: flex;
-    align-items: center;
-
-    .icon {
-      margin-right: 2px;
-    }
-  }
-
-  &:active {
-    box-shadow: inset 0 2px rgba(0, 0, 0, 0.05);
-  }
-  &:focus {
-    outline: none;
-  }
-
-  &.button-no-border {
-    border: none;
-    box-shadow: none;
-  }
-}
-
-// Priority concerns
-// - font color, background, and border
-.button-default {
-  color: @90;
-  background: #fff;
-  border: 1px solid @trim-dark;
-  &:hover,
-  &:focus,
-  &:active {
-    color: @100;
-    border-color: @trim-darkest;
-  }
-}
-
-.button-link {
-  color: @blue;
-  background: transparent;
-  border: none;
-  box-shadow: none;
-  font-weight: 400;
-
-  .button-label {
-    padding: 0;
-  }
-
-  &.button-disabled {
-    background: transparent;
-    border: none;
-    box-shadow: none;
-    &:hover,
-    &:focus,
-    &:active {
-      background: transparent;
-      border: none;
-      box-shadow: none;
-    }
-  }
-}
-
-.button-primary {
-  color: #fff;
-  background: @purple;
-  border: 1px solid @purple-darkest;
-  &:hover,
-  &:focus,
-  &:active {
-    color: #fff;
-    background: @purple-dark;
-    border-color: darken(@purple-darkest, 5);
-  }
-}
-
-.button-danger {
-  color: #fff;
-  background: @red;
-  border: 1px solid @red-dark;
-  &:hover,
-  &:focus,
-  &:active {
-    color: #fff;
-    background: @red-dark;
-    border-color: darken(@red-darkest, 5);
-  }
-}
-
-.button-success {
-  color: #fff;
-  background: @green-dark;
-  border: 1px solid @green-light;
-  &:hover,
-  &:focus,
-  &:active {
-    color: #fff;
-    background: @green;
-    border-color: @green-light;
-  }
-}
-
-.button-busy {
-  opacity: 0.65;
-}
-
-.button-disabled {
-  // old values: color: @50, border: @trim
-  // changed to match `.btn.disabled`
-  color: #ced3d6;
-  background: #fff;
-  border: 1px solid #e3e5e6;
-  cursor: not-allowed;
-  box-shadow: none;
-  opacity: 0.65;
-
-  &:hover,
-  &:focus,
-  &:active {
-    color: #ced3d6;
-    background: #fff;
-    border: 1px solid #e3e5e6;
-  }
-}
-
-// Size Concerns
-// - Padding and font size
-
-.button-zero {
-  .button-label {
-    padding: 0px;
-  }
-}
-
-.button-xs {
-  font-size: 12px;
-
-  .button-label {
-    padding: 6px 10px;
-  }
-}
-
-.button-sm {
-  font-size: 12px;
-
-  .button-label {
-    padding: 8px 12px;
-  }
-}
-
-.button-lg {
-  font-size: 16px;
-
-  .button-label {
-    padding: 14px 20px;
-  }
-}

+ 1 - 1
tests/acceptance/test_organization_settings.py

@@ -86,7 +86,7 @@ class OrganizationSettingsTest(AcceptanceTestCase):
             self.browser.click('#require2FA')
 
             self.browser.wait_until('.modal')
-            self.browser.click('.modal .button-primary')
+            self.browser.click('.modal [data-test-id="confirm-modal"]')
             self.browser.wait_until_not('.modal')
             self.browser.wait_until('.ref-toast.ref-error')
             self.load_organization_helper("setting 2fa without 2fa enabled")

Some files were not shown because too many files changed in this diff