Repository: Sage/carbon (ts)

Issue:
StepFlow - change title prop type
### Description

Dialog’s title prop can be an Element and we use it to display an icon next to the title when the action is destructive.
We are now introducing StepFlow in Dialogs but StepFlow’s title is only a string.


### Suggested solution

Make StepFlow's title prop accept an Element

### Demo URL

_No response_

### Alternatives

_No response_

### Additional context

_No response_

### Confidentiality

- [X] I confirm there is no confidential or commercially sensitive information included.

Pull request:
feat(step-flow): allow component title to be compositional
fix #6732 

### Proposed behaviour

<!--
A clear and concise description of what changes this PR makes. If applicable, include any UI screenshots to help explain your request. If you are a Sage contributor, please DO NOT share any commercially sensitive information, such as UI screenshots or code from Sage products.
-->

The `StepFlow` component will support a more compositional approach, allowing consumers to pass a node to the `title` prop. This means consumers can render any relevant elements within the node passed to the `title` prop, providing greater flexibility to meet specific design requirements. 

In order to ensure the component still functions, and our accessibility solutions still work, consumers need to render the `StepFlowTitle` sub component. This new sub component will not be noticeable for current consumers just wishing to pass a string to the `title` prop, but can be used and rendered within a custom node to ensure the component functions correctly.

![Screenshot 2024-06-28 at 12 30 20](https://github.com/Sage/carbon/assets/114918852/c00a58c2-a055-4457-ad2d-9f40d8f9425d)

![Screenshot 2024-06-28 at 12 30 12](https://github.com/Sage/carbon/assets/114918852/2da32819-b469-4010-9be3-e8b4da6d22a5)

<!--
A clear and concise description of the behaviour before this change. If applicable, include any UI screenshots to help explain your request. If you are a Sage contributor, please DO NOT share any commercially sensitive information, such as UI screenshots or code from Sage products.
-->

Currently, consumers are restricted to just passing a title via the `title` prop which is limited to being a string. This means if consumers wanted to add additional context to this title via a node, they are unable to do so.

### Checklist

<!-- Each PR should include the following -->

- [x] Commits follow our style guide
- [x] Related issues linked in commit messages if required
- [x] Screenshots are included in the PR if useful
- [ ] All themes are supported if required
- [x] Unit tests added or updated if required
- [x] Playwright automation tests added or updated if required
- [x] Storybook added or updated if required
- [ ] Translations added or updated (including creating or amending translation keys table in storybook) if required
- [ ] Typescript `d.ts` file added or updated if required
- [x] Related docs have been updated if required

#### QA

- [ ] Tested in provided StackBlitz sandbox/Storybook
- [ ] Add new Playwright test coverage if required
- [ ] Carbon implementation matches Design System/designs
- [ ] UI Tests GitHub check reviewed if required

### Additional context

<!-- Add any other context or links about the pull request here. -->

### Testing instructions

<!--
How can a reviewer test this PR?

If this PR addresses a pre-existing bug, please include a link to a sandbox that reproduces the original bug. A starter template has been provided to help you do this:
<https://stackblitz.com/fork/github/Parsium/carbon-starter>
-->


Patch:
diff --git a/src/components/step-flow/__internal__/step-flow.context.ts b/src/components/step-flow/__internal__/step-flow.context.ts
new file mode 100644
index 000000000..e41f635cd
--- /dev/null
+++ b/src/components/step-flow/__internal__/step-flow.context.ts
@@ -0,0 +1,13 @@
+import React from "react";
+
+export type StepFlowContextType = {
+  validatedCurrentStep?: number;
+  totalSteps?: number;
+  titleVariant?: "h1" | "h2";
+  category?: string;
+  titleRef?: React.RefObject<HTMLDivElement>;
+};
+
+const StepFlowContext = React.createContext<StepFlowContextType>({});
+
+export default StepFlowContext;
diff --git a/src/components/step-flow/index.ts b/src/components/step-flow/index.ts
index 6e24b7b40..6b1170f1e 100644
--- a/src/components/step-flow/index.ts
+++ b/src/components/step-flow/index.ts
@@ -1,2 +1,7 @@
 export { default as StepFlow } from "./step-flow.component";
-export type { StepFlowProps } from "./step-flow.component";
+export { default as StepFlowTitle } from "./step-flow-title/step-flow-title.component";
+export type {
+  StepFlowProps,
+  StepFlowHandle,
+  Steps,
+} from "./step-flow.component";
diff --git a/src/components/step-flow/step-flow-title/step-flow-title.component.tsx b/src/components/step-flow/step-flow-title/step-flow-title.component.tsx
new file mode 100644
index 000000000..50e4053fe
--- /dev/null
+++ b/src/components/step-flow/step-flow-title/step-flow-title.component.tsx
@@ -0,0 +1,77 @@
+import React, { useContext } from "react";
+import Typography from "../../typography";
+import useLocale from "../../../hooks/__internal__/useLocale";
+import StepFlowContext from "../__internal__/step-flow.context";
+import { StyledTitleFocusWrapper } from "../step-flow.style";
+
+export interface StepFlowTitleProps {
+  /** The title of the current step.  */
+  titleString: 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";
+  /** Override the screen reader only title with any additional context or information.
+   * If not provided, only the `titleString` prop will be used.
+   */
+  screenReaderOnlyTitle?: string;
+}
+
+export const StepFlowTitle = ({
+  titleString,
+  titleVariant,
+  screenReaderOnlyTitle,
+}: StepFlowTitleProps) => {
+  const locale = useLocale();
+
+  const {
+    titleRef,
+    validatedCurrentStep,
+    totalSteps,
+    category,
+    titleVariant: titleVariantContext,
+  } = useContext(StepFlowContext);
+
+  return (
+    <StyledTitleFocusWrapper
+      data-role="title-text-wrapper"
+      data-element="title-text-wrapper"
+      tabIndex={-1}
+      ref={titleRef}
+    >
+      <Typography
+        variant={titleVariant || titleVariantContext || "h1"}
+        data-element="title-text"
+      >
+        <Typography
+          fontWeight="900"
+          fontSize="var(--fontSizes600)"
+          lineHeight="var(--sizing375)"
+          variant="span"
+          aria-hidden="true"
+          data-element="visible-title-text"
+          data-role="visible-title-text"
+        >
+          {titleString}
+        </Typography>
+        {validatedCurrentStep && totalSteps && (
+          <Typography
+            variant="span"
+            data-element="visually-hidden-title-text"
+            data-role="visually-hidden-title-text"
+            screenReaderOnly
+          >
+            {locale.stepFlow.screenReaderOnlyTitle(
+              screenReaderOnlyTitle || titleString,
+              validatedCurrentStep,
+              totalSteps,
+              category
+            )}
+          </Typography>
+        )}
+      </Typography>
+    </StyledTitleFocusWrapper>
+  );
+};
+
+export default StepFlowTitle;
diff --git a/src/components/step-flow/step-flow.component.tsx b/src/components/step-flow/step-flow.component.tsx
index edae31803..55a3016a2 100644
--- a/src/components/step-flow/step-flow.component.tsx
+++ b/src/components/step-flow/step-flow.component.tsx
@@ -9,21 +9,24 @@ import {
   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";
+import StepFlowContext from "./__internal__/step-flow.context";
+import StepFlowTitle from "./step-flow-title/step-flow-title.component";
 
 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;
+  /** The title of the current step, this can be a string or a valid React node
+   * which contains the `<StepFlowTitle />` component as a descendant.
+   */
+  title: React.ReactNode;
   /** Set the variant of the internal 'Typography' component which contains the title.
    * However, despite the chosen variant the styling will always be overridden.
    */
@@ -57,7 +60,7 @@ export const StepFlow = forwardRef<StepFlowHandle, StepFlowProps>(
     {
       category,
       title,
-      titleVariant,
+      titleVariant = "h1",
       totalSteps,
       currentStep,
       showProgressIndicator = false,
@@ -146,37 +149,21 @@ export const StepFlow = forwardRef<StepFlowHandle, StepFlowProps>(
     );
 
     const stepFlowTitle = (
-      <StyledTitleFocusWrapper
-        data-role="title-text-wrapper"
-        data-element="title-text-wrapper"
-        tabIndex={-1}
-        ref={titleRef}
+      <StepFlowContext.Provider
+        value={{
+          validatedCurrentStep,
+          totalSteps,
+          titleVariant,
+          category,
+          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>
+        {typeof title === "string" ? (
+          <StepFlowTitle titleString={title} />
+        ) : (
+          title
+        )}
+      </StepFlowContext.Provider>
     );
 
     const stepFlowLabel = (
diff --git a/src/components/step-flow/step-flow.mdx b/src/components/step-flow/step-flow.mdx
index 4a604d734..b4db7bd49 100644
--- a/src/components/step-flow/step-flow.mdx
+++ b/src/components/step-flow/step-flow.mdx
@@ -33,7 +33,7 @@ Use a step flow to help a user complete tasks in a specific order, based on thei
 ## Quick Start
 
 ```js
-import StepFlow from "carbon-react/lib/components/step-flow";
+import { StepFlow, StepFlowTitle, StepFlowHandle, Steps } from "carbon-react/lib/components/step-flow";
 ```
 
 ## Examples
@@ -46,6 +46,28 @@ An example of how this can be achieved is provided in the **Example implementati
 
 <Canvas of={StepFlowStories.DefaultStory} />
 
+### Step Flow Title node
+
+To allow a compositional approach, the `title` prop accepts a node. This can be used to compose custom content to be passed as the title of the `StepFlow` component.
+However, it is important to note that it is recommended that the `StepFlowTitle` sub component should be a descendant of the passed node.
+
+<Canvas of={StepFlowStories.TitleNodeStory} />
+
+### Step Flow Title node with a screen reader only title
+
+The `screenReaderOnlyTitle` prop can be used to pass a custom screen reader only title. This is useful for providing screen reader users with all of the necessary information or context
+which may not be available visually.
+
+<Canvas of={StepFlowStories.TitleNodeStoryWithScreenReaderOnlyTitle} />
+
+Please see below an example of what this look like in the DOM.
+
+```jsx
+<span data-element="visually-hidden-title-text">
+  Step Title with a pointer image. Step 1 of 3.
+</span>
+```
+
 ### with category
 
 <Canvas of={StepFlowStories.CategoryStory} />
@@ -118,14 +140,9 @@ stepFlowHandle.current?.focus();
 
 This will ensure that screen reader users are not only made aware of any changes to information, but can then also navigate down the page from the `StepFlow` component as they see fit.
 
-### Example implementation with translations
-
-Various translations can be applied to both the step label, screen reader only title and close icon aria-label.
+### Example implementation with title node
 
-When providing the screen reader only title the `title`, `category` (if provided), `currentStep` and `totalSteps`, have all been passed to ensure screen reader users have all of the same information
-as users who can see the component. Please see below how this has been achieved with a French translation.
-
-<Canvas of={StepFlowStories.ExampleImplementationWithTranslations} />
+<Canvas of={StepFlowStories.ExampleImplementationWithTitleNode} />
 
 ## Props
 
@@ -163,3 +180,4 @@ The following keys are available to override the translations for this component
     },
   ]}
 />
+
diff --git a/src/components/step-flow/step-flow.pw.tsx b/src/components/step-flow/step-flow.pw.tsx
index 681983845..b0adb2bd4 100644
--- a/src/components/step-flow/step-flow.pw.tsx
+++ b/src/components/step-flow/step-flow.pw.tsx
@@ -6,10 +6,12 @@ import {
 } from "../../../playwright/support/helper";
 import {
   StepFlowComponent,
+  StepFlowComponentWithStepFlowTitleNode,
   StepFlowComponentWithRefAndButtons,
+  StepFlowComponentWithRefAndButtonsAndStepFlowTitleNode,
 } from "./components.test-pw";
 
-import { Steps } from "./step-flow.component";
+import { StepFlowTitle, Steps } from ".";
 
 import {
   stepFlowProgressIndicator,
@@ -64,6 +66,23 @@ test.describe("Prop checks for StepFlow component", () => {
     });
   });
 
+  (["h1", "h2"] as const).forEach((titleVariants) => {
+    test(`should render with the titleVariant prop passed to the 'StepFlowTitle' sub component, when the prop's value is ${titleVariants}`, async ({
+      mount,
+      page,
+    }) => {
+      await mount(
+        <StepFlowComponent
+          title={
+            <StepFlowTitle titleString="foo" titleVariant={titleVariants} />
+          }
+        />
+      );
+
+      await expect(page.locator(titleVariants)).toContainText("foo");
+    });
+  });
+
   test("should render the correct element when 'showProgressIndicator' is true", async ({
     mount,
     page,
@@ -237,9 +256,30 @@ test.describe("Ref checks for StepFlow component", () => {
 
     await expect(stepFlowTitleTextWrapper(page)).toBeFocused();
   });
+
+  test("should focus on title DOM element when a focus ref is passed, and a node is passed to `title` with the `StepFlowTitle` as a descendant", async ({
+    mount,
+    page,
+  }) => {
+    await mount(<StepFlowComponentWithRefAndButtonsAndStepFlowTitleNode />);
+
+    await expect(stepFlowTitleTextWrapper(page)).not.toBeFocused();
+    await button(page).nth(1).click();
+
+    await expect(stepFlowTitleTextWrapper(page)).toBeFocused();
+  });
 });
 
 test.describe("Accessibility tests for StepFlow component", () => {
+  test("should pass accessibility checks when a node is passed to`title` with the `StepFlowTile` as a descendant", async ({
+    mount,
+    page,
+  }) => {
+    await mount(<StepFlowComponentWithStepFlowTitleNode />);
+
+    await checkAccessibility(page);
+  });
+
   test("should pass accessibility checks when component is rendered with required props", async ({
     mount,
     page,
@@ -258,6 +298,15 @@ test.describe("Accessibility tests for StepFlow component", () => {
     await checkAccessibility(page);
   });
 
+  test("should pass accessibility checks when component is rendered with a ref and buttons and a node is passed to `title` with the `StepFlowTile` as a descendant", async ({
+    mount,
+    page,
+  }) => {
+    await mount(<StepFlowComponentWithRefAndButtonsAndStepFlowTitleNode />);
+
+    await checkAccessibility(page);
+  });
+
   test("should pass accessibility checks when 'showProgressIndicator' is true", async ({
     mount,
     page,
diff --git a/src/components/step-flow/step-flow.stories.tsx b/src/components/step-flow/step-flow.stories.tsx
index 2199a31c4..3edeb39d2 100644
--- a/src/components/step-flow/step-flow.stories.tsx
+++ b/src/components/step-flow/step-flow.stories.tsx
@@ -1,17 +1,22 @@
 import React, { useState, useRef } from "react";
 import { Meta, StoryObj } from "@storybook/react";
+import isChromatic from "../../../.storybook/isChromatic";
 
 import generateStyledSystemProps from "../../../.storybook/utils/styled-system-props";
 
-import I18nProvider from "../i18n-provider/i18n-provider.component";
 import Button from "../button";
 import Form from "../form";
 import Dialog from "../dialog";
 import Typography from "../typography";
 import Textarea from "../textarea";
+import Box from "../box";
+import Icon from "../icon";
+import Image from "../image";
+import pointSvg from "../../../.assets/point.svg";
 
-import { Steps, StepFlowHandle } from "./step-flow.component";
-import { StepFlow } from ".";
+import { StepFlow, StepFlowTitle, StepFlowHandle, Steps } from ".";
+
+const defaultOpenState = isChromatic();
 
 const styledSystemProps = generateStyledSystemProps({
   margin: true,
@@ -24,6 +29,22 @@ const meta: Meta<typeof StepFlow> = {
     ...styledSystemProps,
   },
   parameters: { controls: { disabled: true } },
+  decorators: [
+    (Story, context) => {
+      const isExampleImplementation = context.name?.includes("Example");
+      return (
+        <>
+          {isExampleImplementation && defaultOpenState ? (
+            <Box width="100%" height={900}>
+              <Story />
+            </Box>
+          ) : (
+            <Story />
+          )}
+        </>
+      );
+    },
+  ],
 };
 
 export default meta;
@@ -34,6 +55,37 @@ export const DefaultStory: Story = () => {
 };
 DefaultStory.storyName = "Default";
 
+export const TitleNodeStory: Story = () => {
+  const titleNode = (
+    <Box display="flex" alignItems="center">
+      <Icon type="bin" />
+      <StepFlowTitle titleString="Step title" />
+    </Box>
+  );
+
+  return <StepFlow title={titleNode} currentStep={1} totalSteps={6} />;
+};
+TitleNodeStory.storyName = "Title Node";
+
+export const TitleNodeStoryWithScreenReaderOnlyTitle: Story = () => {
+  const titleNode = (
+    <Box display="flex" alignItems="center">
+      <StepFlowTitle
+        titleString="Step title"
+        screenReaderOnlyTitle="Step Title with a pointer image"
+      />
+      <Image alt="" src={pointSvg} decorative size={50} />
+    </Box>
+  );
+
+  return <StepFlow title={titleNode} currentStep={1} totalSteps={6} />;
+};
+TitleNodeStoryWithScreenReaderOnlyTitle.storyName =
+  "Title Node with Screen Reader Only Title";
+TitleNodeStoryWithScreenReaderOnlyTitle.parameters = {
+  chromatic: { disableSnapshot: true },
+};
+
 export const CategoryStory: Story = () => {
   return (
     <StepFlow
@@ -84,6 +136,9 @@ export const TotalStepsStory: Story = () => {
   );
 };
 TotalStepsStory.storyName = "Total Steps";
+TotalStepsStory.parameters = {
+  chromatic: { disableSnapshot: true },
+};
 
 export const ShowCloseIconStory: Story = () => {
   return (
@@ -103,11 +158,11 @@ export const ExampleImplementation: Story = () => {
   const lowestStep = 1;
   const highestStep = 3;
 
-  const [isOpen, setIsOpen] = useState(false);
+  const [isOpen, setIsOpen] = useState(defaultOpenState);
   const [step, setStep] = useState(lowestStep);
   const stepFlowHandle = useRef<StepFlowHandle>(null);
 
-  const stepTitles = ["Transaction Type", "Add refund", "Refund details"];
+  const stepTitles = ["Step title 1", "Step title 2", "Step title 3"];
 
   function handleClick(clickType: string) {
     stepFlowHandle.current?.focus();
@@ -127,7 +182,7 @@ export const ExampleImplementation: Story = () => {
         showCloseIcon={false}
         title={
           <StepFlow
-            category="Add client"
+            category="Main goal"
             title={stepTitles[step - 1]}
             currentStep={step as Steps}
             totalSteps={highestStep}
@@ -159,7 +214,7 @@ export const ExampleImplementation: Story = () => {
             This is an example of a Dialog with a Form as content, with a Step
             Flow to help users complete tasks in a specific order.
           </Typography>
-          <Textarea label="Reason For Refund" />
+          <Textarea label="Textarea label" />
         </Form>
       </Dialog>
     </>
@@ -167,19 +222,15 @@ export const ExampleImplementation: Story = () => {
 };
 ExampleImplementation.storyName = "Example Implementation";
 
-export const ExampleImplementationWithTranslations: Story = () => {
+export const ExampleImplementationWithTitleNode: Story = () => {
   const lowestStep = 1;
   const highestStep = 3;
 
-  const [isOpen, setIsOpen] = useState(false);
+  const [isOpen, setIsOpen] = useState(defaultOpenState);
   const [step, setStep] = useState(lowestStep);
   const stepFlowHandle = useRef<StepFlowHandle>(null);
 
-  const stepTitles = [
-    "Type de transaction",
-    "Ajouter un remboursement",
-    "Détails du remboursement",
-  ];
+  const stepTitles = ["Step title 1", "Step title 2", "Step title 3"];
 
   function handleClick(clickType: string) {
     stepFlowHandle.current?.focus();
@@ -191,65 +242,57 @@ export const ExampleImplementationWithTranslations: Story = () => {
     }
   }
 
+  const titleNode = (
+    <Box display="flex" alignItems="center">
+      <Icon type="bin" />
+      <StepFlowTitle titleString={stepTitles[step - 1]} />
+    </Box>
+  );
   return (
-    <I18nProvider
-      locale={{
-        locale: () => "fr-FR",
-        stepFlow: {
-          stepLabel: (currentStep, totalSteps) =>
-            `Étape ${currentStep} de ${totalSteps}`,
-          screenReaderOnlyTitle: (title, currentStep, totalSteps, category) =>
-            `${category}. ${title}. Étape ${currentStep} de ${totalSteps}.`,
-          closeIconAriaLabel: () => "Fermer",
-        },
-      }}
-    >
-      <>
-        <Button onClick={() => setIsOpen(true)}>Open Dialog</Button>
-        <Dialog
-          open={isOpen}
-          showCloseIcon={false}
-          title={
-            <StepFlow
-              category="Ajouter un client"
-              title={stepTitles[step - 1]}
-              currentStep={step as Steps}
-              totalSteps={highestStep}
-              ref={stepFlowHandle}
-              showProgressIndicator
-              showCloseIcon
-              onDismiss={() => setIsOpen(false)}
-              mb="20px"
-            />
+    <>
+      <Button onClick={() => setIsOpen(true)}>Open Dialog</Button>
+      <Dialog
+        open={isOpen}
+        showCloseIcon={false}
+        title={
+          <StepFlow
+            category="Main goal"
+            title={titleNode}
+            currentStep={step as Steps}
+            totalSteps={highestStep}
+            ref={stepFlowHandle}
+            showProgressIndicator
+            showCloseIcon
+            onDismiss={() => setIsOpen(false)}
+            mb="20px"
+          />
+        }
+      >
+        <Form
+          stickyFooter
+          leftSideButtons={
+            <Button buttonType="tertiary" onClick={() => handleClick("Back")}>
+              Back
+            </Button>
+          }
+          rightSideButtons={
+            <Button
+              buttonType="primary"
+              onClick={() => handleClick("Continue")}
+            >
+              Continue
+            </Button>
           }
         >
-          <Form
-            stickyFooter
-            leftSideButtons={
-              <Button buttonType="tertiary" onClick={() => handleClick("Back")}>
-                Retour
-              </Button>
-            }
-            rightSideButtons={
-              <Button
-                buttonType="primary"
-                onClick={() => handleClick("Continue")}
-              >
-                Continuer
-              </Button>
-            }
-          >
-            <Typography>
-              Il s'agit d'un exemple de boîte de dialogue avec un formulaire
-              comme contenu, avec un flux d'étapes pour aider les utilisateurs à
-              effectuer des tâches dans un ordre spécifique.
-            </Typography>
-            <Textarea label="Motif du remboursement" />
-          </Form>
-        </Dialog>
-      </>
-    </I18nProvider>
+          <Typography>
+            This is an example of a Dialog with a Form as content, with a Step
+            Flow to help users complete tasks in a specific order.
+          </Typography>
+          <Textarea label="Textarea label" />
+        </Form>
+      </Dialog>
+    </>
   );
 };
-ExampleImplementationWithTranslations.storyName =
-  "Example Implementation with Translations";
+ExampleImplementationWithTitleNode.storyName =
+  "Example Implementation with title node";


Test patch:
diff --git a/src/components/step-flow/components.test-pw.tsx b/src/components/step-flow/components.test-pw.tsx
index 44e078980..739563bd7 100644
--- a/src/components/step-flow/components.test-pw.tsx
+++ b/src/components/step-flow/components.test-pw.tsx
@@ -1,13 +1,24 @@
 import React, { useRef } from "react";
 import Button from "../button";
 import Box from "../box";
-import { StepFlow, StepFlowProps } from ".";
-import { StepFlowHandle } from "./step-flow.component";
+import { StepFlow, StepFlowProps, StepFlowHandle, StepFlowTitle } from ".";
 
 export const StepFlowComponent = (props: Partial<StepFlowProps>) => (
   <StepFlow title="foo" currentStep={1} totalSteps={8} {...props} />
 );
 
+export const StepFlowComponentWithStepFlowTitleNode = () => {
+  const titleNode = (
+    <>
+      <>
+        <StepFlowTitle titleString="Step title" />
+      </>
+    </>
+  );
+
+  return <StepFlow title={titleNode} currentStep={1} totalSteps={8} />;
+};
+
 export const StepFlowComponentWithRefAndButtons = (
   props: Partial<StepFlowProps>
 ) => {
@@ -35,3 +46,39 @@ export const StepFlowComponentWithRefAndButtons = (
     </Box>
   );
 };
+
+export const StepFlowComponentWithRefAndButtonsAndStepFlowTitleNode = (
+  props: Partial<StepFlowProps>
+) => {
+  const stepFlowHandle = useRef<StepFlowHandle>(null);
+
+  const focusOnTitle = () => {
+    stepFlowHandle.current?.focus();
+  };
+
+  const titleNode = (
+    <>
+      <>
+        <StepFlowTitle titleString="Step title" />
+      </>
+    </>
+  );
+
+  return (
+    <Box>
+      <StepFlow
+        title={titleNode}
+        currentStep={1}
+        totalSteps={8}
+        ref={stepFlowHandle}
+        {...props}
+      />
+      <Button buttonType="tertiary" onClick={() => focusOnTitle()} mr={2}>
+        Back
+      </Button>
+      <Button buttonType="primary" onClick={() => focusOnTitle()}>
+        Continue
+      </Button>
+    </Box>
+  );
+};
diff --git a/src/components/step-flow/step-flow-title/step-flow-title.test.tsx b/src/components/step-flow/step-flow-title/step-flow-title.test.tsx
new file mode 100644
index 000000000..efe9cd7cf
--- /dev/null
+++ b/src/components/step-flow/step-flow-title/step-flow-title.test.tsx
@@ -0,0 +1,112 @@
+import React from "react";
+import { render, screen, within } from "@testing-library/react";
+import { StepFlow, StepFlowTitle } from "..";
+
+test("when the 'titleString' prop is passed, the correct visible element and text renders", () => {
+  render(<StepFlowTitle titleString="title" />);
+
+  const heading = screen.getByRole("heading", { level: 1 });
+  expect(heading).toBeVisible();
+  expect(heading).toHaveTextContent("title");
+});
+
+test("when the `StepFlowTitle` is not passed via the `title` prop in `StepFlow`, the screen reader only text does not render", () => {
+  render(<StepFlowTitle titleString="title" />);
+
+  const screenReaderOnlyText = screen.queryByTestId(
+    "visually-hidden-title-text"
+  );
+  expect(screenReaderOnlyText).not.toBeInTheDocument();
+});
+
+test("when the `StepFlowTitle` is passed via the `title` prop in `StepFlow`, the screen reader only text should be properly populated via context", () => {
+  render(
+    <StepFlow
+      currentStep={2}
+      totalSteps={4}
+      category="category"
+      title={<StepFlowTitle titleString="title" />}
+    />
+  );
+
+  const title = screen.getByRole("heading", { level: 1 });
+  expect(within(title).getByText(`category. title. Step 2 of 4.`)).toHaveStyle({
+    border: "0",
+    height: "1px",
+    margin: "-1px",
+    overflow: "hidden",
+    padding: "0",
+    position: "absolute",
+    width: "1px",
+    "white-space": "nowrap",
+  });
+});
+
+test("when the 'screenReaderOnlyTitle' prop is passed, it overrides the value passed to 'titleString', and the correct screen reader only text renders", () => {
+  render(
+    <StepFlow
+      currentStep={1}
+      totalSteps={8}
+      title={
+        <StepFlowTitle
+          titleString="title"
+          screenReaderOnlyTitle="this title overrides the other title"
+        />
+      }
+    />
+  );
+
+  const title = screen.getByRole("heading", { level: 1 });
+  expect(
+    within(title).getByText(
+      `this title overrides the other title. Step 1 of 8.`
+    )
+  ).toHaveStyle({
+    border: "0",
+    height: "1px",
+    margin: "-1px",
+    overflow: "hidden",
+    padding: "0",
+    position: "absolute",
+    width: "1px",
+    "white-space": "nowrap",
+  });
+});
+
+test("renders level one heading when the 'titleVariant' prop is not passed", () => {
+  render(<StepFlowTitle titleString="title" />);
+
+  const heading = screen.getByRole("heading", { level: 1 });
+  expect(heading).toBeVisible();
+  expect(heading).toHaveTextContent("title");
+});
+
+test.each([
+  [1, "h1"],
+  [2, "h2"],
+] as const)(
+  "renders level %s heading when the 'titleVariant' prop is %s",
+  (level, titleVariant) => {
+    render(<StepFlowTitle titleString="title" titleVariant={titleVariant} />);
+
+    const heading = screen.getByRole("heading", { level });
+    expect(heading).toBeVisible();
+    expect(heading).toHaveTextContent("title");
+  }
+);
+
+test("renders level two heading when the 'titleVariant' prop is h2 passed directly to `StepFlowTitle`, and the 'titleVariant' prop is also passed as h1 to `StepFlow`. As the 'titleVariant' on `StepFlowTitle` takes priority", () => {
+  render(
+    <StepFlow
+      currentStep={2}
+      totalSteps={4}
+      category="category"
+      titleVariant="h1"
+      title={<StepFlowTitle titleString="title" titleVariant="h2" />}
+    />
+  );
+
+  const heading = screen.getByRole("heading", { level: 2 });
+  expect(heading).toBeVisible();
+  expect(heading).toHaveTextContent("title");
+});
diff --git a/src/components/step-flow/step-flow.test.tsx b/src/components/step-flow/step-flow.test.tsx
index e037d788a..b184e9ea0 100644
--- a/src/components/step-flow/step-flow.test.tsx
+++ b/src/components/step-flow/step-flow.test.tsx
@@ -2,8 +2,7 @@ import React from "react";
 import { render, screen, within } from "@testing-library/react";
 import userEvent from "@testing-library/user-event";
 import enGB from "../../locales/en-gb";
-import { StepFlow } from "./index";
-import { StepFlowHandle, Steps } from "./step-flow.component";
+import { StepFlow, StepFlowHandle, StepFlowTitle, Steps } from ".";
 import Button from "../button";
 import I18nProvider from "../i18n-provider";
 
@@ -62,7 +61,7 @@ describe("Step Flow component", () => {
     expect(screen.getByText("bar")).toBeVisible();
   });
 
-  it("when the 'title' prop is passed, the correct element and text renders", () => {
+  it("when the 'title' prop is passed as a string, the correct element and text renders", () => {
     render(
       <StepFlow
         title="baz"
@@ -76,6 +75,25 @@ describe("Step Flow component", () => {
     expect(screen.getByText("baz")).toBeVisible();
   });
 
+  it("when the 'title' prop is passed via the `StepFlowTitle` sub component, the correct element and text renders", () => {
+    const stepFlowNode = (
+      <>
+        <StepFlowTitle titleString="node" />
+      </>
+    );
+    render(
+      <StepFlow
+        title={stepFlowNode}
+        currentStep={5}
+        totalSteps={6}
+        category="bar"
+        ref={() => {}}
+      />
+    );
+
+    expect(screen.getByText("node")).toBeVisible();
+  });
+
   it("renders level one heading when the 'titleVariant' prop is not passed", () => {
     render(
       <StepFlow title="Title" currentStep={5} totalSteps={6} ref={() => {}} />
@@ -403,8 +421,8 @@ describe("Step Flow component", () => {
     }
   );
 
-  describe("when ref handle is passed to StepFlow", () => {
-    it("calling exposed focus method refocuses on StepFlow's root container", async () => {
+  describe("when ref handle is passed", () => {
+    it("calling exposed focus method when the `title` prop is a string, refocuses on StepFlow's root container", async () => {
       const MockComponent = () => {
         const stepFlowHandle = React.useRef<StepFlowHandle>(null);
 
@@ -433,6 +451,44 @@ describe("Step Flow component", () => {
 
       expect(screen.getByTestId("title-text-wrapper")).toHaveFocus();
     });
+
+    it("calling exposed focus method when the `title` prop is a node with the `StepFlowTitle` as a descendant, refocuses on StepFlow's root container", async () => {
+      const MockComponent = () => {
+        const stepFlowHandle = React.useRef<StepFlowHandle>(null);
+
+        const titleNode = (
+          <>
+            <>
+              <StepFlowTitle titleString="title" />
+            </>
+          </>
+        );
+
+        return (
+          <div>
+            <StepFlow
+              totalSteps={5}
+              currentStep={1}
+              ref={stepFlowHandle}
+              title={titleNode}
+            />
+            <Button onClick={() => stepFlowHandle.current?.focus()}>
+              Press me to refocus on Dialog
+            </Button>
+          </div>
+        );
+      };
+
+      const user = userEvent.setup();
+      render(<MockComponent />);
+      const button = screen.getByRole("button", {
+        name: "Press me to refocus on Dialog",
+      });
+
+      await user.click(button);
+
+      expect(screen.getByTestId("title-text-wrapper")).toHaveFocus();
+    });
   });
 
   describe("console warning checks", () => {
@@ -446,6 +502,7 @@ describe("Step Flow component", () => {
     const noRefWarnMessage =
       "[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 mockRef = { current: null };
 
     beforeEach(() => {