Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(step-flow): add new component #6481

Merged
merged 1 commit into from
Feb 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading