Skip to content

Commit

Permalink
frontend: add nonLinear feature to Stepper (#3060)
Browse files Browse the repository at this point in the history
<!--- TITLE FORMAT: "component: short description", e.g. "k8s: add pod
log reader" -->

## Description
* Add nonLinear feature to Stepper
* Change Wizard actions in reducer
* Handle onNext prop without submit

## Testing Performed

## Non-linear stepper
1- Done
2- Active
3- Inactive 
4- Inactive with hover

![Screenshot 2024-07-30 at 1 59 31 p
m](https://github.com/user-attachments/assets/3c37ae68-a4cf-417c-8636-10832657165d)

## Linear stepper
![Screenshot 2024-07-30 at 2 02 18 p
m](https://github.com/user-attachments/assets/2883f1e6-1eaa-40fe-9a8e-64d9164e0ca0)
  • Loading branch information
scsjonatan authored Aug 12, 2024
1 parent 8efb167 commit bb420d6
Show file tree
Hide file tree
Showing 16 changed files with 190 additions and 85 deletions.
1 change: 1 addition & 0 deletions frontend/packages/core/src/Contexts/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export { ApplicationContext, useAppContext } from "./app-context";
export { ShortLinkContext, useShortLinkContext } from "./short-link-context";
export { WizardContext, useWizardContext } from "./wizard-context";
export type { WizardNavigationProps } from "./wizard-context";
export { WorkflowStorageContext, useWorkflowStorageContext } from "./workflow-storage-context";
export type {
WorkflowRemoveDataFn,
Expand Down
9 changes: 8 additions & 1 deletion frontend/packages/core/src/Contexts/wizard-context.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import React from "react";

export interface WizardNavigationProps {
toOrigin?: boolean;
keepSearch?: boolean;
}

export interface ContextProps {
displayWarnings: (warnings: string[]) => void;
onBack: (params?: { toOrigin?: boolean; keepSearch?: boolean }) => void;
onBack: (params?: WizardNavigationProps) => void;
onNext?: (params?: WizardNavigationProps) => void;
onSubmit: () => void;
setOnSubmit: (f: (...args: any[]) => void) => void;
setIsLoading: (isLoading: boolean) => void;
setHasError: (hasError: boolean) => void;
setIsComplete?: (isComplete: boolean) => void;
}

const WizardContext = React.createContext<() => ContextProps>(undefined);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export default {
displayWarnings: () => {},
onBack: () => {},
setHasError: () => {},
setIsComplete: () => {},
};
}}
>
Expand Down
7 changes: 6 additions & 1 deletion frontend/packages/core/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,12 @@ export type { BaseWorkflowProps, WorkflowConfiguration } from "./AppProvider/wor
export type { ButtonProps } from "./button";
export type { CardHeaderSummaryProps } from "./card";
export type { GridJustification } from "./grid";
export type { WorkflowRemoveDataFn, WorkflowRetrieveDataFn, WorkflowStoreDataFn } from "./Contexts";
export type {
WorkflowRemoveDataFn,
WorkflowRetrieveDataFn,
WorkflowStoreDataFn,
WizardNavigationProps,
} from "./Contexts";
export type { ClutchError } from "./Network/errors";
export type { TypographyProps } from "./typography";
export type { StyledComponent } from "@emotion/styled";
82 changes: 58 additions & 24 deletions frontend/packages/core/src/stepper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
import {
alpha,
Step as MuiStep,
StepButton as MuiStepButton,
StepConnector as MuiStepConnector,
StepLabel as MuiStepLabel,
Stepper as MuiStepper,
Expand All @@ -17,7 +18,7 @@ import {

import styled from "./styled";

const StepContainer = styled("div")<{ $orientation: StepperOrientation }>(
const StepContainer = styled("div")<{ $orientation: StepperOrientation; $nonLinear: boolean }>(
{
".MuiStepper-root": {
background: "transparent",
Expand Down Expand Up @@ -53,16 +54,21 @@ const StepContainer = styled("div")<{ $orientation: StepperOrientation }>(
},
},
props => ({ theme }: { theme: Theme }) => ({
".MuiStep-root:hover": {
".MuiStepLabel-iconContainer .icon-circle-nonlinear-pending": {
border: props.$nonLinear ? "2px solid black" : "",
},
},
".MuiStepLabel-label": {
fontWeight: 500,
fontSize: "14px",
color: alpha(theme.palette.secondary[900], 0.38),
color: theme.palette.secondary[600],
},
".MuiStepLabel-label.Mui-active": {
color: theme.palette.secondary[900],
color: theme.palette.primary[600],
},
".MuiStepLabel-label.Mui-completed": {
color: alpha(theme.palette.secondary[900], 0.38),
color: theme.palette.secondary[600],
},
...(props.$orientation === "horizontal"
? {
Expand All @@ -78,9 +84,11 @@ const StepContainer = styled("div")<{ $orientation: StepperOrientation }>(
},
},
".MuiStepConnector-line": {
height: "5px",
height: props.$nonLinear ? "3px" : "5px",
border: 0,
backgroundColor: theme.palette.secondary[200],
backgroundColor: props.$nonLinear
? theme.palette.primary[600]
: theme.palette.secondary[200],
borderRadius: "4px",
},
".Mui-active .MuiStepConnector-line": {
Expand All @@ -93,7 +101,9 @@ const StepContainer = styled("div")<{ $orientation: StepperOrientation }>(
: {
margin: "0px 2px 8px 2px",
".MuiStepConnector-line": {
borderColor: theme.palette.secondary[300],
borderColor: props.$nonLinear
? theme.palette.primary[600]
: theme.palette.secondary[300],
},
".Mui-active .MuiStepConnector-line": {
borderColor: theme.palette.primary[600],
Expand Down Expand Up @@ -138,20 +148,21 @@ type StepIconVariant = "active" | "pending" | "success" | "failed";
export interface StepIconProps {
index: number;
variant: StepIconVariant;
nonLinear?: boolean;
}

const StepIcon: React.FC<StepIconProps> = ({ index, variant }) => {
const StepIcon: React.FC<StepIconProps> = ({ index, variant, nonLinear = false }) => {
const theme = useTheme();
const stepIconVariants = {
active: {
background: theme.palette.contrastColor,
border: `1px solid ${theme.palette.primary[600]}`,
border: `2px solid ${theme.palette.primary[600]}`,
font: theme.palette.primary[600],
},
pending: {
background: theme.palette.secondary[200],
border: theme.palette.secondary[200],
font: alpha(theme.palette.secondary[900], 0.38),
background: nonLinear ? theme.palette.secondary[100] : theme.palette.secondary[200],
border: nonLinear ? theme.palette.secondary[100] : theme.palette.secondary[200],
font: nonLinear ? theme.palette.secondary[600] : alpha(theme.palette.secondary[900], 0.38),
},
success: {
background: theme.palette.primary[600],
Expand All @@ -171,8 +182,9 @@ const StepIcon: React.FC<StepIconProps> = ({ index, variant }) => {
} else if (variant === "failed") {
Icon = <ClearIcon font={color.font} fontSize="large" />;
}
const pendingClass = nonLinear && variant === "pending" ? "icon-circle-nonlinear-pending" : "";
return (
<Circle background={color.background} border={color.border}>
<Circle background={color.background} border={color.border} className={pendingClass}>
<DefaultIcon font={color.font}>{Icon}</DefaultIcon>
</Circle>
);
Expand All @@ -183,19 +195,28 @@ const StepIcon: React.FC<StepIconProps> = ({ index, variant }) => {
export interface StepProps {
label: string;
error?: boolean;
isComplete?: boolean;
}
/* eslint-enable react/no-unused-prop-types */

const Step: React.FC<StepProps> = ({ children }) => <>{children}</>;

export interface StepperProps extends Pick<MuiStepperProps, "orientation"> {
export interface StepperProps extends Pick<MuiStepperProps, "orientation" | "nonLinear"> {
activeStep: number;
children?: React.ReactElement<StepProps>[] | React.ReactElement<StepProps>;
handleStepClick?: (index: number) => void;
}

const Stepper = ({ activeStep, orientation = "horizontal", children }: StepperProps) => (
<StepContainer $orientation={orientation}>
const Stepper = ({
activeStep,
orientation = "horizontal",
children,
nonLinear = false,
handleStepClick,
}: StepperProps) => (
<StepContainer $orientation={orientation} $nonLinear={nonLinear}>
<MuiStepper
nonLinear={nonLinear}
activeStep={activeStep}
connector={<MuiStepConnector />}
alternativeLabel={orientation === "horizontal"}
Expand All @@ -205,19 +226,32 @@ const Stepper = ({ activeStep, orientation = "horizontal", children }: StepperPr
const stepProps = {
index: idx + 1,
variant: "pending" as StepIconVariant,
nonLinear,
};
if (idx === activeStep) {
stepProps.variant = step.props.error ? "failed" : "active";
} else if (idx < activeStep) {
const { isComplete = false } = step.props;

if (isComplete || (!nonLinear && idx < activeStep)) {
stepProps.variant = "success";
} else if (idx === activeStep) {
stepProps.variant = step.props.error ? "failed" : "active";
}

const label = step.props.label ?? `Step ${idx + 1}`;
const icon = <StepIcon {...stepProps} />;

return (
<MuiStep key={step.props.label}>
{/* eslint-disable-next-line react/no-unstable-nested-components */}
<MuiStepLabel StepIconComponent={() => <StepIcon {...stepProps} />}>
{step.props.label ?? `Step ${idx + 1}`}
</MuiStepLabel>
<MuiStep key={label} completed={isComplete}>
{nonLinear ? (
<MuiStepButton
disabled={isComplete}
onClick={() => handleStepClick && handleStepClick(idx)}
icon={icon}
>
{label}
</MuiStepButton>
) : (
<MuiStepLabel icon={icon}>{label}</MuiStepLabel>
)}
</MuiStep>
);
})}
Expand Down
23 changes: 21 additions & 2 deletions frontend/packages/core/src/stories/stepper.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as React from "react";
import { action } from "@storybook/addon-actions";
import type { Meta } from "@storybook/react";

import { Button, ButtonGroup } from "../button";
Expand All @@ -25,8 +26,10 @@ const PrimaryTemplate = ({
stepCount,
activeStep,
orientation,
nonLinear,
}: StepperProps & { stepCount: number }) => {
const [curStep, setCurStep] = React.useState(activeStep || 0);
const [completedSteps, setCompletedSteps] = React.useState({});

const handleNext = () => {
setCurStep(prevActiveStep => prevActiveStep + 1);
Expand All @@ -36,20 +39,35 @@ const PrimaryTemplate = ({
setCurStep(prevActiveStep => prevActiveStep - 1);
};

const handleStepClick = step => {
setCurStep(step);
action("handleStepClick")(step);
};

const handleReset = () => {
setCurStep(0);
setCompletedSteps({});
};

const handleCompleteStep = () => {
setCompletedSteps({ ...completedSteps, [curStep]: true });
};

return (
<>
<Grid container direction={orientation === "horizontal" ? "column" : "row"}>
<Grid item xs={1}>
<Stepper activeStep={curStep} orientation={orientation}>
<Stepper
activeStep={curStep}
orientation={orientation}
handleStepClick={handleStepClick}
nonLinear={nonLinear}
>
{Array(stepCount)
.fill(null)
.map((_, index: number) => (
// eslint-disable-next-line react/no-array-index-key
<Step key={index} label={`Step ${index + 1}`} />
<Step key={index} label={`Step ${index + 1}`} isComplete={completedSteps[index]} />
))}
</Stepper>
</Grid>
Expand All @@ -66,6 +84,7 @@ const PrimaryTemplate = ({
<Button onClick={handleNext} text={curStep === stepCount ? "Finish" : "Next"} />
</>
)}
<Button onClick={handleCompleteStep} text="Complete" disabled={!nonLinear} />
</ButtonGroup>
</>
);
Expand Down
23 changes: 17 additions & 6 deletions frontend/packages/wizard/src/state.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,43 @@
import React from "react";

enum WizardAction {
enum WizardActionType {
NEXT,
BACK,
RESET,
GO_TO_STEP,
}

interface WizardAction {
type: WizardActionType;
step?: number;
}

interface StateProps {
activeStep: number;
}

const reducer = (state: StateProps, action: WizardAction): StateProps => {
switch (action) {
case WizardAction.NEXT:
switch (action.type) {
case WizardActionType.NEXT:
return {
...state,
activeStep: state.activeStep + 1,
};
case WizardAction.BACK:
case WizardActionType.BACK:
return {
...state,
activeStep: state.activeStep > 0 ? state.activeStep - 1 : 0,
};
case WizardAction.RESET:
case WizardActionType.RESET:
return {
...state,
activeStep: 0,
};
case WizardActionType.GO_TO_STEP:
return {
...state,
activeStep: action.step,
};
default:
throw new Error(`Unknown wizard state: ${action}`);
}
Expand All @@ -40,4 +51,4 @@ const useWizardState = (): [StateProps, React.Dispatch<WizardAction>] => {
return React.useReducer(reducer, initialState);
};

export { WizardAction, useWizardState };
export { WizardActionType, useWizardState };
7 changes: 6 additions & 1 deletion frontend/packages/wizard/src/step.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,24 @@ const Grid = styled(ClutchGrid)({

export interface WizardStepProps {
isLoading: boolean;
isComplete?: boolean;
error?: ClutchError;
}

const WizardStep: React.FC<WizardStepProps> = ({ isLoading, error, children }) => {
const WizardStep: React.FC<WizardStepProps> = ({ isComplete, isLoading, error, children }) => {
const wizardContext = useWizardContext();
const hasError = error !== undefined && error !== null;
const showLoading = !hasError && isLoading;
const completed = isComplete && !isLoading && !hasError;
React.useEffect(() => {
wizardContext.setIsLoading(showLoading);
}, [showLoading]);
React.useEffect(() => {
wizardContext.setHasError(hasError);
}, [error]);
React.useEffect(() => {
wizardContext.setIsComplete(completed);
}, [completed]);
if (showLoading) {
return <Loadable isLoading={isLoading}>{children}</Loadable>;
}
Expand Down
Loading

0 comments on commit bb420d6

Please sign in to comment.