bufferapp/ui

View on GitHub
src/components/Button/Button.tsx

Summary

Maintainability
A
55 mins
Test Coverage
A
100%
import React from 'react'
import PropTypes from 'prop-types'
import styled from 'styled-components'
import * as Styles from './style'
import ChevronDown from '../Icon/Icons/ChevronDown'
import Select from '../Select/Select'

/*
Since buttons keep their own inline-block display type, we can only imitate this by using a wrapper (with
 `display: inline-block`) an internal container (with `display: flex`), and then the actual contents (including real
  buttons
 */

export const ButtonWrapperStyled = styled.div`
  ${Styles.ButtonWrapperBase};
  ${(props) =>
    // @ts-expect-error TS(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
    Styles[[props.type, props.disabled ? 'Disabled' : ''].join('')]};
  ${(props): string =>  
// @ts-expect-error TS(2322) FIXME: Type 'FlattenSimpleInterpolation | ""' is not assi... Remove this comment to see the full error message
 (props.fullWidth ? Styles.fullWidth : '')};
`

export const ButtonContainerStyled = styled.div`
  ${Styles.ButtonContainerBase};
`

export const ButtonStyled = styled.button`
  ${Styles.ButtonNestedBase};
  ${(props) =>
    // @ts-expect-error TS(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
    Styles[props.size]};
  ${(props) =>
    // @ts-expect-error TS(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
    Styles[props.disabled ? 'disabled' : '']};
  ${(props): string =>  
// @ts-expect-error TS(2322) FIXME: Type 'FlattenSimpleInterpolation | ""' is not assi... Remove this comment to see the full error message
 (props.fullWidth ? Styles.fullWidth : '')};
`

const Loading = styled.img`
  width: 24px;
  margin-left: 10px;
`

const VisuallyHiddenLabel = styled.span`
  position: absolute;
  overflow: hidden;
  clip: rect(0 0 0 0);
  height: 1px;
  width: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
`

/** All buttons, including text and split-buttons, follow the same core principles in dimensions, padding, and font sizes.
 * Combined with simple modifiers, they can be changed in size and appearance.  */
const Button = ({
  // @ts-expect-error TS(7031) FIXME: Binding element 'disabled' implicitly has an 'any'... Remove this comment to see the full error message
  disabled,
  // @ts-expect-error TS(7031) FIXME: Binding element 'onClick' implicitly has an 'any' ... Remove this comment to see the full error message
  onClick,
  // @ts-expect-error TS(7031) FIXME: Binding element 'type' implicitly has an 'any' typ... Remove this comment to see the full error message
  type,
  // @ts-expect-error TS(7031) FIXME: Binding element 'size' implicitly has an 'any' typ... Remove this comment to see the full error message
  size,
  // @ts-expect-error TS(7031) FIXME: Binding element 'label' implicitly has an 'any' ty... Remove this comment to see the full error message
  label,
  // @ts-expect-error TS(7031) FIXME: Binding element 'isSplit' implicitly has an 'any' ... Remove this comment to see the full error message
  isSplit,
  // @ts-expect-error TS(7031) FIXME: Binding element 'loading' implicitly has an 'any' ... Remove this comment to see the full error message
  loading,
  // @ts-expect-error TS(7031) FIXME: Binding element 'icon' implicitly has an 'any' typ... Remove this comment to see the full error message
  icon,
  // @ts-expect-error TS(7031) FIXME: Binding element 'iconEnd' implicitly has an 'any' ... Remove this comment to see the full error message
  iconEnd,
  // @ts-expect-error TS(7031) FIXME: Binding element 'hasIconOnly' implicitly has an 'a... Remove this comment to see the full error message
  hasIconOnly,
  // @ts-expect-error TS(7031) FIXME: Binding element 'isSelect' implicitly has an 'any'... Remove this comment to see the full error message
  isSelect,
  // @ts-expect-error TS(7031) FIXME: Binding element 'items' implicitly has an 'any' ty... Remove this comment to see the full error message
  items,
  // @ts-expect-error TS(7031) FIXME: Binding element 'selectPosition' implicitly has an... Remove this comment to see the full error message
  selectPosition,
  // @ts-expect-error TS(7031) FIXME: Binding element 'onSelectClick' implicitly has an ... Remove this comment to see the full error message
  onSelectClick,
  // @ts-expect-error TS(7031) FIXME: Binding element 'fullWidth' implicitly has an 'any... Remove this comment to see the full error message
  fullWidth,
  // @ts-expect-error TS(7031) FIXME: Binding element 'tooltip' implicitly has an 'any' ... Remove this comment to see the full error message
  tooltip,
  // @ts-expect-error TS(7031) FIXME: Binding element 'ref' implicitly has an 'any' type... Remove this comment to see the full error message
  ref,
  // @ts-expect-error TS(7031) FIXME: Binding element 'hideSearch' implicitly has an 'an... Remove this comment to see the full error message
  hideSearch,
  // @ts-expect-error TS(7031) FIXME: Binding element 'textToLeft' implicitly has an 'an... Remove this comment to see the full error message
  textToLeft,
  // @ts-expect-error TS(7031) FIXME: Binding element 'className' implicitly has an 'any... Remove this comment to see the full error message
  className,
  // @ts-expect-error TS(7031) FIXME: Binding element 'children' implicitly has an 'any'... Remove this comment to see the full error message
  children,
  // @ts-expect-error TS(7031) FIXME: Binding element 'onOpen' implicitly has an 'any' t... Remove this comment to see the full error message
  onOpen,
  ...props
}) => (
  <ButtonWrapperStyled
    className={className}
    // @ts-expect-error TS(2769) FIXME: No overload matches this call.
    disabled={disabled}
    type={type}
    fullWidth={fullWidth}
    $loading={loading}
  >
    <ButtonContainerStyled>
      <ButtonStyled
        onClick={!disabled ? onClick : undefined}
        disabled={disabled}
        // @ts-expect-error TS(2769) FIXME: No overload matches this call.
        isSplit={isSplit}
        icon={icon}
        hasIconOnly={hasIconOnly}
        data-tip={tooltip}
        ref={ref}
        aria-haspopup="false"
        size={size}
        fullWidth={fullWidth}
        type={type}
        {...props}
      >
        {!iconEnd && icon}
        {hasIconOnly && <VisuallyHiddenLabel>{label}</VisuallyHiddenLabel>}
        {!hasIconOnly && (
          // @ts-expect-error TS(2769) FIXME: No overload matches this call.
          <Styles.ButtonLabel hasIcon={!!icon} iconEnd={!!iconEnd}>
            {label}
          </Styles.ButtonLabel>
        )}
        {iconEnd && icon}

        {isSelect && (type === 'primary' || type === 'secondary') && (
          // @ts-expect-error TS(2769) FIXME: No overload matches this call.
          <Styles.ButtonArrow textToLeft={textToLeft}>
            <ChevronDown
              color={type === 'primary' ? 'white' : 'grayDark'}
              size={size}
              isChevron
            />
          </Styles.ButtonArrow>
        )}

        {loading && <Loading src="./images/loading-gray.svg" alt="loading" />}
      </ButtonStyled>
      {isSplit &&
        (type === 'primary' || type === 'secondary') &&
        (children ? (
          React.Children.map(children, (child) => React.cloneElement(child, {}))
        ) : (
          <Select
            // @ts-expect-error TS(2769) FIXME: No overload matches this call.
            onSelectClick={onSelectClick}
            items={items}
            type={type}
            size={size}
            isSplit
            yPosition={selectPosition}
            xPosition="right"
            disabled={disabled}
            hideSearch={hideSearch}
            onOpen={onOpen}
          />
        ))}
    </ButtonContainerStyled>
  </ButtonWrapperStyled>
)

Button.propTypes = {
  /** Is the button disabled */
  disabled: PropTypes.bool,

  /** Size of the Button */
  size: PropTypes.oneOf(['small', 'large', 'medium']),

  /** OnClick handler */
  onClick: PropTypes.func.isRequired,

  /** Button label */
  label: PropTypes.string,

  /** Type of button */
  type: PropTypes.oneOf([
    'primary',
    'secondary',
    'text',
    'error',
    'danger',
    'orange',
  ]),

  /** Is the Button Split  */
  isSplit: PropTypes.bool,

  /** Is this the Select button with chevron */
  isSelect: PropTypes.bool,

  /** Is the Button Loading  */
  loading: PropTypes.bool,

  /** Does the button have only an icon and no label */
  hasIconOnly: PropTypes.bool,

  /** Icon to show with the label */
  icon: PropTypes.node,

  /** Icon to show with the label */
  iconEnd: PropTypes.bool,

  /** Items to display in the Split Button popup */
  items: PropTypes.arrayOf(
    PropTypes.shape({
      id: PropTypes.string,
      title: PropTypes.string,
    }),
  ),

  /** Child element(s) to use for custom Split Button child content */
  children: PropTypes.element,

  /** Position of the Select popup */
  selectPosition: PropTypes.oneOf(['top', 'bottom']),

  /** Function to call on Split Button selected item click */
  onSelectClick: PropTypes.func,

  /** Is the button the full width of the parent container */
  fullWidth: PropTypes.bool,

  /** Tooltip to show on the component */
  tooltip: PropTypes.string,

  /** The prop to get the DOM element of the Button */
  ref: PropTypes.node,

  /** Is search hidden */
  hideSearch: PropTypes.bool,

  /** class passed by the dom element */
  className: PropTypes.string,

  /** onOpen function to fire when the Dropdown menu is open */
  onOpen: PropTypes.func,
}

Button.defaultProps = {
  disabled: false,
  isSplit: undefined,
  loading: false,
  size: 'medium',
  type: 'secondary',
  label: undefined,
  hasIconOnly: false,
  icon: undefined,
  iconEnd: false,
  isSelect: undefined,
  items: undefined,
  selectPosition: 'bottom',
  onSelectClick: undefined,
  fullWidth: false,
  tooltip: undefined,
  ref: undefined,
  hideSearch: true,
  className: undefined,
  children: undefined,
  onOpen: null,
}

export default Button