浏览代码

Step component (#1198)

* create Stepper and remove FileStep

* refactoring

* cleaning

* update name

* CR fixes

* update storybook title
Bartosz Dryl 3 年之前
父节点
当前提交
1c23247f86

+ 1 - 43
src/components/Dialogs/Multistepper/Multistepper.style.ts

@@ -1,20 +1,10 @@
 import styled from '@emotion/styled'
 
-import { Text } from '@/shared/components/Text'
 import { SvgGlyphChevronRight } from '@/shared/icons'
-import { colors, media, sizes, typography } from '@/shared/theme'
+import { colors, media, sizes } from '@/shared/theme'
 
 import { BaseDialog } from '../BaseDialog'
 
-type CircleProps = {
-  isFilled?: boolean
-  isActive?: boolean
-}
-
-type StyledStepInfoProps = {
-  isActive?: boolean
-}
-
 export const StyledDialog = styled(BaseDialog)`
   max-width: 740px;
 `
@@ -51,40 +41,8 @@ export const StyledStepsInfoContainer = styled.div`
     width: 100%;
     grid-template-columns: repeat(6, auto);
     align-items: center;
-    grid-column-gap: ${sizes(4)};
   }
 `
-export const StyledStepInfo = styled.div<StyledStepInfoProps>`
-  display: ${({ isActive }) => (isActive ? 'flex' : 'none')};
-  align-items: center;
-
-  ${media.small} {
-    display: flex;
-  }
-`
-export const StyledCircle = styled.div<CircleProps>`
-  display: flex;
-  flex-shrink: 0;
-  justify-content: center;
-  align-items: center;
-  width: 32px;
-  height: 32px;
-  border-radius: 100%;
-  background-color: ${({ isActive }) => (isActive ? colors.blue[500] : colors.gray[400])};
-  color: ${colors.gray[50]};
-`
-export const StyledStepInfoText = styled.div<StyledStepInfoProps>`
-  display: flex;
-  flex-direction: column;
-  flex-grow: 1;
-  justify-content: center;
-  font-weight: ${typography.weights.semibold};
-  margin-left: ${sizes(2)};
-`
-
-export const StyledStepTitle = styled(Text)`
-  margin-top: ${sizes(1)};
-`
 
 export const StyledChevron = styled(SvgGlyphChevronRight)`
   margin: 0 ${sizes(1)};

+ 4 - 24
src/components/Dialogs/Multistepper/Multistepper.tsx

@@ -1,18 +1,8 @@
 import React, { Fragment } from 'react'
 
-import { Text } from '@/shared/components/Text'
-import { SvgGlyphCheck } from '@/shared/icons'
+import { Step } from '@/shared/components/Step'
 
-import {
-  StyledChevron,
-  StyledCircle,
-  StyledDialog,
-  StyledHeader,
-  StyledStepInfo,
-  StyledStepInfoText,
-  StyledStepTitle,
-  StyledStepsInfoContainer,
-} from './Multistepper.style'
+import { StyledChevron, StyledDialog, StyledHeader, StyledStepsInfoContainer } from './Multistepper.style'
 
 import { BaseDialogProps } from '../BaseDialog'
 
@@ -38,18 +28,8 @@ export const Multistepper: React.FC<MultistepperProps> = ({ steps, currentStepId
 
             return (
               <Fragment key={idx}>
-                <StyledStepInfo isActive={isActive}>
-                  <StyledCircle isFilled={isActive || isCompleted} isActive={isActive}>
-                    {isCompleted ? <SvgGlyphCheck /> : idx + 1}
-                  </StyledCircle>
-                  <StyledStepInfoText isActive={isActive}>
-                    <Text variant="caption" secondary>
-                      Step {idx + 1}
-                    </Text>
-                    <StyledStepTitle variant="overhead">{step.title}</StyledStepTitle>
-                  </StyledStepInfoText>
-                </StyledStepInfo>
-                {isLast ? null : <StyledChevron />}
+                <Step title={step.title} number={idx + 1} active={isActive} completed={isCompleted} />
+                {!isLast && <StyledChevron />}
               </Fragment>
             )
           })}

+ 2 - 0
src/shared/components/CircularProgress/CircularProgress.style.tsx

@@ -20,6 +20,8 @@ const getStrokeColor = (variant?: TrailVariant) => {
 }
 
 export const SVG = styled.svg`
+  fill: none;
+
   /* needed when parent container has display: flex */
   width: 100%;
 `

+ 0 - 26
src/shared/components/FileStep/FileStep.stories.tsx

@@ -1,26 +0,0 @@
-import { Meta, Story } from '@storybook/react'
-import React from 'react'
-
-import { FileStep, FileStepProps } from './FileStep'
-
-export default {
-  title: 'Shared/F/FileStep',
-  component: FileStep,
-  argTypes: {
-    step: {
-      defaultValue: 'video',
-    },
-    overhead: {
-      defaultValue: 'Video File',
-    },
-    subtitle: {
-      defaultValue: 'Select Video File',
-    },
-  },
-} as Meta
-
-const Template: Story<FileStepProps> = (args) => {
-  return <FileStep {...args} />
-}
-
-export const Default = Template.bind({})

+ 0 - 93
src/shared/components/FileStep/FileStep.tsx

@@ -1,93 +0,0 @@
-import React, { useEffect, useState } from 'react'
-
-import { SvgGlyphFileVideo, SvgGlyphLock, SvgGlyphTrash } from '@/shared/icons'
-import { FileType } from '@/types/files'
-
-import {
-  FileName,
-  Overhead,
-  StepDetails,
-  StepNumber,
-  StepStatus,
-  StepWrapper,
-  StyledProgress,
-  Thumbnail,
-} from './FileStep.style'
-
-import { IconButton } from '../IconButton'
-
-export type FileStepProps = {
-  stepNumber: number
-  active: boolean
-  onDelete: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void
-  type: FileType
-  isFileSet?: boolean
-  thumbnailUrl?: string | null
-  onSelect?: (step: FileType) => void
-  disabled?: boolean
-  isLoading?: boolean
-}
-
-export const FileStep: React.FC<FileStepProps> = ({
-  stepNumber = 1,
-  active,
-  isFileSet,
-  type,
-  onDelete,
-  thumbnailUrl,
-  onSelect,
-  isLoading,
-  disabled,
-}) => {
-  const handleChangeStep = () => {
-    !disabled && onSelect?.(type)
-  }
-  const [circularProgress, setCircularProgress] = useState(0)
-
-  useEffect(() => {
-    if (!isLoading) {
-      setCircularProgress(0)
-      return
-    }
-    const timeout = setTimeout(() => {
-      setCircularProgress(circularProgress + 20)
-    }, 50)
-
-    return () => clearTimeout(timeout)
-  }, [circularProgress, isLoading])
-
-  const stepSubtitle =
-    type === 'video'
-      ? isFileSet
-        ? 'Video file'
-        : 'Add video file'
-      : isFileSet
-      ? 'Thumbnail image'
-      : 'Add thumbnail image'
-
-  return (
-    <StepWrapper aria-disabled={disabled} active={active} onClick={handleChangeStep}>
-      <StepStatus>
-        {!isFileSet && <StepNumber active={active}>{stepNumber}</StepNumber>}
-        {isFileSet &&
-          (isLoading ? (
-            <StyledProgress value={circularProgress} maxValue={100} />
-          ) : (
-            <Thumbnail>
-              {type === 'video' && <SvgGlyphFileVideo />}
-              {type === 'image' && thumbnailUrl && <img src={thumbnailUrl} alt="thumbnail" />}
-            </Thumbnail>
-          ))}
-        <StepDetails>
-          <Overhead variant="overhead">Step {stepNumber}</Overhead>
-          <FileName variant="subtitle2">{stepSubtitle}</FileName>
-        </StepDetails>
-      </StepStatus>
-      {isFileSet && (
-        <IconButton variant="tertiary" disabled={disabled} onClick={onDelete}>
-          {disabled ? <SvgGlyphLock /> : <SvgGlyphTrash />}
-        </IconButton>
-      )}
-    </StepWrapper>
-  )
-}

+ 0 - 1
src/shared/components/FileStep/index.ts

@@ -1 +0,0 @@
-export * from './FileStep'

+ 18 - 13
src/shared/components/MultiFileSelect/MultiFileSelect.tsx

@@ -2,7 +2,7 @@ import React, { useEffect, useRef, useState } from 'react'
 import { FileRejection } from 'react-dropzone'
 
 import { ImageCropDialog, ImageCropDialogImperativeHandle } from '@/components/Dialogs/ImageCropDialog'
-import { SvgGlyphChevronRight } from '@/shared/icons'
+import { SvgGlyphChevronRight, SvgGlyphFileVideo } from '@/shared/icons'
 import { AssetDimensions, ImageCropData } from '@/types/cropper'
 import { FileType } from '@/types/files'
 import { validateImage } from '@/utils/image'
@@ -11,7 +11,7 @@ import { getVideoMetadata } from '@/utils/video'
 import { MultiFileSelectContainer, StepDivider, StepsContainer } from './MultiFileSelect.style'
 
 import { FileSelect } from '../FileSelect'
-import { FileStep } from '../FileStep'
+import { Step } from '../Step'
 
 type InputFile = {
   url?: string | null
@@ -203,27 +203,32 @@ export const MultiFileSelect: React.FC<MultiFileSelectProps> = ({
         error={error}
       />
       <StepsContainer>
-        <FileStep
-          stepNumber={1}
+        <Step
+          variant="file"
+          number={1}
+          title={files.video ? 'Video file' : 'Add video file'}
           active={step === 'video'}
-          isFileSet={!!files.video}
+          stepPlaceholder={!!files.video && <SvgGlyphFileVideo />}
           disabled={editMode}
-          type="video"
+          completed={!!files.video}
           onDelete={() => handleDeleteFile('video')}
-          onSelect={handleChangeStep}
+          onClick={() => handleChangeStep('video')}
           isLoading={isLoading}
         />
         <StepDivider>
           <SvgGlyphChevronRight />
         </StepDivider>
-        <FileStep
-          stepNumber={2}
+        <Step
+          variant="file"
+          stepPlaceholder={files.thumbnail?.url ? <img src={files.thumbnail?.url} alt="thumbnail" /> : undefined}
+          number={2}
+          title={files.thumbnail ? 'Thumbnail image' : 'Add thumbnail image'}
           active={step === 'image'}
-          isFileSet={!!files.thumbnail?.url}
-          type="image"
-          onDelete={() => handleDeleteFile('image')}
-          onSelect={handleChangeStep}
           thumbnailUrl={files.thumbnail?.url}
+          disabled={editMode}
+          completed={!!files.thumbnail?.url}
+          onDelete={() => handleDeleteFile('image')}
+          onClick={() => handleChangeStep('image')}
         />
       </StepsContainer>
       <ImageCropDialog ref={dialogRef} imageType="videoThumbnail" onConfirm={updateThumbnailFile} />

+ 23 - 0
src/shared/components/Step/Step.stories.tsx

@@ -0,0 +1,23 @@
+import { Meta, Story } from '@storybook/react'
+import React from 'react'
+
+import { Step, StepProps } from './Step'
+
+export default {
+  title: 'Shared/S/Step',
+  component: Step,
+  args: {
+    number: 1,
+    title: 'Step title',
+    thumbnailUrl: 'https://eu-central-1.linodeobjects.com/atlas-assets/cover-video/thumbnail.jpg',
+  },
+} as Meta
+
+const Template: Story<StepProps> = (args) => <Step {...args} />
+
+export const Default = Template.bind({})
+
+export const File = Template.bind({})
+File.args = {
+  variant: 'file',
+}

+ 39 - 17
src/shared/components/FileStep/FileStep.style.ts → src/shared/components/Step/Step.styles.ts

@@ -1,31 +1,56 @@
+import { css } from '@emotion/react'
 import styled from '@emotion/styled'
 
-import { colors, sizes, transitions, typography } from '@/shared/theme'
+import { colors, media, sizes, transitions, typography } from '@/shared/theme'
 
 import { CircularProgress } from '../CircularProgress'
 import { Text } from '../Text'
 
-type StepProps = {
+type StepWrapperProps = {
   active?: boolean
   disabled?: boolean
+  variant?: 'file' | 'default'
 }
 
-export const StepWrapper = styled.div<StepProps>`
+const stepperVariantStyles = (variant: 'file' | 'default', active?: boolean) => {
+  switch (variant) {
+    case 'default':
+      return css`
+        padding: 0;
+        display: ${active ? 'flex' : 'none'};
+        align-items: center;
+
+        ${media.small} {
+          display: flex;
+        }
+      `
+    case 'file':
+      return css`
+        padding: ${sizes(3)} ${sizes(4)};
+        cursor: pointer;
+        border: 1px solid ${active ? colors.blue[500] : colors.gray[500]};
+        background-color: ${active ? colors.transparentPrimary[10] : 'none'};
+
+        :hover:not([aria-disabled='true']) {
+          background-color: ${colors.transparentPrimary[18]};
+        }
+      `
+    default:
+      return
+  }
+}
+
+export const StepWrapper = styled.div<StepWrapperProps>`
   height: ${sizes(14)};
   padding: ${sizes(3)} ${sizes(4)};
   width: 100%;
   display: flex;
   justify-content: space-between;
   align-items: center;
-  border: 1px solid ${({ active }) => (active ? colors.blue[500] : colors.gray[600])};
   transition: border ${transitions.timings.routing} ${transitions.easing},
     background-color ${transitions.timings.routing} ${transitions.easing};
-  cursor: pointer;
-  background-color: ${({ active }) => (active ? 'rgba(180, 187, 255, 0.06)' : 'none')};
 
-  :hover:not([aria-disabled='true']) {
-    background-color: rgba(180, 187, 255, 0.12);
-  }
+  ${({ variant = 'default', active }) => stepperVariantStyles(variant, active)};
 
   &[aria-disabled='true'] {
     opacity: 0.6;
@@ -41,8 +66,8 @@ export const StepStatus = styled.div`
   position: relative;
 `
 
-export const StepNumber = styled.div<StepProps>`
-  background-color: ${({ active }) => (active ? colors.blue[500] : colors.gray[600])};
+export const StepNumber = styled.div<StepWrapperProps>`
+  background-color: ${({ active }) => (active ? colors.blue[500] : colors.gray[500])};
   font-size: ${typography.sizes.subtitle2};
   color: ${colors.white};
   border-radius: 100%;
@@ -62,15 +87,12 @@ export const StepDetails = styled.div`
 
 export const Overhead = styled(Text)`
   display: block;
-  color: ${colors.gray[300]};
-  font-weight: ${typography.weights.regular};
   overflow: hidden;
 `
 
-export const FileName = styled(Text)`
+export const StepTitle = styled(Text)`
   display: block;
-  font-family: ${typography.fonts.headers};
-  font-size: ${typography.sizes.caption};
+  margin-top: ${sizes(1)};
   height: 100%;
   white-space: nowrap;
   overflow: hidden;
@@ -82,7 +104,7 @@ export const StyledProgress = styled(CircularProgress)`
   height: ${sizes(7)};
 `
 
-export const Thumbnail = styled.div`
+export const StepImage = styled.div`
   flex-shrink: 0;
   color: white;
   background: ${colors.gray[600]};

+ 81 - 0
src/shared/components/Step/Step.tsx

@@ -0,0 +1,81 @@
+import React, { useEffect, useState } from 'react'
+
+import { SvgGlyphCheck, SvgGlyphLock, SvgGlyphTrash } from '@/shared/icons'
+
+import {
+  Overhead,
+  StepDetails,
+  StepImage,
+  StepNumber,
+  StepStatus,
+  StepTitle,
+  StepWrapper,
+  StyledProgress,
+} from './Step.styles'
+
+import { IconButton } from '../IconButton'
+
+export type StepProps = {
+  title: string
+  variant?: 'file' | 'default'
+  completed?: boolean
+  thumbnailUrl?: string | null
+  isLoading?: boolean
+  disabled?: boolean
+  active?: boolean
+  number?: number
+  stepPlaceholder?: React.ReactNode
+  onClick?: () => void
+  onDelete?: () => void
+}
+export const Step: React.FC<StepProps> = ({
+  variant = 'default',
+  isLoading,
+  disabled,
+  active,
+  completed,
+  onClick,
+  title,
+  number,
+  stepPlaceholder,
+  onDelete,
+}) => {
+  const [circularProgress, setCircularProgress] = useState(0)
+
+  useEffect(() => {
+    if (!isLoading) {
+      setCircularProgress(0)
+      return
+    }
+    const timeout = setTimeout(() => {
+      setCircularProgress(circularProgress + 20)
+    }, 50)
+
+    return () => clearTimeout(timeout)
+  }, [circularProgress, isLoading])
+
+  return (
+    <StepWrapper aria-disabled={disabled} active={active} onClick={() => !disabled && onClick?.()} variant={variant}>
+      <StepStatus>
+        {isLoading ? (
+          <StyledProgress value={circularProgress} maxValue={100} />
+        ) : stepPlaceholder ? (
+          <StepImage>{stepPlaceholder}</StepImage>
+        ) : (
+          <StepNumber active={active}>{completed ? <SvgGlyphCheck /> : number}</StepNumber>
+        )}
+        <StepDetails>
+          <Overhead variant="caption" secondary>
+            Step {number}
+          </Overhead>
+          <StepTitle variant="overhead">{title}</StepTitle>
+        </StepDetails>
+      </StepStatus>
+      {onDelete && completed && !isLoading && (
+        <IconButton variant="tertiary" disabled={disabled} onClick={() => !disabled && onDelete()}>
+          {disabled ? <SvgGlyphLock /> : <SvgGlyphTrash />}
+        </IconButton>
+      )}
+    </StepWrapper>
+  )
+}

+ 1 - 0
src/shared/components/Step/index.ts

@@ -0,0 +1 @@
+export * from './Step'