Skip to content

Commit

Permalink
feat: New Component: Step indicator (#1047)
Browse files Browse the repository at this point in the history
  • Loading branch information
brandonlenz authored Apr 12, 2021
1 parent 87a591d commit d61988e
Show file tree
Hide file tree
Showing 6 changed files with 341 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React from 'react'
import { StepIndicatorStep } from '../StepIndicatorStep/StepIndicatorStep'
import { StepIndicator } from '../StepIndicator/StepIndicator'

export default {
title: 'Components/Step Indicator',
component: StepIndicator,
parameters: {
info: `
### USWDS 2.0 Step Indicator component
Source: https://designsystem.digital.gov/components/step-indicator/
Updates users on their progress through a multi-step process.
`,
},
}

export const defaultStepIndicator = (): React.ReactElement => (
<StepIndicator>
<StepIndicatorStep label="Personal information" status="complete" />
<StepIndicatorStep label="Household status" status="complete" />
<StepIndicatorStep label="Supporting documents" status="current" />
<StepIndicatorStep label="Signature" />
<StepIndicatorStep label="Review and submit" />
</StepIndicator>
)

export const noLabels = (): React.ReactElement => (
<StepIndicator showLabels={false}>
<StepIndicatorStep label="Personal information" status="complete" />
<StepIndicatorStep label="Household status" status="complete" />
<StepIndicatorStep label="Supporting documents" status="current" />
<StepIndicatorStep label="Signature" />
<StepIndicatorStep label="Review and submit" />
</StepIndicator>
)

export const centered = (): React.ReactElement => (
<StepIndicator centered>
<StepIndicatorStep label="Personal information" status="complete" />
<StepIndicatorStep label="Household status" status="complete" />
<StepIndicatorStep label="Supporting documents" status="current" />
<StepIndicatorStep label="Signature" />
<StepIndicatorStep label="Review and submit" />
</StepIndicator>
)

export const counters = (): React.ReactElement => (
<StepIndicator counters="default">
<StepIndicatorStep label="Personal information" status="complete" />
<StepIndicatorStep label="Household status" status="complete" />
<StepIndicatorStep label="Supporting documents" status="current" />
<StepIndicatorStep label="Signature" />
<StepIndicatorStep label="Review and submit" />
</StepIndicator>
)

export const smallCounters = (): React.ReactElement => (
<StepIndicator counters="small">
<StepIndicatorStep label="Personal information" status="complete" />
<StepIndicatorStep label="Household status" status="complete" />
<StepIndicatorStep label="Supporting documents" status="current" />
<StepIndicatorStep label="Signature" />
<StepIndicatorStep label="Review and submit" />
</StepIndicator>
)
113 changes: 113 additions & 0 deletions src/components/stepindicator/StepIndicator/StepIndicator.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import React from 'react'
import { render } from '@testing-library/react'
import { StepIndicatorStep } from '../StepIndicatorStep/StepIndicatorStep'
import { StepIndicator } from '../StepIndicator/StepIndicator'

const step1 = 'Step 1'
const step2 = 'Step 2'
const step3 = 'Step 3'

describe('StepIndicator component', () => {
it('renders without errors', () => {
const { getByRole, queryByText, queryAllByText, queryByTestId } = render(
<StepIndicator>
<StepIndicatorStep label={step1} status="complete" />
<StepIndicatorStep label={step2} status="current" />
<StepIndicatorStep label={step3} status="incomplete" />
</StepIndicator>
)

const stepIndicator = queryByTestId('step-indicator')

expect(stepIndicator).toBeInTheDocument()
expect(stepIndicator).toHaveClass('usa-step-indicator')
expect(queryByText(step1)).toBeInTheDocument()
expect(queryAllByText(step2)).toHaveLength(2)
expect(queryByText(step3)).toBeInTheDocument()
expect(getByRole('list')).toHaveClass('usa-step-indicator__segments')
})

it('renders properly with no labels', () => {
const { getByRole, queryByText, queryAllByText, queryByTestId } = render(
<StepIndicator showLabels={false}>
<StepIndicatorStep label={step1} status="complete" />
<StepIndicatorStep label={step2} status="current" />
<StepIndicatorStep label={step3} status="incomplete" />
</StepIndicator>
)

const stepIndicator = queryByTestId('step-indicator')

expect(stepIndicator).toBeInTheDocument()
expect(stepIndicator).toHaveClass(
'usa-step-indicator usa-step-indicator--no-labels'
)
expect(queryByText(step1)).toBeInTheDocument()
expect(queryAllByText(step2)).toHaveLength(2)
expect(queryByText(step3)).toBeInTheDocument()
expect(getByRole('list')).toHaveClass('usa-step-indicator__segments')
})

it('renders properly with counters', () => {
const { getByRole, queryByText, queryAllByText, queryByTestId } = render(
<StepIndicator counters="default">
<StepIndicatorStep label={step1} status="complete" />
<StepIndicatorStep label={step2} status="current" />
<StepIndicatorStep label={step3} status="incomplete" />
</StepIndicator>
)

const stepIndicator = queryByTestId('step-indicator')

expect(stepIndicator).toBeInTheDocument()
expect(stepIndicator).toHaveClass(
'usa-step-indicator usa-step-indicator--counters'
)
expect(queryByText(step1)).toBeInTheDocument()
expect(queryAllByText(step2)).toHaveLength(2)
expect(queryByText(step3)).toBeInTheDocument()
expect(getByRole('list')).toHaveClass('usa-step-indicator__segments')
})

it('renders properly with small counters', () => {
const { getByRole, queryByText, queryAllByText, queryByTestId } = render(
<StepIndicator counters="small">
<StepIndicatorStep label={step1} status="complete" />
<StepIndicatorStep label={step2} status="current" />
<StepIndicatorStep label={step3} status="incomplete" />
</StepIndicator>
)

const stepIndicator = queryByTestId('step-indicator')

expect(stepIndicator).toBeInTheDocument()
expect(stepIndicator).toHaveClass(
'usa-step-indicator usa-step-indicator--counters-sm'
)
expect(queryByText(step1)).toBeInTheDocument()
expect(queryAllByText(step2)).toHaveLength(2)
expect(queryByText(step3)).toBeInTheDocument()
expect(getByRole('list')).toHaveClass('usa-step-indicator__segments')
})

it('renders properly with centered labels', () => {
const { getByRole, queryByText, queryAllByText, queryByTestId } = render(
<StepIndicator centered>
<StepIndicatorStep label={step1} status="complete" />
<StepIndicatorStep label={step2} status="current" />
<StepIndicatorStep label={step3} status="incomplete" />
</StepIndicator>
)

const stepIndicator = queryByTestId('step-indicator')

expect(stepIndicator).toBeInTheDocument()
expect(stepIndicator).toHaveClass(
'usa-step-indicator usa-step-indicator--center'
)
expect(queryByText(step1)).toBeInTheDocument()
expect(queryAllByText(step2)).toHaveLength(2)
expect(queryByText(step3)).toBeInTheDocument()
expect(getByRole('list')).toHaveClass('usa-step-indicator__segments')
})
})
74 changes: 74 additions & 0 deletions src/components/stepindicator/StepIndicator/StepIndicator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import React from 'react'
import classnames from 'classnames'
import { StepIndicatorStepProps } from '../StepIndicatorStep/StepIndicatorStep'

interface StepIndicatorProps {
showLabels?: boolean
counters?: 'none' | 'default' | 'small'
centered?: boolean
children: React.ReactElement<StepIndicatorStepProps>[]
className?: string
divProps?: JSX.IntrinsicElements['div']
listProps?: JSX.IntrinsicElements['ol']
}
export const StepIndicator = (
props: StepIndicatorProps
): React.ReactElement => {
const {
showLabels = true,
counters = 'none',
centered = false,
children,
className,
divProps,
listProps,
} = props

const classes = classnames(
'usa-step-indicator',
{
'usa-step-indicator--no-labels': !showLabels,
'usa-step-indicator--counters': counters === 'default',
'usa-step-indicator--counters-sm': counters === 'small',
'usa-step-indicator--center': centered,
},
className
)

const findCurrentStepIndex = (): number => {
const i = children.findIndex((step) => step.props.status === 'current')
return i === -1 ? 0 : i
}
const currentStepIndex = findCurrentStepIndex()
const currentStepNumber = currentStepIndex + 1
const currentStepLabel = children[parseInt(`${currentStepIndex}`)].props.label
const totalNumberOfSteps = children.length

return (
<div
className={classes}
data-testid="step-indicator"
aria-label="progress"
{...divProps}>
<ol className="usa-step-indicator__segments" {...listProps}>
{children}
</ol>
<div className="usa-step-indicator__header">
<h2 className="usa-step-indicator__heading">
<span className="usa-step-indicator__heading-counter">
<span className="usa-sr-only">Step</span>
<span className="usa-step-indicator__current-step">
{currentStepNumber}
</span>
&nbsp;
<span className="usa-step-indicator__total-steps">{`of ${totalNumberOfSteps}`}</span>
&nbsp;
</span>
<span className="usa-step-indicator__heading-text">
{currentStepLabel}
</span>
</h2>
</div>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React from 'react'
import { render } from '@testing-library/react'
import { StepIndicatorStep } from './StepIndicatorStep'

describe('Step component', () => {
it('renders without errors', () => {
const { getByRole, queryByText } = render(
<StepIndicatorStep label="Test Step" />
)
expect(queryByText('Test Step')).toBeInTheDocument()
expect(getByRole('listitem')).toHaveClass('usa-step-indicator__segment')
})

it('renders with incomplete status', () => {
const { getByRole, queryByText } = render(
<StepIndicatorStep label="Test Step" status="incomplete" />
)
expect(queryByText('Test Step')).toBeInTheDocument()
expect(queryByText('not completed')).toHaveClass('usa-sr-only')
expect(getByRole('listitem')).toHaveClass('usa-step-indicator__segment')
})

it('renders with current status', () => {
const { getByRole, queryByText } = render(
<StepIndicatorStep label="Test Step" status="current" />
)
expect(queryByText('Test Step')).toBeInTheDocument()
expect(getByRole('listitem')).toHaveClass(
'usa-step-indicator__segment usa-step-indicator__segment--current'
)
})

it('renders with complete status', () => {
const { getByRole, queryByText } = render(
<StepIndicatorStep label="Test Step" status="complete" />
)
expect(queryByText('Test Step')).toBeInTheDocument()
expect(queryByText('completed')).toHaveClass('usa-sr-only')
expect(getByRole('listitem')).toHaveClass(
'usa-step-indicator__segment usa-step-indicator__segment--complete'
)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import classnames from 'classnames'
import React from 'react'

export interface StepIndicatorStepProps {
label: string
status?: 'complete' | 'current' | 'incomplete'
className?: string
}

export const StepIndicatorStep = (
props: StepIndicatorStepProps & JSX.IntrinsicElements['li']
): React.ReactElement => {
const { label, status = 'incomplete', className, ...liProps } = props

const classes = classnames(
'usa-step-indicator__segment',
{
'usa-step-indicator__segment--complete': status === 'complete',
'usa-step-indicator__segment--current': status === 'current',
},
className
)

return (
<li
className={classes}
aria-current={status === 'current' ? 'true' : undefined}
{...liProps}>
<span className="usa-step-indicator__segment-label">
{label}
&nbsp;
{status !== 'current' && (
<span className="usa-sr-only">
{status === 'complete' ? 'completed' : 'not completed'}
</span>
)}
</span>
</li>
)
}
6 changes: 5 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export { FooterNav } from './components/Footer/FooterNav/FooterNav'
export { Logo } from './components/Footer/Logo/Logo'
export { SocialLinks } from './components/Footer/SocialLinks/SocialLinks'

/** Card Components */
/** Card components */
export { CardGroup } from './components/card/CardGroup/CardGroup'
export { Card } from './components/card/Card/Card'
export { CardHeader } from './components/card/CardHeader/CardHeader'
Expand All @@ -82,6 +82,10 @@ export { BreadcrumbBar } from './components/breadcrumb/BreadcrumbBar/BreadcrumbB
export { Breadcrumb } from './components/breadcrumb/Breadcrumb/Breadcrumb'
export { BreadcrumbLink } from './components/breadcrumb/BreadcrumbLink/BreadcrumbLink'

/** StepIndicator components */
export { StepIndicator } from './components/stepindicator/StepIndicator/StepIndicator'
export { StepIndicatorStep } from './components/stepindicator/StepIndicatorStep/StepIndicatorStep'

export { Search } from './components/Search/Search'

/** Truss-designed components */
Expand Down

0 comments on commit d61988e

Please sign in to comment.