Rewrite sequence component (#682)
* Rework sequence component to be more declarative and simple * Renamed to SimpleStepper
This commit is contained in:
@@ -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;
|
||||
+27
-32
@@ -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;
|
||||
+4
-2
@@ -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 };
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user