Skip to content

Commit

Permalink
refactor(tabs,stepper): refactors to utilize basetabs
Browse files Browse the repository at this point in the history
  • Loading branch information
dzucconi committed Mar 9, 2021
1 parent 074c0a7 commit e9b1952
Show file tree
Hide file tree
Showing 12 changed files with 243 additions and 380 deletions.
18 changes: 10 additions & 8 deletions packages/palette/src/elements/BaseTabs/BaseTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@ import { Text, TextProps } from "../Text"
*/
export type BaseTabProps = TextProps & {
active?: boolean
} & (// FIXME: Should be able to remove this typing once styled-components is updated
// Default:
| {}
} & (
| // FIXME: Should be able to remove this typing once styled-components is updated
// Default:
{}
// When displaying as an anchor tag, accept anchor props:
| ({ as: "a" } & (React.AnchorHTMLAttributes<HTMLAnchorElement>))
| ({ as: "a" } & React.AnchorHTMLAttributes<HTMLAnchorElement>)
// Cop-out: if you pass as with a complex component, loosen up the types:
| ({ as: React.ReactElement<unknown> } & Record<string, unknown>))
| ({ as: React.ReactElement<unknown> } & Record<string, unknown>)
)

const BaseTabActiveMixin = css`
const baseTabActiveMixin = css`
outline: 0;
color: ${color("black100")};
border-bottom-color: ${color("black100")} !important;
Expand All @@ -38,10 +40,10 @@ export const BaseTab = styled(Text)<BaseTabProps>`
z-index: 1;
color: ${color("black60")};
${({ active }) => active && BaseTabActiveMixin}
${({ active }) => active && baseTabActiveMixin}
&:focus,
&.active {
${BaseTabActiveMixin}
${baseTabActiveMixin}
}
&:focus {
Expand Down
32 changes: 28 additions & 4 deletions packages/palette/src/elements/Stepper/Stepper.story.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { action } from "@storybook/addon-actions"
import React from "react"
import React, { useEffect, useState } from "react"
import { States } from "storybook-states"
import { Step, Stepper, StepperProps } from "./"

Expand All @@ -22,12 +22,36 @@ export const Default = () => {
{(props) => {
return (
<Stepper onChange={action("onChange")} {...props}>
<Step name="Review" />
<Step name="Confirm" />
<Step name="Pay" />
<Step name="Review">Review panel</Step>
<Step name="Confirm">Confirm panel</Step>
<Step name="Pay">Pay panel</Step>
</Stepper>
)
}}
</States>
)
}

export const ChangingCurrentStep = () => {
const [cursor, setCursor] = useState(0)

useEffect(() => {
const interval = setInterval(() => {
setCursor((prevCursor) => prevCursor + 1)
}, 1000)

return () => {
clearInterval(interval)
}
}, [])

const index = cursor % 3

return (
<Stepper initialTabIndex={index} currentStepIndex={index} disableNavigation>
<Step name="Review">Review panel</Step>
<Step name="Confirm">Confirm panel</Step>
<Step name="Pay">Pay panel</Step>
</Stepper>
)
}
127 changes: 46 additions & 81 deletions packages/palette/src/elements/Stepper/Stepper.tsx
Original file line number Diff line number Diff line change
@@ -1,105 +1,70 @@
import React from "react"
import styled from "styled-components"
import { space } from "../../helpers/space"
import { CheckIcon } from "../../svgs/CheckIcon"
import { ChevronIcon } from "../../svgs/ChevronIcon"
import { Box } from "../Box"
import { Flex } from "../Flex"
import { Tab, Tabs, TabsProps } from "../Tabs"
import { sharedTabsStyles } from "../Tabs"
import { Sans } from "../Typography"
import { TabsTab, TabsTabProps } from "../Tabs/TabsTab"

export interface StepperProps extends TabsProps {
/** The initial step stepper renders */
initialTabIndex?: number

/** The step user currently is at (e.g. previous steps completed) */
currentStepIndex: number

/** Prevents the tabs from being directly clickable */
disableNavigation?: boolean
}

/** Stepper */
export const Stepper = (props: StepperProps) => {
export const Stepper: React.FC<StepperProps> = ({
currentStepIndex,
disableNavigation,
...rest
}) => {
return (
<Tabs
// This key is required to ensure the tab state updates with
// the currentStepIndex change
key={props.currentStepIndex}
separator={
<ChevronWrapper>
<ChevronIcon fill="black30" width="12px" />
</ChevronWrapper>
}
transformTabBtn={transformTabBtn}
{...props}
separator={<ChevronIcon mx={2} fill="black30" width="12px" />}
Tab={({ children, ...tab }) => {
return (
<StepperTab
currentStepIndex={currentStepIndex}
disableNavigation={disableNavigation}
{...tab}
>
{children}
</StepperTab>
)
}}
{...rest}
/>
)
}

/** Step */
export const Step = (props) => <Tab {...props} />

const DisabledStepButton = ({ children }) => (
<DisabledStepContainer>
<Sans size="3t" color="black30">
{children}
</Sans>
</DisabledStepContainer>
)

const transformTabBtn = (
element: JSX.Element,
tabIndex: number,
props: any
): JSX.Element => {
const { currentStepIndex, initialTabIndex = 0, disableNavigation } = props
const returnDisabledButton = disableNavigation && tabIndex !== initialTabIndex

const disabledButton = (
<DisabledStepButton key={tabIndex}>
{element.props.children}
</DisabledStepButton>
)
export const Step = Tab

// Don't allow users to jump ahead
if (tabIndex > currentStepIndex) {
return disabledButton
const StepperTab: React.FC<
Pick<StepperProps, "currentStepIndex" | "disableNavigation"> & TabsTabProps
> = ({
children,
active,
currentStepIndex,
disableNavigation,
index,
...rest
}) => {
return (
<TabsTab
active={active}
disabled={disableNavigation || index > currentStepIndex}
index={index}
{...rest}
>
<Flex>
{currentStepIndex > index && <CheckIcon fill="green100" mr={1} />}

// Step done
} else if (currentStepIndex && tabIndex < currentStepIndex) {
return (
<Flex key={tabIndex}>
<CheckMarkWrapper>
<CheckIcon fill="green100" />
</CheckMarkWrapper>
{returnDisabledButton && tabIndex !== initialTabIndex
? disabledButton
: element}
<div /> {/* hack for getting rid of last-child in Tabs.tsx */}
<Box color={index > currentStepIndex ? "black30" : undefined}>
{children}
</Box>
</Flex>
)
// Disabled
} else if (returnDisabledButton) {
return disabledButton

// Step
} else {
return element
}
</TabsTab>
)
}

const ChevronWrapper = styled.span`
margin: 0 ${space(2)}px;
line-height: normal;
`

/** CheckMarkWrapper */
export const CheckMarkWrapper = styled.span`
margin-right: ${space(1)}px;
line-height: normal;
`

const DisabledStepContainer = styled.div`
${sharedTabsStyles.tabContainer};
cursor: default;
`
19 changes: 10 additions & 9 deletions packages/palette/src/elements/Stepper/__tests__/Stepper.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,13 @@ import { Step, Stepper } from "../Stepper"

describe("Stepper", () => {
const getWrapper = (props = {}) => {
const _props = {
initialTabIndex: 0,
currentStepIndex: 0,
disableNavigation: false,
...props,
}

return mount(
<Stepper {..._props}>
<Stepper
initialTabIndex={0}
currentStepIndex={0}
disableNavigation={false}
{...props}
>
<Step name="Review" />
<Step name="Confirm" />
<Step name="Pay" />
Expand All @@ -36,6 +34,9 @@ describe("Stepper", () => {
const wrapper = getWrapper({
disableNavigation: true,
})
expect(wrapper.find("DisabledStepButton").length).toBe(2)

expect(
wrapper.find("button").map((button) => button.prop("disabled"))
).toStrictEqual([true, true, true])
})
})
17 changes: 17 additions & 0 deletions packages/palette/src/elements/Tabs/Tab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from "react"

export interface TabProps {
/** Display name of the Tab */
name: string | JSX.Element
/**
* Arbitrary data that can be associated with a Tab.
* Will be passed to the parent <Tabs>'s onChange handler.
*/
data?: any
}

/**
* An individual tab.
* Does nothing on its own; props are deal with inside of Tabs.
*/
export const Tab: React.FC<TabProps> = ({ children }) => <>{children}</>

0 comments on commit e9b1952

Please sign in to comment.