Rewrite sequence component (#682)

* Rework sequence component to be more declarative and simple
* Renamed to SimpleStepper
This commit is contained in:
Wesley Yee
2020-05-05 15:27:35 -04:00
committed by GitHub
parent 1fae559054
commit ddff830b04
11 changed files with 498 additions and 379 deletions
@@ -0,0 +1,86 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { FC } from 'react';
import {
Step as MuiStep,
StepContent,
StepLabel,
Typography,
makeStyles,
} from '@material-ui/core';
import SimpleStepperFooter from './SimpleStepperFooter';
const useStyles = makeStyles((theme) => ({
end: {
padding: theme.spacing(3),
},
}));
export type StepActions = {
showNext?: boolean;
canNext?: () => boolean;
onNext?: () => void;
nextStep?: (current: number, last: number) => number;
nextText?: string;
showBack?: boolean;
backText?: string;
onBack?: () => void;
showRestart?: boolean;
canRestart?: () => boolean;
onRestart?: () => void;
restartText?: string;
};
export type StepProps = {
title: string;
children: React.ReactElement;
end?: boolean;
actions?: StepActions;
};
const Step: FC<StepProps> = ({
title,
children,
end,
actions,
...muiProps
}) => {
const classes = useStyles();
// The end step is not a part of the stepper
// It simply is the final screen with an option to have buttons such as reset or back
return end ? (
<div className={classes.end}>
<Typography variant="h6">{title}</Typography>
{children}
<SimpleStepperFooter actions={{ ...(actions || {}), showNext: false }} />
</div>
) : (
<MuiStep {...muiProps}>
<StepLabel>
<Typography variant="h6">{title}</Typography>
</StepLabel>
<StepContent>
{children}
<SimpleStepperFooter actions={actions} />
</StepContent>
</MuiStep>
);
};
export default Step;
@@ -1,81 +0,0 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useState } from 'react';
import { TextField } from '@material-ui/core';
import Sequence, { StepType } from '.';
export default {
title: 'Sequence',
component: Sequence,
};
const steps = [
{
title: 'Step 1!',
content: <div>Slide 1 content</div>,
},
{
title: 'Step 2!',
content: <div>Slide 2 content</div>,
},
{
title: 'Step 3!',
content: <div>Slide 3 content</div>,
},
];
export const Horizontal = () => (
<Sequence steps={steps} orientation="horizontal" />
);
export const Vertical = () => <Sequence steps={steps} orientation="vertical" />;
export const ConditionalButtons = () => {
const [required, setRequired] = useState(false);
const customSteps: StepType[] = [...steps];
customSteps[0] = {
title: 'Step 1 with required field',
actions: {
canNext: () => required,
},
content: (
<div>
<TextField
variant="outlined"
placeholder="Required*"
onChange={e => setRequired(!!e.target.value)}
/>
</div>
),
};
return <Sequence steps={customSteps} orientation="vertical" />;
};
export const CompletionStep = () => {
const completed = {
title: 'Succcess!',
content: <div>You've completed the sequence</div>,
};
return (
<Sequence
steps={steps.slice(0, 1)}
completed={completed}
orientation="vertical"
/>
);
};
@@ -1,107 +0,0 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import { wrapInTestApp } from '@backstage/test-utils';
import Sequence, { StepType } from './Sequence';
describe('Sequence', () => {
let steps: StepType[];
beforeEach(() => {
steps = [
{
title: 'Step 0',
content: <div>step0</div>,
},
{
title: 'Step 1',
content: <div>step1</div>,
actions: { nextStep: () => 3 },
},
{
title: 'Step 2',
content: <div>step2</div>,
},
{
title: 'Step 3',
content: <div>step3</div>,
},
];
});
it('Handles nextStep property', () => {
const rendered = render(
wrapInTestApp(<Sequence orientation="horizontal" steps={steps} />),
);
fireEvent.click(rendered.getByText('Next'));
expect(rendered.getByText('step1')).toBeInTheDocument();
fireEvent.click(rendered.getByText('Next'));
expect(rendered.getByText('step3')).toBeInTheDocument();
fireEvent.click(rendered.getByText('Back'));
expect(rendered.getByText('step1')).toBeInTheDocument();
});
it('Shows controls and content when going back to first step', () => {
const rendered = render(
wrapInTestApp(<Sequence orientation="horizontal" steps={steps} />),
);
fireEvent.click(rendered.getByText('Next'));
expect(rendered.getByText('step1')).toBeInTheDocument();
fireEvent.click(rendered.getByText('Next'));
expect(rendered.getByText('step3')).toBeInTheDocument();
fireEvent.click(rendered.getByText('Back'));
expect(rendered.getByText('step1')).toBeInTheDocument();
expect(rendered.getByText('Next')).toBeInTheDocument();
fireEvent.click(rendered.getByText('Back'));
expect(rendered.getByText('step0')).toBeInTheDocument();
expect(rendered.getByText('Next')).toBeInTheDocument();
});
it('uses nextText if specified in all steps', () => {
const nextTextSteps = [
{
title: 'Step 0',
actions: {
nextText: 'Step0Next',
},
content: <div>step0</div>,
},
{
title: 'Final Step',
actions: {
nextText: 'FinalStepNext',
},
content: <div>final step</div>,
},
];
const rendered = render(
wrapInTestApp(
<Sequence orientation="horizontal" steps={nextTextSteps} />,
),
);
expect(rendered.getByText('Step0Next')).toBeInTheDocument();
fireEvent.click(rendered.getByText('Step0Next'));
expect(rendered.getByText('FinalStepNext')).toBeInTheDocument();
});
});
@@ -1,156 +0,0 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { Fragment, FC, useState } from 'react';
import {
Stepper,
Step,
StepContent,
StepLabel,
Typography,
makeStyles,
} from '@material-ui/core';
import SequenceFooter from './SequenceFooter';
const useStyles = makeStyles((theme: any) => ({
content: {
padding: theme.spacing(4, 4, 4, 4),
},
completionBox: {
padding: theme.spacing(3),
},
}));
type Orientation = 'horizontal' | 'vertical';
export type StepActions = {
showNext?: boolean;
canNext?: () => boolean;
onNext?: () => void;
nextStep?: (current: number, last: number) => number;
nextText?: string;
showBack?: boolean;
backText?: string;
onBack?: () => void;
showRestart?: boolean;
canRestart?: () => boolean;
onRestart?: () => void;
restartText?: string;
completedButton?: JSX.Element;
};
export type StepType = {
title: string;
actions?: StepActions;
content: JSX.Element;
};
export interface SequenceProps {
completed?: StepType;
elevated?: boolean;
orientation: Orientation;
onSequenceStepChange?: (prevIndex: number, nextIndex: number) => void;
steps: StepType[];
footerProps?: any;
}
const Sequence: FC<SequenceProps> = ({
completed,
elevated,
orientation,
onSequenceStepChange,
steps,
footerProps,
}) => {
const classes = useStyles();
const [stepIndex, setStepIndex] = useState<number>(0);
const [stepArray, setStepArray] = useState<number[]>([0]);
const isVertical = orientation === 'vertical';
const isCompleted = !!completed && stepIndex === steps.length;
const horizontalStep = !isVertical && steps[stepIndex]?.content;
return (
<Fragment>
<Stepper
activeStep={stepIndex}
orientation={orientation}
elevation={elevated ? 2 : 0}
>
{steps.map((step, i) => (
<Step key={i}>
<StepLabel>
<Typography variant="h6">{step.title}</Typography>
</StepLabel>
{isVertical && (
<StepContent>
{step.content}
<SequenceFooter
stepIndex={i}
setStepIndex={setStepIndex}
stepArray={stepArray}
setStepArray={setStepArray}
length={steps.length}
onSequenceStepChange={onSequenceStepChange}
actions={step.actions || {}}
{...footerProps}
/>
</StepContent>
)}
</Step>
))}
</Stepper>
{isCompleted && (
<div key="completed" className={classes.completionBox}>
<Typography variant="h6"> {completed?.title} </Typography>
{completed?.content}
<SequenceFooter
stepIndex={stepIndex}
setStepIndex={setStepIndex}
stepArray={stepArray}
setStepArray={setStepArray}
length={steps.length}
actions={{ ...(completed?.actions || {}), showNext: false }}
{...footerProps}
>
{completed?.actions?.completedButton}
</SequenceFooter>
</div>
)}
{/* Horizontal steppers have buttons on the bottom of stepper */}
{!isVertical && (
<>
<div className={classes.content}>
{horizontalStep}
<SequenceFooter
stepIndex={stepIndex}
setStepIndex={setStepIndex}
stepArray={stepArray}
setStepArray={setStepArray}
length={steps.length}
onSequenceStepChange={onSequenceStepChange}
actions={steps[stepIndex]?.actions || {}}
{...footerProps}
/>
</div>
</>
)}
</Fragment>
);
};
export default Sequence;
@@ -0,0 +1,81 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useState } from 'react';
import { TextField } from '@material-ui/core';
import { SimpleStepper, SimpleStepperStep } from '.';
export default {
title: 'SimpleStepper',
component: SimpleStepper,
};
export const Default = () => (
<SimpleStepper>
<SimpleStepperStep title="Step 1">
<div>This is the content for step 1</div>
</SimpleStepperStep>
<SimpleStepperStep title="Step 2">
<div>This is the content for step 2</div>
</SimpleStepperStep>
<SimpleStepperStep title="Step 3">
<div>This is the content for step 3</div>
</SimpleStepperStep>
</SimpleStepper>
);
export const ConditionalButtons = () => {
const [required, setRequired] = useState(false);
return (
<SimpleStepper>
<SimpleStepperStep
title="Step 1 with required field"
actions={{
canNext: () => required,
}}
>
<TextField
variant="outlined"
placeholder="Required*"
onChange={(e) => setRequired(!!e.target.value)}
/>
</SimpleStepperStep>
<SimpleStepperStep title="Step 2">
<div>This is the content for step 2</div>
</SimpleStepperStep>
<SimpleStepperStep title="Step 3">
<div>This is the content for step 3</div>
</SimpleStepperStep>
</SimpleStepper>
);
};
export const CompletionStep = () => {
return (
<SimpleStepper>
<SimpleStepperStep title="Step 1">
<div>This is the content for step 1</div>
</SimpleStepperStep>
<SimpleStepperStep title="Step 2">
<div>This is the content for step 2</div>
</SimpleStepperStep>
<SimpleStepperStep title="Success!" end>
<div>You've completed the Stepper</div>
</SimpleStepperStep>
</SimpleStepper>
);
};
@@ -0,0 +1,129 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { render, fireEvent, within } from '@testing-library/react';
import { wrapInTestApp } from '@backstage/test-utils';
import Stepper from './SimpleStepper';
import Step from './SimpleStepperStep';
const getTextInSlide = (rendered: any, index: number) =>
within(rendered.getByTestId(`step${index}`)).getByText;
describe('Stepper', () => {
it('Handles nextStep property', async () => {
const rendered = render(
wrapInTestApp(
<Stepper>
<Step title="Step 0" data-testid="step0">
<div>step0</div>
</Step>
<Step
title="Step 1"
actions={{ nextStep: () => 3 }}
data-testid="step1"
>
<div>step1</div>
</Step>
<Step title="Step 2" data-testid="step2">
<div>step2</div>
</Step>
<Step title="Step 3" data-testid="step3">
<div>step3</div>
</Step>
</Stepper>,
),
);
fireEvent.click(getTextInSlide(rendered, 0)('Next') as Node);
expect(rendered.getByText('step1')).toBeInTheDocument();
fireEvent.click(getTextInSlide(rendered, 1)('Next') as Node);
expect(rendered.getByText('step3')).toBeInTheDocument();
fireEvent.click(getTextInSlide(rendered, 3)('Back') as Node);
expect(rendered.getByText('step1')).toBeInTheDocument();
});
it('Shows controls and content when going back to first step', () => {
const rendered = render(
wrapInTestApp(
<Stepper>
<Step title="Step 0" data-testid="step0">
<div>step0</div>
</Step>
<Step
title="Step 1"
actions={{ nextStep: () => 3 }}
data-testid="step1"
>
<div>step1</div>
</Step>
<Step title="Step 2" data-testid="step2">
<div>step2</div>
</Step>
<Step title="Step 3" data-testid="step3">
<div>step3</div>
</Step>
</Stepper>,
),
);
fireEvent.click(getTextInSlide(rendered, 0)('Next') as Node);
expect(rendered.getByText('step1')).toBeInTheDocument();
fireEvent.click(getTextInSlide(rendered, 1)('Next') as Node);
expect(rendered.getByText('step3')).toBeInTheDocument();
fireEvent.click(getTextInSlide(rendered, 3)('Back') as Node);
expect(rendered.getByText('step1')).toBeInTheDocument();
expect(getTextInSlide(rendered, 1)('Next')).toBeInTheDocument();
fireEvent.click(getTextInSlide(rendered, 1)('Back') as Node);
expect(rendered.getByText('step0')).toBeInTheDocument();
expect(getTextInSlide(rendered, 0)('Next')).toBeInTheDocument();
});
it('uses nextText if specified in all steps', () => {
const rendered = render(
wrapInTestApp(
<Stepper>
<Stepper>
<Step
title="Step 0"
actions={{
nextText: 'Step0Next',
}}
>
<div>step0</div>
</Step>
<Step
title="Step 1"
actions={{
nextText: 'FinalStepNext',
}}
>
<div>final step</div>
</Step>
</Stepper>
</Stepper>,
),
);
expect(rendered.getByText('Step0Next')).toBeInTheDocument();
fireEvent.click(rendered.getByText('Step0Next'));
expect(rendered.getByText('FinalStepNext')).toBeInTheDocument();
});
});
@@ -0,0 +1,84 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { Children, isValidElement, FC, useState } from 'react';
import { Stepper as MuiStepper } from '@material-ui/core';
type InternalState = {
stepperLength: number;
stepIndex: number;
setStepIndex: any;
stepHistory: number[];
setStepHistory: any;
onStepChange?: (prevIndex: number, nextIndex: number) => void;
};
const noop = () => {};
export const VerticalStepperContext = React.createContext<InternalState>({
stepperLength: 0,
stepIndex: 0,
setStepIndex: noop,
stepHistory: [],
setStepHistory: noop,
onStepChange: noop,
});
export interface StepperProps {
elevated?: boolean;
onStepChange?: (prevIndex: number, nextIndex: number) => void;
}
const Stepper: FC<StepperProps> = ({ children, elevated, onStepChange }) => {
const [stepIndex, setStepIndex] = useState<number>(0);
const [stepHistory, setStepHistory] = useState<number[]>([]);
const steps: React.ReactNode[] = [];
let endStep;
Children.forEach(children, (child) => {
if (isValidElement(child)) {
if (child.props.end) {
endStep = child;
} else {
steps.push(child);
}
}
});
return (
<>
<VerticalStepperContext.Provider
value={{
stepIndex,
setStepIndex,
stepHistory,
setStepHistory,
onStepChange,
stepperLength: Children.count(children),
}}
>
<MuiStepper
activeStep={stepIndex}
orientation="vertical"
elevation={elevated ? 2 : 0}
>
{steps}
</MuiStepper>
</VerticalStepperContext.Provider>
{stepIndex >= Children.count(children) - 1 && endStep}
</>
);
};
export default Stepper;
@@ -13,11 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { FC, ReactNode } from 'react';
import React, { useContext, FC, ReactNode } from 'react';
import { Button, makeStyles } from '@material-ui/core';
import { StepActions } from './Sequence';
import { StepActions } from './SimpleStepperStep';
import { VerticalStepperContext } from './SimpleStepper';
const useStyles = makeStyles(theme => ({
const useStyles = makeStyles((theme) => ({
root: {
marginTop: theme.spacing(3),
'& button': {
@@ -65,37 +66,31 @@ const BackBtn: FC<{
</Button>
);
export type SequenceFooterProps = {
stepIndex: number;
setStepIndex: React.Dispatch<React.SetStateAction<number>>;
stepArray: number[];
setStepArray: React.Dispatch<React.SetStateAction<number[]>>;
length: number;
actions: StepActions;
onSequenceStepChange?: (old: number, updated: number) => void;
export type SimpleStepperFooterProps = {
actions?: StepActions;
children?: ReactNode;
className: string;
};
const SequenceFooter: FC<SequenceFooterProps> = ({
stepIndex,
setStepIndex,
stepArray,
setStepArray,
length,
onSequenceStepChange,
actions,
const SimpleStepperFooter: FC<SimpleStepperFooterProps> = ({
actions = {},
children,
className,
}) => {
const classes = useStyles();
const {
stepperLength,
stepIndex,
setStepIndex,
stepHistory,
setStepHistory,
onStepChange,
} = useContext(VerticalStepperContext);
const onChange = (newIndex: number, callback?: () => void) => {
if (callback) {
callback();
}
if (onSequenceStepChange) {
onSequenceStepChange(stepIndex, newIndex);
if (onStepChange) {
onStepChange(stepIndex, newIndex);
}
setStepIndex(newIndex);
@@ -103,23 +98,23 @@ const SequenceFooter: FC<SequenceFooterProps> = ({
const handleNext = () => {
const newIndex = actions.nextStep
? actions.nextStep(stepIndex, length - 1)
? actions.nextStep(stepIndex, stepperLength - 1)
: stepIndex + 1;
onChange(newIndex, actions.onNext);
setStepArray([...stepArray, newIndex]);
setStepHistory([...stepHistory, newIndex]);
};
const handleBack = () => {
stepArray.pop();
onChange(stepArray[stepArray.length - 1], actions.onBack);
setStepArray([...stepArray]);
stepHistory.pop();
onChange(stepHistory[stepHistory.length - 1], actions.onBack);
setStepHistory([...stepHistory]);
};
const handleRestart = () => {
onChange(0, actions.onRestart);
setStepArray([0]);
setStepHistory([0]);
};
return (
<div className={`${classes.root} ${className}`}>
<div className={classes.root}>
{[undefined, true].includes(actions.showBack) && stepIndex !== 0 && (
<BackBtn
text={actions.backText}
@@ -133,7 +128,7 @@ const SequenceFooter: FC<SequenceFooterProps> = ({
text={actions.nextText}
handleClick={handleNext}
disabled={
(!!length && stepIndex >= length) ||
(!!stepperLength && stepIndex >= stepperLength) ||
(!!actions.canNext && !actions.canNext())
}
stepIndex={stepIndex}
@@ -151,4 +146,4 @@ const SequenceFooter: FC<SequenceFooterProps> = ({
);
};
export default SequenceFooter;
export default SimpleStepperFooter;
@@ -0,0 +1,86 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { FC } from 'react';
import {
Step as MuiStep,
StepContent,
StepLabel,
Typography,
makeStyles,
} from '@material-ui/core';
import SimpleStepperFooter from './SimpleStepperFooter';
const useStyles = makeStyles((theme) => ({
end: {
padding: theme.spacing(3),
},
}));
export type StepActions = {
showNext?: boolean;
canNext?: () => boolean;
onNext?: () => void;
nextStep?: (current: number, last: number) => number;
nextText?: string;
showBack?: boolean;
backText?: string;
onBack?: () => void;
showRestart?: boolean;
canRestart?: () => boolean;
onRestart?: () => void;
restartText?: string;
};
export type StepProps = {
title: string;
children: React.ReactElement;
end?: boolean;
actions?: StepActions;
};
const Step: FC<StepProps> = ({
title,
children,
end,
actions,
...muiProps
}) => {
const classes = useStyles();
// The end step is not a part of the stepper
// It simply is the final screen with an option to have buttons such as reset or back
return end ? (
<div className={classes.end}>
<Typography variant="h6">{title}</Typography>
{children}
<SimpleStepperFooter actions={{ ...(actions || {}), showNext: false }} />
</div>
) : (
<MuiStep {...muiProps}>
<StepLabel>
<Typography variant="h6">{title}</Typography>
</StepLabel>
<StepContent>
{children}
<SimpleStepperFooter actions={actions} />
</StepContent>
</MuiStep>
);
};
export default Step;
@@ -14,5 +14,7 @@
* limitations under the License.
*/
export { default } from './Sequence';
export type { StepType } from './Sequence';
import SimpleStepper from './SimpleStepper';
import SimpleStepperStep from './SimpleStepperStep';
export { SimpleStepper, SimpleStepperStep };
+1 -1
View File
@@ -32,7 +32,7 @@ export { default as CircleProgress } from './components/ProgressBars/CircleProgr
export { default as HorizontalProgress } from './components/ProgressBars/HorizontalProgress';
export { default as CopyTextButton } from './components/CopyTextButton';
export { default as Progress } from './components/Progress';
export { default as Sequence } from './components/Sequence';
export * from './components/SimpleStepper';
export { AlphaLabel, BetaLabel } from './components/Lifecycle';
export { default as SupportButton } from './components/SupportButton';
export { default as SortableTable } from './components/SortableTable';