Forráskód Böngészése

update Button component with new variants (#983)

mikkio-j 3 éve
szülő
commit
e4910e98dc

+ 100 - 1
src/shared/components/Button/Button.stories.tsx

@@ -14,7 +14,11 @@ export default {
     className: { table: { disable: true } },
     to: { table: { disable: true } },
     type: { table: { disable: true } },
-    variant: { table: { disable: true } },
+    textOnly: { table: { disable: true } },
+    iconPlacement: {
+      control: { type: 'select', options: ['left', 'right'] },
+      defaultValue: 'left',
+    },
   },
 } as Meta
 
@@ -41,7 +45,102 @@ export const Tertiary = Template.bind({})
 Tertiary.args = {
   variant: 'tertiary',
 }
+export const Destructive = Template.bind({})
+Destructive.args = {
+  variant: 'destructive',
+}
+export const DestructiveSecondary = Template.bind({})
+DestructiveSecondary.args = {
+  variant: 'destructive-secondary',
+}
+export const Warning = Template.bind({})
+Warning.args = {
+  variant: 'warning',
+}
+export const WarningSecondary = Template.bind({})
+WarningSecondary.args = {
+  variant: 'warning-secondary',
+}
 export const WithIcon = Template.bind({})
 WithIcon.args = {
   icon: <SvgGlyphAddVideo />,
 }
+const TextOnlyTemplate: Story<ButtonProps> = (args) => (
+  <div>
+    <div style={{ display: 'flex', gap: '20px', marginBottom: '20px' }}>
+      <Button {...args} size="large">
+        Large
+      </Button>
+      <Button {...args} size="medium">
+        Medium
+      </Button>
+      <Button {...args} size="small">
+        Small
+      </Button>
+    </div>
+    <div style={{ display: 'flex', gap: '20px', marginBottom: '20px' }}>
+      <Button {...args} size="large" variant="secondary">
+        Large
+      </Button>
+      <Button {...args} size="medium" variant="secondary">
+        Medium
+      </Button>
+      <Button {...args} size="small" variant="secondary">
+        Small
+      </Button>
+    </div>
+    <div style={{ display: 'flex', gap: '20px', marginBottom: '20px' }}>
+      <Button {...args} size="large" variant="destructive-secondary">
+        Large
+      </Button>
+      <Button {...args} size="medium" variant="destructive-secondary">
+        Medium
+      </Button>
+      <Button {...args} size="small" variant="destructive-secondary">
+        Small
+      </Button>
+    </div>
+    <div style={{ display: 'flex', gap: '20px', marginBottom: '20px' }}>
+      <Button {...args} size="large" variant="warning-secondary">
+        Large
+      </Button>
+      <Button {...args} size="medium" variant="warning-secondary">
+        Medium
+      </Button>
+      <Button {...args} size="small" variant="warning-secondary">
+        Small
+      </Button>
+    </div>
+  </div>
+)
+
+const IconOnlyTemplate: Story<ButtonProps> = (args) => (
+  <>
+    <Button {...args} size="large"></Button>
+    <Button {...args} size="medium"></Button>
+    <Button {...args} size="small"></Button>
+    <Button {...args} size="large" variant="secondary"></Button>
+    <Button {...args} size="medium" variant="secondary"></Button>
+    <Button {...args} size="small" variant="secondary"></Button>
+    <Button {...args} size="large" variant="tertiary"></Button>
+    <Button {...args} size="medium" variant="tertiary"></Button>
+    <Button {...args} size="small" variant="tertiary"></Button>
+    <Button {...args} size="large" variant="destructive-secondary"></Button>
+    <Button {...args} size="medium" variant="destructive-secondary"></Button>
+    <Button {...args} size="small" variant="destructive-secondary"></Button>
+    <Button {...args} size="large" variant="warning-secondary"></Button>
+    <Button {...args} size="medium" variant="warning-secondary"></Button>
+    <Button {...args} size="small" variant="warning-secondary"></Button>
+  </>
+)
+
+export const TextOnly = TextOnlyTemplate.bind({})
+TextOnly.args = {
+  textOnly: true,
+  icon: <SvgGlyphAddVideo />,
+}
+
+export const IconOnly = IconOnlyTemplate.bind({})
+IconOnly.args = {
+  icon: <SvgGlyphAddVideo />,
+}

+ 30 - 9
src/shared/components/Button/Button.style.ts

@@ -1,21 +1,41 @@
 import { SerializedStyles, css } from '@emotion/react'
 import styled from '@emotion/styled'
 
-import { colors, sizes } from '@/shared/theme'
+import { sizes } from '@/shared/theme'
 
-import { ButtonBase, ButtonSize } from '../ButtonBase'
+import { ButtonBase, ButtonSize, ButtonVariant } from '../ButtonBase'
 import { Text } from '../Text'
 
+export type IconPlacement = 'left' | 'right'
+type ButtonIconWrapperProps = {
+  iconPlacement: IconPlacement
+  iconOnly?: boolean
+}
+
 type ButtonSizeProps = {
   size: ButtonSize
 }
 
-export type TextColorVariant = 'default' | 'error'
 type TextProps = {
-  textColorVariant?: TextColorVariant
+  textColorVariant?: ButtonVariant
+  textOnly?: boolean
+  iconOnly?: boolean
 } & ButtonSizeProps
 
-const sizeOverwriteStyles = ({ size }: ButtonSizeProps): SerializedStyles => {
+const sizeOverwriteStyles = ({
+  size,
+  textOnly,
+  iconOnly,
+}: Pick<TextProps, 'size' | 'textOnly' | 'iconOnly'>): SerializedStyles => {
+  if (textOnly)
+    return css`
+      padding-left: 0;
+      padding-right: 0;
+    `
+  if (iconOnly) {
+    /* stylelint-disable no-empty-source */
+    return css``
+  }
   switch (size) {
     case 'large':
       return css`
@@ -58,13 +78,14 @@ export const StyledButtonBase = styled(ButtonBase)<ButtonSizeProps>`
   ${sizeOverwriteStyles};
 `
 
-export const ButtonIconWrapper = styled.span`
-  margin-right: ${sizes(2)};
+export const ButtonIconWrapper = styled.span<ButtonIconWrapperProps>`
+  margin-right: ${({ iconPlacement, iconOnly }) => (iconPlacement === 'left' && !iconOnly ? sizes(2) : 0)};
+  margin-left: ${({ iconPlacement, iconOnly }) => (iconPlacement === 'right' && !iconOnly ? sizes(2) : 0)};
 `
 
 export const StyledText = styled(Text)<TextProps>`
   /* compensate for line-height being 1 */
-  ${textPaddingStyles};
+  ${textPaddingStyles}
 
-  color: ${({ textColorVariant }) => textColorVariant === 'error' && colors.error};
+  color: inherit;
 `

+ 28 - 6
src/shared/components/Button/Button.tsx

@@ -1,13 +1,13 @@
 import React from 'react'
 
-import { ButtonIconWrapper, StyledButtonBase, StyledText, TextColorVariant } from './Button.style'
+import { ButtonIconWrapper, IconPlacement, StyledButtonBase, StyledText } from './Button.style'
 
 import { ButtonBaseProps, ButtonSize } from '../ButtonBase'
 import { TextVariant } from '../Text'
 
 export type ButtonProps = {
   icon?: React.ReactNode
-  textColorVariant?: TextColorVariant
+  iconPlacement?: IconPlacement
   children: string
 } & Omit<ButtonBaseProps, 'children'>
 
@@ -18,15 +18,37 @@ const BUTTON_SIZE_TO_TEXT_VARIANT: Record<ButtonSize, TextVariant> = {
 }
 
 export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
-  ({ icon, children, size = 'medium', textColorVariant = 'default', ...baseButtonProps }, ref) => {
+  ({ icon, children, size = 'medium', iconPlacement = 'left', variant, textOnly, ...baseButtonProps }, ref) => {
+    const iconOnly = !children
     return (
-      <StyledButtonBase ref={ref} size={size} {...baseButtonProps}>
-        {icon && <ButtonIconWrapper>{icon}</ButtonIconWrapper>}
+      <StyledButtonBase
+        ref={ref}
+        size={size}
+        {...baseButtonProps}
+        textOnly={textOnly}
+        variant={variant}
+        iconOnly={iconOnly}
+      >
+        {icon && iconPlacement === 'left' && (
+          <ButtonIconWrapper iconOnly={iconOnly} iconPlacement={iconPlacement}>
+            {icon}
+          </ButtonIconWrapper>
+        )}
         {children && (
-          <StyledText variant={BUTTON_SIZE_TO_TEXT_VARIANT[size]} textColorVariant={textColorVariant} size={size}>
+          <StyledText
+            variant={BUTTON_SIZE_TO_TEXT_VARIANT[size]}
+            textColorVariant={variant || 'primary'}
+            textOnly={textOnly}
+            size={size}
+          >
             {children}
           </StyledText>
         )}
+        {icon && iconPlacement === 'right' && (
+          <ButtonIconWrapper iconOnly={iconOnly} iconPlacement={iconPlacement}>
+            {icon}
+          </ButtonIconWrapper>
+        )}
       </StyledButtonBase>
     )
   }

+ 162 - 8
src/shared/components/ButtonBase/ButtonBase.style.ts

@@ -4,45 +4,76 @@ import styled from '@emotion/styled'
 
 import { colors, sizes, transitions } from '../../theme'
 
-export type ButtonVariant = 'primary' | 'secondary' | 'tertiary'
+export type ButtonVariant =
+  | 'primary'
+  | 'secondary'
+  | 'tertiary'
+  | 'destructive'
+  | 'destructive-secondary'
+  | 'warning'
+  | 'warning-secondary'
 export type ButtonSize = 'large' | 'medium' | 'small'
 export type ButtonBaseStyleProps = {
   variant: ButtonVariant
   size: ButtonSize
   clickable?: boolean
+  textOnly: boolean
+  iconOnly: boolean
 }
 
-const variantStyles = ({ variant }: ButtonBaseStyleProps): SerializedStyles => {
+const variantStyles = ({ variant, textOnly, iconOnly }: ButtonBaseStyleProps): SerializedStyles => {
   switch (variant) {
     case 'primary':
       return css`
         background-color: ${colors.blue[500]};
+        color: ${textOnly ? colors.blue[300] : colors.gray[50]};
 
-        &:hover {
+        path {
+          fill: ${textOnly && colors.blue[300]};
+        }
+
+        &:hover,
+        &:focus {
           background-color: ${colors.blue[700]};
         }
 
         &:active {
           background-color: ${colors.blue[900]};
+          color: ${textOnly && colors.blue[400]};
+
+          path {
+            fill: ${textOnly && colors.blue[400]};
+          }
         }
       `
     case 'secondary':
       return css`
         /* 1px inset border */
         box-shadow: inset 0 0 0 1px ${colors.gray[500]};
+        color: ${colors.gray[50]};
 
-        &:hover {
-          box-shadow: inset 0 0 0 1px ${colors.gray[50]};
+        &:hover,
+        &:focus {
+          box-shadow: inset 0 0 0 2px ${colors.gray[300]};
+          border-color: ${colors.gray[300]};
         }
 
         &:active {
-          box-shadow: inset 0 0 0 1px ${colors.gray[50]};
-          background-color: ${colors.blue[500]};
+          box-shadow: inset 0 0 0 2px ${colors.gray[50]};
+          color: ${textOnly && colors.gray[300]};
+
+          path {
+            fill: ${textOnly && colors.gray[300]};
+          }
         }
       `
     case 'tertiary':
       return css`
-        &:hover {
+        color: ${colors.gray[50]};
+        ${iconOnly && `border-radius: 50%`};
+
+        &:hover,
+        &:focus {
           background-color: ${colors.transparentPrimary[12]};
         }
 
@@ -50,6 +81,84 @@ const variantStyles = ({ variant }: ButtonBaseStyleProps): SerializedStyles => {
           background-color: ${colors.transparentPrimary[6]};
         }
       `
+    case 'destructive':
+      return css`
+        background-color: ${colors.secondary.alert[100]};
+        color: ${colors.gray[50]};
+
+        &:hover,
+        &:focus {
+          background-color: ${colors.secondary.alert[200]};
+        }
+
+        &:active {
+          background-color: ${colors.secondary.alert[300]};
+        }
+      `
+    case 'destructive-secondary':
+      return css`
+        box-shadow: inset 0 0 0 1px ${colors.gray[400]};
+        color: ${colors.secondary.alert[50]};
+
+        path {
+          fill: ${colors.secondary.alert[50]};
+        }
+
+        &:hover,
+        &:focus {
+          box-shadow: inset 0 0 0 2px ${colors.gray[300]};
+        }
+
+        &:active {
+          box-shadow: inset 0 0 0 2px ${colors.gray[50]};
+          color: ${textOnly && colors.secondary.alert[100]};
+
+          path {
+            fill: ${textOnly && colors.secondary.alert[100]};
+          }
+        }
+      `
+    case 'warning':
+      return css`
+        background-color: ${colors.secondary.warning[100]};
+        color: ${colors.gray[900]};
+
+        path {
+          fill: ${colors.gray[900]};
+        }
+
+        &:hover,
+        &:focus {
+          background-color: ${colors.secondary.warning[200]};
+        }
+
+        &:active {
+          background-color: ${colors.secondary.warning[300]};
+        }
+      `
+    case 'warning-secondary':
+      return css`
+        color: ${colors.secondary.warning[100]};
+        box-shadow: inset 0 0 0 1px ${colors.gray[400]};
+
+        path {
+          fill: ${colors.secondary.warning[100]};
+        }
+
+        &:hover,
+        &:focus {
+          box-shadow: inset 0 0 0 2px ${colors.gray[300]};
+        }
+
+        &:active {
+          color: ${textOnly && colors.secondary.warning[300]};
+          box-shadow: inset 0 0 0 2px ${colors.gray[50]};
+
+          path {
+            fill: ${textOnly && colors.secondary.warning[300]};
+          }
+        }
+      `
   }
 }
 
@@ -70,6 +179,46 @@ const sizeStyles = ({ size }: ButtonBaseStyleProps): SerializedStyles => {
   }
 }
 
+const textOnlyStyles = ({ textOnly }: ButtonBaseStyleProps): SerializedStyles | null =>
+  textOnly
+    ? css`
+        background-color: ${colors.transparent};
+        box-shadow: none;
+        padding: 0;
+
+        &:hover,
+        &:focus {
+          background-color: ${colors.transparent};
+          box-shadow: none;
+        }
+
+        &:active {
+          background-color: ${colors.transparent};
+          box-shadow: none;
+        }
+      `
+    : null
+
+export const BorderWrapper = styled.div<Pick<ButtonBaseStyleProps, 'textOnly'>>`
+  display: flex;
+  align-items: center;
+  box-sizing: border-box;
+  margin-top: -0.5px;
+  margin-bottom: -0.5px;
+  height: 100%;
+  visibility: hidden;
+  border-bottom-width: ${({ textOnly }) => textOnly && '1px'};
+  border-bottom-style: ${({ textOnly }) => textOnly && 'solid'};
+
+  &:hover {
+    visibility: visible;
+  }
+
+  * {
+    visibility: visible;
+  }
+`
+
 export const StyledButtonBase = styled('button', { shouldForwardProp: isPropValid })<ButtonBaseStyleProps>`
   display: inline-flex;
   align-items: center;
@@ -91,4 +240,9 @@ export const StyledButtonBase = styled('button', { shouldForwardProp: isPropVali
 
   ${variantStyles};
   ${sizeStyles};
+  ${textOnlyStyles};
+
+  :focus ${BorderWrapper} {
+    visibility: visible;
+  }
 `

+ 21 - 3
src/shared/components/ButtonBase/ButtonBase.tsx

@@ -2,13 +2,15 @@ import { To } from 'history'
 import React from 'react'
 import { Link } from 'react-router-dom'
 
-import { ButtonBaseStyleProps, StyledButtonBase } from './ButtonBase.style'
+import { BorderWrapper, ButtonBaseStyleProps, StyledButtonBase } from './ButtonBase.style'
 
 export type ButtonBaseProps = {
   disabled?: boolean
   onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void
   to?: To
   type?: 'button' | 'submit'
+  textOnly?: boolean
+  iconOnly?: boolean
   children?: React.ReactNode
   className?: string
 } & Partial<Pick<ButtonBaseStyleProps, 'size' | 'variant'>>
@@ -26,7 +28,21 @@ const getLinkPropsFromTo = (to?: To) => {
 }
 
 export const ButtonBase = React.forwardRef<HTMLButtonElement, ButtonBaseProps>(
-  ({ onClick, to, type = 'button', children, size = 'medium', variant = 'primary', disabled, ...styleProps }, ref) => {
+  (
+    {
+      onClick,
+      to,
+      type = 'button',
+      children,
+      size = 'medium',
+      variant = 'primary',
+      textOnly = false,
+      iconOnly = false,
+      disabled,
+      ...styleProps
+    },
+    ref
+  ) => {
     const clickable = !!onClick || !!to || type === 'submit'
 
     const linkProps = getLinkPropsFromTo(to)
@@ -42,9 +58,11 @@ export const ButtonBase = React.forwardRef<HTMLButtonElement, ButtonBaseProps>(
         {...linkProps}
         size={size}
         variant={variant}
+        textOnly={textOnly}
+        iconOnly={iconOnly}
         {...styleProps}
       >
-        {children}
+        <BorderWrapper textOnly={textOnly}>{children}</BorderWrapper>
       </StyledButtonBase>
     )
   }

+ 16 - 0
src/shared/components/IconButton/IconButton.stories.tsx

@@ -46,3 +46,19 @@ export const Tertiary = Template.bind({})
 Tertiary.args = {
   variant: 'tertiary',
 }
+export const Destructive = Template.bind({})
+Destructive.args = {
+  variant: 'destructive',
+}
+export const DestructiveSecondary = Template.bind({})
+DestructiveSecondary.args = {
+  variant: 'destructive-secondary',
+}
+export const Warning = Template.bind({})
+Warning.args = {
+  variant: 'warning',
+}
+export const WarningSecondary = Template.bind({})
+WarningSecondary.args = {
+  variant: 'warning-secondary',
+}

+ 1 - 1
src/shared/components/IconButton/IconButton.tsx

@@ -4,7 +4,7 @@ import { StyledButtonBase } from './IconButton.style'
 
 import { ButtonBaseProps } from '../ButtonBase'
 
-export type IconButtonProps = ButtonBaseProps & {
+export type IconButtonProps = Omit<ButtonBaseProps, 'textOnly'> & {
   children: ReactNode
 }
 

+ 1 - 1
src/views/studio/EditVideoSheet/EditVideoForm/EditVideoForm.tsx

@@ -618,7 +618,7 @@ export const EditVideoForm: React.FC<EditVideoFormProps> = ({
           </FormField>
           {isEdit && (
             <DeleteVideoContainer>
-              <DeleteVideoButton size="large" variant="tertiary" textColorVariant="error" onClick={handleDeleteVideo}>
+              <DeleteVideoButton size="large" variant="destructive-secondary" onClick={handleDeleteVideo}>
                 Delete video
               </DeleteVideoButton>
             </DeleteVideoContainer>