From ee08d970e223b0bfbfc045546eec136f34a92202 Mon Sep 17 00:00:00 2001 From: Ricardo Figueroa Date: Tue, 2 Feb 2021 16:10:16 -0600 Subject: [PATCH] feat(component): add Stepper component --- .../src/components/Stepper/Step/Step.tsx | 52 ++ .../src/components/Stepper/Step/index.ts | 1 + .../src/components/Stepper/Step/styled.tsx | 60 +++ .../src/components/Stepper/Stepper.tsx | 35 ++ .../Stepper/__snapshots__/spec.tsx.snap | 459 ++++++++++++++++++ .../src/components/Stepper/index.ts | 4 + .../src/components/Stepper/spec.tsx | 20 + packages/big-design/src/components/index.ts | 1 + packages/docs/PropTables/StepperPropTable.tsx | 21 + packages/docs/PropTables/index.ts | 1 + packages/docs/components/SideNav/SideNav.tsx | 3 + packages/docs/next.config.js | 1 + packages/docs/pages/Stepper/StepperPage.tsx | 42 ++ 13 files changed, 700 insertions(+) create mode 100644 packages/big-design/src/components/Stepper/Step/Step.tsx create mode 100644 packages/big-design/src/components/Stepper/Step/index.ts create mode 100644 packages/big-design/src/components/Stepper/Step/styled.tsx create mode 100644 packages/big-design/src/components/Stepper/Stepper.tsx create mode 100644 packages/big-design/src/components/Stepper/__snapshots__/spec.tsx.snap create mode 100644 packages/big-design/src/components/Stepper/index.ts create mode 100644 packages/big-design/src/components/Stepper/spec.tsx create mode 100644 packages/docs/PropTables/StepperPropTable.tsx create mode 100644 packages/docs/pages/Stepper/StepperPage.tsx diff --git a/packages/big-design/src/components/Stepper/Step/Step.tsx b/packages/big-design/src/components/Stepper/Step/Step.tsx new file mode 100644 index 000000000..22b7017a4 --- /dev/null +++ b/packages/big-design/src/components/Stepper/Step/Step.tsx @@ -0,0 +1,52 @@ +import { CheckIcon } from '@bigcommerce/big-design-icons'; +import React, { memo } from 'react'; + +import { StyledDash, StyledLight, StyledSrOnlyText, StyledStep, StyledText } from './styled'; + +interface StepProps { + index: number; + selectedIndex: number; + stepperLength: number; + text: string; +} + +export const Step: React.FC = memo(({ index, selectedIndex, stepperLength, text }) => { + const isComplete = selectedIndex > index; + const isSelected = selectedIndex === index; + const isActive = isSelected || isComplete; + + return ( + + + {isComplete ? ( + + ) : ( + // span is needed to preserve white space inside a flex container + + Step + {` ${index + 1} `} + of {stepperLength} + + )} + + + {text} + + ); +}); + +Step.displayName = 'Step'; diff --git a/packages/big-design/src/components/Stepper/Step/index.ts b/packages/big-design/src/components/Stepper/Step/index.ts new file mode 100644 index 000000000..f5929d435 --- /dev/null +++ b/packages/big-design/src/components/Stepper/Step/index.ts @@ -0,0 +1 @@ +export { Step } from './Step'; diff --git a/packages/big-design/src/components/Stepper/Step/styled.tsx b/packages/big-design/src/components/Stepper/Step/styled.tsx new file mode 100644 index 000000000..7be91ab02 --- /dev/null +++ b/packages/big-design/src/components/Stepper/Step/styled.tsx @@ -0,0 +1,60 @@ +import { theme as defaultTheme } from '@bigcommerce/big-design-theme'; +import { hideVisually } from 'polished'; +import styled from 'styled-components'; + +import { Box } from '../../Box'; +import { Flex } from '../../Flex'; +import { Grid } from '../../Grid'; +import { StyleableText } from '../../Typography/private'; + +export const StyledStep = styled(Grid)` + flex-basis: 100%; + gap: ${({ theme }) => theme.spacing.xSmall}; + grid-template-areas: + 'light dash' + 'text text'; + grid-template-columns: auto 1fr; + + ${({ theme }) => theme.breakpoints.tablet} { + grid-template-columns: ${({ theme }) => theme.spacing.xLarge} 1fr; + grid-template-rows: ${({ theme }) => theme.spacing.xLarge} 1fr; + max-width: ${({ theme }) => theme.helpers.remCalc(228)}; + min-width: ${({ theme }) => theme.helpers.remCalc(96)}; + } +`; + +export const StyledSrOnlyText = styled.span` + ${({ theme }) => theme.breakpoints.tablet} { + ${hideVisually()} + } +`; + +export const StyledLight = styled(Flex)` + border-radius: ${({ theme }) => theme.spacing.medium}; + color: ${({ theme }) => theme.colors.white}; + grid-area: light; + user-select: none; + + ${({ theme }) => theme.breakpoints.tablet} { + border-radius: ${({ theme }) => theme.borderRadius.circle}; + font-size: ${({ theme }) => theme.typography.fontSize.medium}; + line-height: ${({ theme }) => theme.lineHeight.medium}; + } +`; + +export const StyledDash = styled(Box)` + align-self: center; + height: ${({ theme }) => theme.helpers.remCalc(2)}; + grid-area: dash; + width: 100%; +`; + +export const StyledText = styled(StyleableText)` + grid-area: text; +`; + +StyledStep.defaultProps = { theme: defaultTheme }; +StyledSrOnlyText.defaultProps = { theme: defaultTheme }; +StyledLight.defaultProps = { theme: defaultTheme }; +StyledDash.defaultProps = { theme: defaultTheme }; +StyledText.defaultProps = { theme: defaultTheme }; diff --git a/packages/big-design/src/components/Stepper/Stepper.tsx b/packages/big-design/src/components/Stepper/Stepper.tsx new file mode 100644 index 000000000..aa7b9463d --- /dev/null +++ b/packages/big-design/src/components/Stepper/Stepper.tsx @@ -0,0 +1,35 @@ +import React, { HTMLAttributes, memo } from 'react'; + +import { Flex } from '../Flex'; + +import { Step } from './Step'; + +export interface StepperProps extends HTMLAttributes { + steps: Array; + currentStep: number; +} + +export const Stepper: React.FC = memo(({ className, style, steps, currentStep, ...props }) => ( + + {steps.map((text, index) => ( + + ))} + +)); + +Stepper.defaultProps = { + steps: [], + currentStep: 0, +}; + +Stepper.displayName = 'Stepper'; diff --git a/packages/big-design/src/components/Stepper/__snapshots__/spec.tsx.snap b/packages/big-design/src/components/Stepper/__snapshots__/spec.tsx.snap new file mode 100644 index 000000000..ff118b4e8 --- /dev/null +++ b/packages/big-design/src/components/Stepper/__snapshots__/spec.tsx.snap @@ -0,0 +1,459 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders Stepper 1`] = ` +.c0 { + margin-bottom: 1.5rem; + box-sizing: border-box; +} + +.c2 { + margin-bottom: 1.5rem; + box-sizing: border-box; +} + +.c5 { + box-sizing: border-box; + background-color: #3C64F4; +} + +.c9 { + box-sizing: border-box; + background-color: #3C64F4; +} + +.c12 { + margin-bottom: 1.5rem; + box-sizing: border-box; +} + +.c15 { + box-sizing: border-box; + background-color: #D9DCE9; +} + +.c16 { + box-sizing: border-box; + background-color: #8C93AD; +} + +.c17 { + box-sizing: border-box; + background-color: #D9DCE9; +} + +.c1 { + -webkit-align-content: stretch; + -ms-flex-line-pack: stretch; + align-content: stretch; + -webkit-align-items: stretch; + -webkit-box-align: stretch; + -ms-flex-align: stretch; + align-items: stretch; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-flex-wrap: nowrap; + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; +} + +.c6 { + -webkit-align-content: stretch; + -ms-flex-line-pack: stretch; + align-content: stretch; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-flex-wrap: nowrap; + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; +} + +.c8 { + vertical-align: middle; + color: #FFFFFF; + height: 1.25rem; + width: 1.25rem; +} + +.c3 { + grid-gap: 1rem; + display: grid; +} + +.c13 { + grid-gap: 1rem; + display: grid; +} + +.c4 { + -webkit-flex-basis: 100%; + -ms-flex-preferred-size: 100%; + flex-basis: 100%; + gap: 0.5rem; + grid-template-areas: 'light dash' 'text text'; + grid-template-columns: auto 1fr; +} + +.c7 { + border-radius: 1rem; + color: #FFFFFF; + grid-area: light; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.c10 { + -webkit-align-self: center; + -ms-flex-item-align: center; + align-self: center; + height: 0.125rem; + grid-area: dash; + width: 100%; +} + +.c11 { + color: #313440; + margin: 0 0 1rem; + font-size: 1rem; + font-weight: 400; + line-height: 1.5rem; + grid-area: text; +} + +.c11:last-child { + margin-bottom: 0; +} + +.c18 { + color: #8C93AD; + margin: 0 0 1rem; + font-size: 1rem; + font-weight: 400; + line-height: 1.5rem; + grid-area: text; +} + +.c18:last-child { + margin-bottom: 0; +} + +@media (min-width:0px) { + .c2 { + display: none; + } +} + +@media (min-width:720px) { + .c2 { + display: grid; + } +} + +@media (min-width:720px) { + .c2 { + margin-right: 0.5rem; + } +} + +@media (min-width:0px) { + .c5 { + padding-top: 0.25rem; + padding-bottom: 0.25rem; + } +} + +@media (min-width:720px) { + .c5 { + padding-top: 0; + padding-bottom: 0; + } +} + +@media (min-width:0px) { + .c5 { + padding-left: 0.5rem; + padding-right: 0.5rem; + } +} + +@media (min-width:720px) { + .c5 { + padding-left: 0; + padding-right: 0; + } +} + +@media (min-width:0px) { + .c9 { + display: none; + } +} + +@media (min-width:720px) { + .c9 { + display: block; + } +} + +@media (min-width:0px) { + .c12 { + display: grid; + } +} + +@media (min-width:720px) { + .c12 { + display: grid; + } +} + +@media (min-width:720px) { + .c12 { + margin-right: 0.5rem; + } +} + +@media (min-width:0px) { + .c15 { + display: none; + } +} + +@media (min-width:720px) { + .c15 { + display: block; + } +} + +@media (min-width:0px) { + .c16 { + padding-top: 0.25rem; + padding-bottom: 0.25rem; + } +} + +@media (min-width:720px) { + .c16 { + padding-top: 0; + padding-bottom: 0; + } +} + +@media (min-width:0px) { + .c16 { + padding-left: 0.5rem; + padding-right: 0.5rem; + } +} + +@media (min-width:720px) { + .c16 { + padding-left: 0; + padding-right: 0; + } +} + +@media (min-width:0px) { + .c17 { + display: none; + } +} + +@media (min-width:720px) { + .c17 { + display: none; + } +} + +@media (min-width:0px) { + .c3 { + display: none; + } +} + +@media (min-width:720px) { + .c3 { + display: grid; + } +} + +@media (min-width:0px) { + .c13 { + display: grid; + } +} + +@media (min-width:720px) { + .c13 { + display: grid; + } +} + +@media (min-width:720px) { + .c4 { + grid-template-columns: 1.5rem 1fr; + grid-template-rows: 1.5rem 1fr; + max-width: 14.25rem; + min-width: 6rem; + } +} + +@media (min-width:720px) { + .c14 { + border: 0; + -webkit-clip: rect(0 0 0 0); + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + white-space: nowrap; + width: 1px; + } +} + +@media (min-width:720px) { + .c7 { + border-radius: 50%; + font-size: 1rem; + line-height: 1.5rem; + } +} + +
+
+
+ + + + +
+
+

+ Step 1 +

+
+
+
+ + + Step + + 2 + + of + 3 + + +
+
+

+ Step 2 +

+
+
+
+ + + Step + + 3 + + of + 3 + + +
+
+

+ Step 3 +

+
+
+`; diff --git a/packages/big-design/src/components/Stepper/index.ts b/packages/big-design/src/components/Stepper/index.ts new file mode 100644 index 000000000..59ed9d095 --- /dev/null +++ b/packages/big-design/src/components/Stepper/index.ts @@ -0,0 +1,4 @@ +export { Stepper } from './Stepper'; +import { StepperProps as _StepperProps } from './Stepper'; + +export type StepperProps = _StepperProps; diff --git a/packages/big-design/src/components/Stepper/spec.tsx b/packages/big-design/src/components/Stepper/spec.tsx new file mode 100644 index 000000000..9af3af562 --- /dev/null +++ b/packages/big-design/src/components/Stepper/spec.tsx @@ -0,0 +1,20 @@ +import 'jest-styled-components'; +import { render } from '@testing-library/react'; +import React from 'react'; + +import { Stepper } from './'; + +const steps: string[] = ['Step 1', 'Step 2', 'Step 3']; + +test('renders Stepper', () => { + const { container } = render(); + expect(container.firstChild).toMatchSnapshot(); +}); + +test('does not forward styles', () => { + const { container } = render( + , + ); + expect(container.getElementsByClassName('test').length).toBe(0); + expect(container.firstChild).not.toHaveStyle('background: red'); +}); diff --git a/packages/big-design/src/components/index.ts b/packages/big-design/src/components/index.ts index 8c86658c8..3f8e9740e 100644 --- a/packages/big-design/src/components/index.ts +++ b/packages/big-design/src/components/index.ts @@ -28,6 +28,7 @@ export * from './Radio'; export * from './Select'; export * from './StatefulTable'; export * from './StatefulTree'; +export * from './Stepper'; export * from './Switch'; export * from './Table'; export * from './Tabs'; diff --git a/packages/docs/PropTables/StepperPropTable.tsx b/packages/docs/PropTables/StepperPropTable.tsx new file mode 100644 index 000000000..8fae7d1e6 --- /dev/null +++ b/packages/docs/PropTables/StepperPropTable.tsx @@ -0,0 +1,21 @@ +import React from 'react'; + +import { Prop, PropTable, PropTableWrapper } from '../components'; + +const stepperProps: Prop[] = [ + { + name: 'steps', + types: 'string[]', + required: true, + description: 'Titles for each segment of the Stepper.', + }, + { + name: 'currentStep', + types: 'number', + description: 'Sets which step is currently active. Starts at 0.', + }, +]; + +export const StepperPropTable: React.FC = (props) => ( + +); diff --git a/packages/docs/PropTables/index.ts b/packages/docs/PropTables/index.ts index 2bf1efb45..7e8f2bfde 100644 --- a/packages/docs/PropTables/index.ts +++ b/packages/docs/PropTables/index.ts @@ -28,6 +28,7 @@ export * from './RadioPropTable'; export * from './SelectPropTable'; export * from './StatefulTablePropTable'; export * from './StatefulTreePropTable'; +export * from './StepperPropTable'; export * from './SwitchPropTable'; export * from './TablePropTable'; export * from './TabsPropTable'; diff --git a/packages/docs/components/SideNav/SideNav.tsx b/packages/docs/components/SideNav/SideNav.tsx index d042f33b2..d23215f7a 100644 --- a/packages/docs/components/SideNav/SideNav.tsx +++ b/packages/docs/components/SideNav/SideNav.tsx @@ -140,6 +140,9 @@ export const SideNav: React.FC = () => { Progress Circle + + Stepper + Tooltip diff --git a/packages/docs/next.config.js b/packages/docs/next.config.js index e03d4666c..1d4d15ec9 100644 --- a/packages/docs/next.config.js +++ b/packages/docs/next.config.js @@ -57,6 +57,7 @@ module.exports = { '/spacing': { page: '/Spacing/SpacingPage' }, '/statefulTable': { page: '/StatefulTable/StatefulTablePage' }, '/statefulTree': { page: '/StatefulTree/StatefulTreePage' }, + '/stepper': { page: '/Stepper/StepperPage' }, '/switch': { page: '/Switch/SwitchPage' }, '/table': { page: '/Table/TablePage' }, '/tabs': { page: '/Tabs/TabsPage' }, diff --git a/packages/docs/pages/Stepper/StepperPage.tsx b/packages/docs/pages/Stepper/StepperPage.tsx new file mode 100644 index 000000000..ef5e39ecc --- /dev/null +++ b/packages/docs/pages/Stepper/StepperPage.tsx @@ -0,0 +1,42 @@ +import { Button, H0, H1, Stepper, Text } from '@bigcommerce/big-design'; +import React, { useState } from 'react'; + +import { Code, CodePreview } from '../../components'; +import { StepperPropTable } from '../../PropTables'; + +const StepperPage = () => ( + <> + Stepper + + + The Stepper component is used to display a set number of steps. Useful for guiding an user + through a multi-step operation. + + + + {/* jsx-to-string:start */} + {function Example() { + const steps = ['Step 1', 'Step 2', 'Step 3']; + const [currentStep, setCurrentStep] = useState(0); + + return ( + <> + + + + + ); + }} + {/* jsx-to-string:end */} + + +

API

+ + +); + +export default StepperPage;