Skip to content

Commit

Permalink
Merge pull request #6481 from Sage/FE-6278
Browse files Browse the repository at this point in the history
feat(step-flow): add new component
  • Loading branch information
edleeks87 authored Feb 6, 2024
2 parents d0c1611 + 707ef4f commit f25e78f
Show file tree
Hide file tree
Showing 16 changed files with 1,574 additions and 1 deletion.
35 changes: 35 additions & 0 deletions playwright/components/step-flow/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Page } from "playwright-core";
import {
STEP_FLOW_PROGRESS_INDICATOR,
STEP_FLOW_CATEGORY_TEXT,
STEP_FLOW_TITLE_TEXT_WRAPPER,
STEP_FLOW_TITLE_TEXT,
STEP_FLOW_VISUALLY_HIDDEN_TITLE_TEXT,
STEP_FLOW_PROGRESS_INDICATOR_BAR,
STEP_FLOW_LABEL,
STEP_FLOW_DISMISS_ICON,
} from "./locators";

// component preview locators
export const stepFlowProgressIndicator = (page: Page) =>
page.locator(STEP_FLOW_PROGRESS_INDICATOR);

export const stepFlowCategoryText = (page: Page) =>
page.locator(STEP_FLOW_CATEGORY_TEXT);

export const stepFlowTitleText = (page: Page) =>
page.locator(STEP_FLOW_TITLE_TEXT);

export const stepFlowTitleTextWrapper = (page: Page) =>
page.locator(STEP_FLOW_TITLE_TEXT_WRAPPER);

export const stepFlowVisuallyHiddenTitleText = (page: Page) =>
page.locator(STEP_FLOW_VISUALLY_HIDDEN_TITLE_TEXT);

export const stepFlowProgressIndicatorBar = (page: Page) =>
page.locator(STEP_FLOW_PROGRESS_INDICATOR_BAR);

export const stepFlowLabel = (page: Page) => page.locator(STEP_FLOW_LABEL);

export const stepFlowDismissIcon = (page: Page) =>
page.locator(STEP_FLOW_DISMISS_ICON);
12 changes: 12 additions & 0 deletions playwright/components/step-flow/locators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export const STEP_FLOW_PROGRESS_INDICATOR =
'[data-element="progress-indicator"]';
export const STEP_FLOW_CATEGORY_TEXT = '[data-element="category-text"]';
export const STEP_FLOW_TITLE_TEXT_WRAPPER =
'[data-element="title-text-wrapper"]';
export const STEP_FLOW_TITLE_TEXT = '[data-element="visible-title-text"]';
export const STEP_FLOW_VISUALLY_HIDDEN_TITLE_TEXT =
'[data-element="visually-hidden-title-text"]';
export const STEP_FLOW_PROGRESS_INDICATOR_BAR =
'[data-element="progress-indicator-bar"]';
export const STEP_FLOW_LABEL = '[data-element="step-label"]';
export const STEP_FLOW_DISMISS_ICON = 'span[data-element="close"]';
37 changes: 37 additions & 0 deletions src/components/step-flow/components.test-pw.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React, { useRef } from "react";
import Button from "../button";
import Box from "../box";
import { StepFlow, StepFlowProps } from ".";
import { StepFlowHandle } from "./step-flow.component";

export const StepFlowComponent = (props: Partial<StepFlowProps>) => (
<StepFlow title="foo" currentStep={1} totalSteps={8} {...props} />
);

export const StepFlowComponentWithRefAndButtons = (
props: Partial<StepFlowProps>
) => {
const stepFlowHandle = useRef<StepFlowHandle>(null);

const focusOnTitle = () => {
stepFlowHandle.current?.focus();
};

return (
<Box>
<StepFlow
title="foo"
currentStep={1}
totalSteps={8}
ref={stepFlowHandle}
{...props}
/>
<Button buttonType="tertiary" onClick={() => focusOnTitle()} mr={2}>
Back
</Button>
<Button buttonType="primary" onClick={() => focusOnTitle()}>
Continue
</Button>
</Box>
);
};
2 changes: 2 additions & 0 deletions src/components/step-flow/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as StepFlow } from "./step-flow.component";
export type { StepFlowProps } from "./step-flow.component";
57 changes: 57 additions & 0 deletions src/components/step-flow/step-flow-test.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React from "react";
import { StepFlow, StepFlowProps } from ".";

export default {
title: "Step Flow/Test",
includeStories: ["Default"],
parameters: {
info: { disable: true },
chromatic: {
disableSnapshot: true,
},
},
argTypes: {
category: {
control: {
type: "text",
},
},
title: {
control: {
type: "text",
},
},
totalSteps: {
control: {
min: 1,
max: 8,
step: 1,
type: "range",
},
},
currentStep: {
control: {
min: 1,
max: 8,
step: 1,
type: "range",
},
},
showProgressIndicator: {
control: {
type: "boolean",
},
},
showCloseIcon: {
control: {
type: "boolean",
},
},
},
};

export const Default = (props: Partial<StepFlowProps>) => (
<StepFlow title="default" currentStep={1} totalSteps={8} {...props} />
);

Default.storyName = "default";
230 changes: 230 additions & 0 deletions src/components/step-flow/step-flow.component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
import React, { useImperativeHandle, useRef, forwardRef } from "react";
import { MarginProps } from "styled-system";
import Icon from "../icon";
import IconButton from "../icon-button";
import {
StyledStepFlow,
StyledStepContent,
StyledStepContentText,
StyledStepLabelAndProgress,
StyledProgressIndicatorBar,
StyledProgressIndicator,
StyledTitleFocusWrapper,
} from "./step-flow.style";
import tagComponent, {
TagProps,
} from "../../__internal__/utils/helpers/tags/tags";
import Typography from "../typography";
import useLocale from "../../hooks/__internal__/useLocale";

export type Steps = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;

export interface StepFlowProps extends MarginProps, TagProps {
/** A category for the user journey. */
category?: string;
/** The title of the current step. */
title: string;
/** Set the variant of the internal 'Typography' component which contains the title.
* However, despite the chosen variant the styling will always be overridden.
*/
titleVariant?: "h1" | "h2";
/** The total steps in the user journey. */
totalSteps: Steps;
/**
* The current step of the user journey. If the set `currentStep` is higher than
* `totalSteps`the value of `currentStep` will be that of `totalSteps` instead.
*/
currentStep: Steps;
/** Determines if the progress indicator is shown. */
showProgressIndicator?: boolean;
/** Determines if the close icon button is shown */
showCloseIcon?: boolean;
/** function runs when user click dismiss button */
onDismiss?: (
e:
| React.KeyboardEvent<HTMLButtonElement>
| React.MouseEvent<HTMLButtonElement>
) => void;
}

export type StepFlowHandle = {
/** Programmatically focus on root container of Dialog. */
focus: () => void;
} | null;

export const StepFlow = forwardRef<StepFlowHandle, StepFlowProps>(
(
{
category,
title,
titleVariant,
totalSteps,
currentStep,
showProgressIndicator = false,
showCloseIcon = false,
onDismiss,
...rest
},
ref
) => {
const totalStepsArray = Array.from(
{ length: totalSteps },
(_, index) => index + 1
);

const validatedCurrentStep =
currentStep > totalSteps ? totalSteps : currentStep;

let currentStepWarnTriggered = false;
let noRefWarnTriggered = false;

/* eslint-disable no-console */
if (!currentStepWarnTriggered && currentStep > totalSteps) {
currentStepWarnTriggered = true;
console.warn(
"[WARNING] The `currentStep` prop should not be higher than the `totalSteps`prop in `StepFlow`." +
" Please ensure `currentStep`s value does not exceed that of `totalSteps`, in the meantime" +
" we have set `currentStep` value to that of `totalSteps`, and all indicators have been marked as completed."
);
}
if (!noRefWarnTriggered && !ref) {
noRefWarnTriggered = true;
console.warn(
"[WARNING] A `ref` should be provided to ensure focus is programmatically focused back to a title div," +
" this ensures screen reader users are informed regarding any changes and can navigate back down the page."
);
}

const progressIndicators = totalStepsArray.map((step) => {
const generateDataState = () => {
if (step === validatedCurrentStep) {
return "in-progress";
}
if (step < validatedCurrentStep) {
return "is-completed";
}
return "not-completed";
};

return (
<StyledProgressIndicator
key={step}
aria-hidden="true"
data-element="progress-indicator"
isCompleted={step < validatedCurrentStep}
isInProgress={step === validatedCurrentStep}
data-state={generateDataState()}
>
&nbsp;
</StyledProgressIndicator>
);
});

const locale = useLocale();

const closeIcon = (
<IconButton
data-element="close"
aria-label={locale.stepFlow.closeIconAriaLabel?.()}
onClick={onDismiss}
>
<Icon type="close" />
</IconButton>
);

const titleRef = useRef<HTMLDivElement>(null);

useImperativeHandle<StepFlowHandle, StepFlowHandle>(
ref,
() => ({
focus() {
titleRef.current?.focus();
},
}),
[]
);

const stepFlowTitle = (
<StyledTitleFocusWrapper
data-element="title-text-wrapper"
tabIndex={-1}
ref={titleRef}
>
<Typography variant={titleVariant || "h1"} data-element="title-text">
<Typography
fontWeight="900"
fontSize="var(--fontSizes600)"
lineHeight="var(--sizing375)"
variant="span"
aria-hidden="true"
data-element="visible-title-text"
>
{title}
</Typography>
<Typography
variant="span"
data-element="visually-hidden-title-text"
screenReaderOnly
>
{locale.stepFlow.screenReaderOnlyTitle(
title,
validatedCurrentStep,
totalSteps,
category
)}
</Typography>
</Typography>
</StyledTitleFocusWrapper>
);

const stepFlowLabel = (
<Typography
variant="span"
fontWeight="400"
fontSize="var(--fontSizes200)"
lineHeight="var(--sizing300)"
data-element="step-label"
aria-hidden="true"
>
{locale.stepFlow.stepLabel(validatedCurrentStep, totalSteps)}
</Typography>
);

return (
<StyledStepFlow {...rest} {...tagComponent("step-flow", rest)}>
<StyledStepContent>
{category ? (
<StyledStepContentText>
<Typography
fontWeight="500"
fontSize="var(--fontSizes100)"
lineHeight="var(--sizing250)"
variant="span"
data-element="category-text"
aria-hidden="true"
>
{category}
</Typography>
{stepFlowTitle}
</StyledStepContentText>
) : (
stepFlowTitle
)}
{showCloseIcon ? closeIcon : null}
</StyledStepContent>
{showProgressIndicator ? (
<StyledStepLabelAndProgress>
{stepFlowLabel}
<StyledProgressIndicatorBar data-element="progress-indicator-bar">
{progressIndicators}
</StyledProgressIndicatorBar>
</StyledStepLabelAndProgress>
) : (
stepFlowLabel
)}
</StyledStepFlow>
);
}
);

export default StepFlow;
Loading

0 comments on commit f25e78f

Please sign in to comment.