diff --git a/src/components/Stepper/Stepper.stories.tsx b/src/components/Stepper/Stepper.stories.tsx new file mode 100644 index 000000000..d650c7d33 --- /dev/null +++ b/src/components/Stepper/Stepper.stories.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; + +import Stepper from './index'; + +const meta = { + title: 'Pending Review/Stepper', + component: Stepper, + argTypes: { + children: { control: false }, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +const steps = [ + { id: 1, label: 'Overview' }, + { id: 2, label: 'Targeting' }, + { id: 3, label: 'Measure' }, +]; + +const DefaultStepper = () => { + const [activeStep, setActiveStep] = React.useState(1); + return ( + + {steps.map((step, index) => ( + setActiveStep(step.id)} + /> + ))} + + ); +}; + +export const Default: Story = { + args: { + children: ( + <> + + + ), + }, + + render: () => , +}; diff --git a/src/components/Stepper/index.d.ts b/src/components/Stepper/index.d.ts new file mode 100644 index 000000000..f8cbacb26 --- /dev/null +++ b/src/components/Stepper/index.d.ts @@ -0,0 +1,29 @@ +import * as React from 'react'; + +export type StepId = string | number; +export type StepLabel = string | React.ReactNode; + +export interface StepperStepProps { + id: StepId; + onClick?: (...args: any[]) => any; + label?: StepLabel; + disabled?: boolean; + isActive?: boolean; + isCompleted?: boolean; + children?: React.ReactNode; + className?: string; + stepIndex?: number; +} + +declare const StepperStep: React.FC; + +export interface StepperProps { + children: React.ReactNode; + dts?: string; +} + +declare const Stepper: React.FC & { + Step: typeof StepperStep; +}; + +export default Stepper; diff --git a/src/components/Stepper/index.jsx b/src/components/Stepper/index.jsx new file mode 100644 index 000000000..9cfb68157 --- /dev/null +++ b/src/components/Stepper/index.jsx @@ -0,0 +1,80 @@ +import React from 'react'; +import _ from 'lodash'; +import classnames from 'classnames'; +import PropTypes from 'prop-types'; +import { expandDts } from '../../utils'; +import './styles.css'; + +const StepperStep = ({ + id, + label = '', + children, + disabled = false, + stepIndex, + onClick, + + isActive, + isCompleted, + className, +}) => { + const classNames = classnames( + 'aui--step', + { + active: isActive, + completed: isCompleted, + disabled: disabled, + }, + className + ); + + return ( +
(disabled ? _.noop : onClick(id))}> +
{children ? children : stepIndex + 1}
+ {!!label &&

{label}

} +
+ ); +}; + +StepperStep.propTypes = { + id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), + onClick: PropTypes.func, + disabled: PropTypes.bool, + isActive: PropTypes.bool, + isCompleted: PropTypes.bool, + children: PropTypes.node, + className: PropTypes.string, + stepIndex: PropTypes.number, +}; + +StepperStep.defaultProps = { + disabled: false, +}; + +const Stepper = ({ children, dts, size = 'medium' }) => { + return ( +
+ {React.Children.map(children, (child, index) => { + return React.cloneElement(child, { + key: index, + stepIndex: index, + ...child.props, + }); + })} +
+ ); +}; +export const sizes = ['small', 'medium', 'large']; + +Stepper.propTypes = { + children: PropTypes.node.isRequired, + dts: PropTypes.string, + /** + * Controls the width for the stepper. + */ + size: PropTypes.oneOf(sizes), +}; + +Stepper.Step = StepperStep; + +export default Stepper; diff --git a/src/components/Stepper/styles.css b/src/components/Stepper/styles.css new file mode 100644 index 000000000..ef91b1311 --- /dev/null +++ b/src/components/Stepper/styles.css @@ -0,0 +1,68 @@ +.aui--stepper { + display: flex; + width: 400px; + + /* &.aui-large { + width: 100%; + } */ + + /* + &.aui-small { + width: calc(100% / 3); + } */ +} + +.aui--step { + position: relative; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + /* width: 100%; + */ + flex: 1; +} + +.aui--step:not(:first-child)::before { + content: ''; + background-color: #cbd5e0; + position: absolute; + width: 100%; + height: 2px; + right: 50%; + + /* half of step height */ + top: 20px; + transform: translateY(-50%); +} + +/* TODO: deal with icon fill color */ +.aui--step-item { + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + z-index: 10; + position: relative; + border-radius: 9999px; + font-weight: 600; + color: #1c1d1f; + background-color: #ebeef2; + cursor: pointer; + + /* todo: box-shadow */ + + /* hover? */ +} + +.active .aui--step-item { + background-color: #1c1d1f; + color: #fff; +} + +.completed:not(:first-child)::before, +.active:not(:first-child)::before { + background-color: #0b0c0c; +}