Repository: Sage/carbon (ts)

Issue:
Dialog: provide a prop to opt out of the default behaviour of focus returning to the triggering button
### Description

I want to be able to override the default focus behaviour of the Dialog on close, to provide a better experience for the user. 

### Reproduction

https://stackblitz.com/edit/parsium-carbon-starter-8kzldp?file=src%2FApp.tsx

### Steps to reproduce

- Open voiceover
- Navigate to `Open dialog` button and click it
- Close Dialog
- Focus is returned back to the button
- Aria-live section is not read out as expected

### JIRA ticket numbers (Sage only)

_No response_

### Suggested solution

_No response_

### Carbon version

144.9.2

### Design tokens version

4.35.0

### Relevant browsers

Chrome

### Relevant OSs

MacOS

### Additional context

_No response_

### Confidentiality

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

Pull request:
feat: add restoreFocusOnClose prop to modal based components
fix #7085 

### 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.
-->

Adds the `restoreFocusOnClose` prop to all components that use `useModalManager` or render the `Modal` component in their component hierarchy. Some components have received the prop from extending other props tables and spreading rest, others have had to be added manually. 

All components effected:

1. Modal
2. Dialog
3. Confirm
4. Alert
5. AdvancedColorPicker
6. DialogFullScreen
7. Sidebar

This prop will allow consumers to opt out of the default behaviour, which currently re-focuses all elements which were focused previously before a modal based component was opened.

### Current behaviour

<!--
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, all modal based components re-focuses the previous active element with no ability to opt out of said behaviour.

### Checklist

<!-- Each PR should include the following -->

- [x] Commits follow our style guide
- [x] Related issues linked in commit messages if required
- [ ] Screenshots are included in the PR if useful
- [ ] All themes are supported if required
- [ ] 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
- [ ] 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/advanced-color-picker/advanced-color-picker.component.tsx b/src/components/advanced-color-picker/advanced-color-picker.component.tsx
index 0536c6a62..72c464a18 100644
--- a/src/components/advanced-color-picker/advanced-color-picker.component.tsx
+++ b/src/components/advanced-color-picker/advanced-color-picker.component.tsx
@@ -14,6 +14,7 @@ import guid from "../../__internal__/utils/helpers/guid";
 import useLocale from "../../hooks/__internal__/useLocale";
 import { Dt, Dd } from "../definition-list";
 import Logger from "../../__internal__/utils/logger";
+import { ModalProps } from "../modal";
 
 let deprecateUncontrolledWarnTriggered = false;
 export interface AdvancedColor {
@@ -21,7 +22,9 @@ export interface AdvancedColor {
   value: string;
 }
 
-export interface AdvancedColorPickerProps extends MarginProps {
+export interface AdvancedColorPickerProps
+  extends MarginProps,
+    Pick<ModalProps, "restoreFocusOnClose"> {
   /** Prop to specify the aria-describedby property of the component */
   "aria-describedby"?: string;
   /**
@@ -71,6 +74,7 @@ export const AdvancedColorPicker = ({
   open = false,
   role,
   selectedColor,
+  restoreFocusOnClose = true,
   ...props
 }: AdvancedColorPickerProps) => {
   const [dialogOpen, setDialogOpen] = useState<boolean>();
@@ -251,6 +255,7 @@ export const AdvancedColorPicker = ({
         bespokeFocusTrap={handleFocus}
         focusFirstElement={selectedColorRef}
         role={role}
+        restoreFocusOnClose={restoreFocusOnClose}
       >
         <StyledAdvancedColorPickerPreview
           data-element="color-picker-preview"
diff --git a/src/components/advanced-color-picker/advanced-color-picker.mdx b/src/components/advanced-color-picker/advanced-color-picker.mdx
index e8010c1ea..a7af50a88 100644
--- a/src/components/advanced-color-picker/advanced-color-picker.mdx
+++ b/src/components/advanced-color-picker/advanced-color-picker.mdx
@@ -32,6 +32,13 @@ Shows how the color picker can be used to select a color from a predefined set.
 
 <Canvas of={AdvancedColorPickerStories.Default} />
 
+### Preventing focus from being restored when AdvancedColorPicker closes
+
+When the `restoreFocusOnClose` prop is `false`, focus will not be restored to the element that was focused before the `AdvancedColorPicker` was opened.
+Focus can instead be programmatically applied to another element if appropriate. 
+
+<Canvas of={AdvancedColorPickerStories.RestoreFocusOnCloseStory} />
+
 ## Props
 
 ### AdvancedColorPicker
diff --git a/src/components/advanced-color-picker/advanced-color-picker.pw.tsx b/src/components/advanced-color-picker/advanced-color-picker.pw.tsx
index 6ae175cb2..9c7e30c86 100644
--- a/src/components/advanced-color-picker/advanced-color-picker.pw.tsx
+++ b/src/components/advanced-color-picker/advanced-color-picker.pw.tsx
@@ -54,6 +54,27 @@ test.describe("when focused", () => {
   });
 });
 
+test("when AdvancedColorPicker is opened and then closed, with the `restoreFocusOnClose` prop passed as `false`, the call to action element should not be focused", async ({
+  mount,
+  page,
+}) => {
+  await mount(
+    <AdvancedColorPickerCustom open={false} restoreFocusOnClose={false} />,
+  );
+
+  const initialCell = advancedColorPickerCell(page);
+  const dialog = page.getByRole("dialog");
+  await expect(initialCell).not.toBeFocused();
+  await expect(dialog).not.toBeVisible();
+
+  await initialCell.click();
+  await expect(dialog).toBeVisible();
+  const closeButton = page.getByLabel("Close");
+  await closeButton.click();
+  await expect(initialCell).not.toBeFocused();
+  await expect(dialog).not.toBeVisible();
+});
+
 test.describe("should render AdvancedColorPicker component and check functionality", () => {
   (
     [
diff --git a/src/components/advanced-color-picker/advanced-color-picker.stories.tsx b/src/components/advanced-color-picker/advanced-color-picker.stories.tsx
index 8cfcaa491..6ea78a84a 100644
--- a/src/components/advanced-color-picker/advanced-color-picker.stories.tsx
+++ b/src/components/advanced-color-picker/advanced-color-picker.stories.tsx
@@ -1,4 +1,4 @@
-import React, { useState } from "react";
+import React, { useState, useRef } from "react";
 import { Meta, StoryObj } from "@storybook/react";
 import { allModes } from "../../../.storybook/modes";
 import isChromatic from "../../../.storybook/isChromatic";
@@ -6,6 +6,7 @@ import generateStyledSystemProps from "../../../.storybook/utils/styled-system-p
 
 import Box from "../box";
 import AdvancedColorPicker from ".";
+import Message from "../message";
 
 const styledSystemProps = generateStyledSystemProps({
   margin: true,
@@ -81,3 +82,61 @@ export const Default: Story = () => {
   );
 };
 Default.storyName = "Default";
+
+export const RestoreFocusOnCloseStory: Story = () => {
+  const [open, setOpen] = useState(false);
+  const [showMessage, setShowMessage] = useState(false);
+  const messageRef = useRef<HTMLDivElement>(null);
+
+  const [color, setColor] = useState("orchid");
+  const onChange = ({ target }: React.ChangeEvent<HTMLInputElement>) => {
+    setColor(target.value);
+  };
+  return (
+    <>
+      <AdvancedColorPicker
+        restoreFocusOnClose={false}
+        name="advancedPicker"
+        availableColors={[
+          { value: "#FFFFFF", label: "white" },
+          { value: "transparent", label: "transparent" },
+          { value: "#000000", label: "black" },
+          { value: "#A3CAF0", label: "blue" },
+          { value: "#FD9BA3", label: "pink" },
+          { value: "#B4AEEA", label: "purple" },
+          { value: "#ECE6AF", label: "goldenrod" },
+          { value: "#EBAEDE", label: "orchid" },
+          { value: "#EBC7AE", label: "desert" },
+          { value: "#AEECEB", label: "turquoise" },
+          { value: "#AEECD6", label: "mint" },
+        ]}
+        defaultColor="#EBAEDE"
+        selectedColor={color}
+        onChange={onChange}
+        onOpen={() => {
+          setOpen(!open);
+          setShowMessage(false);
+        }}
+        onClose={() => {
+          setOpen(false);
+          setShowMessage(true);
+          setTimeout(() => messageRef.current?.focus(), 1);
+        }}
+        onBlur={() => {}}
+        open={open}
+        mb={showMessage ? 5 : 0}
+      />
+      {showMessage && (
+        <Message
+          ref={messageRef}
+          variant="error"
+          onDismiss={() => setShowMessage(false)}
+        >
+          Some custom message
+        </Message>
+      )}
+    </>
+  );
+};
+RestoreFocusOnCloseStory.storyName = "With Restore Focus Close";
+RestoreFocusOnCloseStory.parameters = { chromatic: { disableSnapshot: true } };
diff --git a/src/components/dialog-full-screen/dialog-full-screen.component.tsx b/src/components/dialog-full-screen/dialog-full-screen.component.tsx
index a4eede263..b04334a8f 100644
--- a/src/components/dialog-full-screen/dialog-full-screen.component.tsx
+++ b/src/components/dialog-full-screen/dialog-full-screen.component.tsx
@@ -97,6 +97,7 @@ export const DialogFullScreen = ({
   focusableSelectors,
   topModalOverride,
   closeButtonDataProps,
+  restoreFocusOnClose = true,
   ...rest
 }: DialogFullScreenProps) => {
   const locale = useLocale();
@@ -164,6 +165,7 @@ export const DialogFullScreen = ({
       onCancel={onCancel}
       disableEscKey={disableEscKey}
       topModalOverride={topModalOverride}
+      restoreFocusOnClose={restoreFocusOnClose}
       {...componentTags}
     >
       <FocusTrap
diff --git a/src/components/dialog-full-screen/dialog-full-screen.mdx b/src/components/dialog-full-screen/dialog-full-screen.mdx
index 92012b881..c8a1cddfd 100644
--- a/src/components/dialog-full-screen/dialog-full-screen.mdx
+++ b/src/components/dialog-full-screen/dialog-full-screen.mdx
@@ -38,6 +38,13 @@ to ensure behaviour is consistent across all browsers.
 
 <Canvas of={DialogFullScreenStories.Default} />
 
+### Preventing focus from being restored when DialogFullScreen closes
+
+When the `restoreFocusOnClose` prop is `false`, focus will not be restored to the element that was focused before the `DialogFullScreen` was opened.
+Focus can instead be programmatically applied to another element if appropriate. 
+
+<Canvas of={DialogFullScreenStories.RestoreFocusOnCloseStory} />
+
 ### With complex example
 
 If you want to use more than one group of `Tabs` remember to put `selectedTabId` prop in every `Tabs` component
diff --git a/src/components/dialog-full-screen/dialog-full-screen.pw.tsx b/src/components/dialog-full-screen/dialog-full-screen.pw.tsx
index 58ad019c8..b73155a14 100644
--- a/src/components/dialog-full-screen/dialog-full-screen.pw.tsx
+++ b/src/components/dialog-full-screen/dialog-full-screen.pw.tsx
@@ -379,6 +379,29 @@ test.describe("render DialogFullScreen component and check properties", () => {
     await expect(firstButton).toBeFocused();
   });
 
+  test("when Dialog Full Screen is opened and then closed, with the `restoreFocusOnClose` prop passed as `false`, the call to action element should not be focused", async ({
+    mount,
+    page,
+  }) => {
+    await mount(
+      <DialogFullScreenComponent open={false} restoreFocusOnClose={false} />,
+    );
+
+    const button = page
+      .getByRole("button")
+      .filter({ hasText: "Open Dialog Full Screen" });
+    const dialogFullScreen = page.getByRole("dialog");
+    await expect(button).not.toBeFocused();
+    await expect(dialogFullScreen).not.toBeVisible();
+
+    await button.click();
+    await expect(dialogFullScreen).toBeVisible();
+    const closeButton = page.getByLabel("Close");
+    await closeButton.click();
+    await expect(button).not.toBeFocused();
+    await expect(dialogFullScreen).not.toBeVisible();
+  });
+
   test("should render component with autofocus disabled", async ({
     mount,
     page,
diff --git a/src/components/dialog-full-screen/dialog-full-screen.stories.tsx b/src/components/dialog-full-screen/dialog-full-screen.stories.tsx
index 927f8ff6b..c890ca5e2 100644
--- a/src/components/dialog-full-screen/dialog-full-screen.stories.tsx
+++ b/src/components/dialog-full-screen/dialog-full-screen.stories.tsx
@@ -11,6 +11,7 @@ import Form from "../form";
 import Textbox from "../textbox";
 import Pill from "../pill";
 import Drawer from "../drawer/drawer.component";
+import Message from "../message";
 import { Tabs, Tab } from "../tabs";
 import useMediaQuery from "../../hooks/useMediaQuery";
 import Link from "../link";
@@ -118,6 +119,76 @@ export const Default: Story = () => {
 Default.storyName = "Default";
 Default.parameters = { chromatic: { disableSnapshot: true } };
 
+export const RestoreFocusOnCloseStory: Story = () => {
+  const [isOpen, setIsOpen] = useState(false);
+  const [showMessage, setShowMessage] = useState(false);
+  const messageRef = useRef<HTMLDivElement>(null);
+
+  return (
+    <>
+      <Button
+        onClick={() => {
+          setIsOpen(true);
+          setShowMessage(false);
+        }}
+        mb={showMessage ? 5 : 0}
+      >
+        Open DialogFullScreen
+      </Button>
+      {showMessage && (
+        <Message
+          ref={messageRef}
+          variant="error"
+          onDismiss={() => setShowMessage(false)}
+        >
+          Some custom message
+        </Message>
+      )}
+      <DialogFullScreen
+        open={isOpen}
+        onCancel={() => {
+          setIsOpen(false);
+          setShowMessage(true);
+          setTimeout(() => messageRef.current?.focus(), 1);
+        }}
+        title="Title"
+        subtitle="Subtitle"
+        restoreFocusOnClose={false}
+      >
+        <Form
+          stickyFooter
+          leftSideButtons={
+            <Button onClick={() => setIsOpen(false)}>Cancel</Button>
+          }
+          saveButton={
+            <Button buttonType="primary" type="submit">
+              Save
+            </Button>
+          }
+        >
+          <Typography>
+            This is an example of a dialog with a Form as content
+          </Typography>
+          <Textbox label="First Name" />
+          <Textbox label="Middle Name" />
+          <Textbox label="Surname" />
+          <Textbox label="Birth Place" />
+          <Textbox label="Favourite Colour" />
+          <Textbox label="Address" />
+          <Textbox label="First Name" />
+          <Textbox label="Middle Name" />
+          <Textbox label="Surname" />
+          <Textbox label="Birth Place" />
+          <Textbox label="Favourite Colour" />
+          <Textbox label="Address" />
+        </Form>
+      </DialogFullScreen>
+    </>
+  );
+};
+RestoreFocusOnCloseStory.storyName = "With Restore Focus On Close";
+RestoreFocusOnCloseStory.parameters = { chromatic: { disableSnapshot: true } };
+
 export const WithComplexExample: Story = () => {
   const [isOpen, setIsOpen] = useState(defaultOpenState);
   const [activeTab1, setActiveTab1] = useState("tab-1");
diff --git a/src/components/dialog/dialog.component.tsx b/src/components/dialog/dialog.component.tsx
index 182e8a5cf..81aa6c554 100644
--- a/src/components/dialog/dialog.component.tsx
+++ b/src/components/dialog/dialog.component.tsx
@@ -126,6 +126,7 @@ export const Dialog = forwardRef<DialogHandle, DialogProps>(
       focusableContainers,
       topModalOverride,
       closeButtonDataProps,
+      restoreFocusOnClose = true,
       ...rest
     },
     ref,
@@ -218,6 +219,7 @@ export const Dialog = forwardRef<DialogHandle, DialogProps>(
         disableClose={disableClose}
         className={className ? `${className} carbon-dialog` : "carbon-dialog"}
         topModalOverride={topModalOverride}
+        restoreFocusOnClose={restoreFocusOnClose}
       >
         <FocusTrap
           autoFocus={!disableAutoFocus}
diff --git a/src/components/dialog/dialog.mdx b/src/components/dialog/dialog.mdx
index a101395ad..e5921c1f2 100644
--- a/src/components/dialog/dialog.mdx
+++ b/src/components/dialog/dialog.mdx
@@ -47,6 +47,13 @@ to ensure behaviour is consistent across all browsers.
 
 <Canvas of={DialogStories.DefaultStory} />
 
+### Preventing focus from being restored when Dialog closes
+
+When the `restoreFocusOnClose` prop is `false`, focus will not be restored to the element that was focused before the `Dialog` was opened.
+Focus can instead be programmatically applied to another element if appropriate. 
+
+<Canvas of={DialogStories.RestoreFocusOnCloseStory} />
+
 ### With Maximum Size
 
 When the `size` prop is `"maximise"` the height and width of the `Dialog`'s modal extends to the majority of the viewport.
diff --git a/src/components/dialog/dialog.pw.tsx b/src/components/dialog/dialog.pw.tsx
index 1f056ebf9..44f60c575 100644
--- a/src/components/dialog/dialog.pw.tsx
+++ b/src/components/dialog/dialog.pw.tsx
@@ -322,6 +322,25 @@ test.describe("Testing Dialog component properties", () => {
     await expect(firstButton).toBeFocused();
   });
 
+  test("when Dialog is opened and then closed, with the `restoreFocusOnClose` prop passed as `false`, the call to action element should not be focused", async ({
+    mount,
+    page,
+  }) => {
+    await mount(<DefaultStory restoreFocusOnClose={false} />);
+
+    const button = page.getByRole("button").filter({ hasText: "Open Dialog" });
+    const dialog = page.getByRole("dialog");
+    await expect(button).not.toBeFocused();
+    await expect(dialog).not.toBeVisible();
+
+    await button.click();
+    await expect(dialog).toBeVisible();
+    const closeButton = page.getByLabel("Close");
+    await closeButton.click();
+    await expect(button).not.toBeFocused();
+    await expect(dialog).not.toBeVisible();
+  });
+
   test("when disableAutoFocus prop is passed, the first focusable element should not be focused", async ({
     mount,
     page,
diff --git a/src/components/dialog/dialog.stories.tsx b/src/components/dialog/dialog.stories.tsx
index 1b0026ac1..f3cc4deb9 100644
--- a/src/components/dialog/dialog.stories.tsx
+++ b/src/components/dialog/dialog.stories.tsx
@@ -14,6 +14,7 @@ import Fieldset from "../fieldset";
 import { RadioButton, RadioButtonGroup } from "../radio-button";
 import Loader from "../loader";
 import Toast from "../toast";
+import Message from "../message";
 import Textarea from "../textarea";
 import CarbonProvider from "../carbon-provider";
 import useMediaQuery from "../../hooks/useMediaQuery";
@@ -113,6 +114,76 @@ export const DefaultStory: Story = {
   },
 };
 
+export const RestoreFocusOnCloseStory: Story = () => {
+  const [isOpen, setIsOpen] = useState(false);
+  const [showMessage, setShowMessage] = useState(false);
+  const messageRef = useRef<HTMLDivElement>(null);
+
+  return (
+    <>
+      <Button
+        onClick={() => {
+          setIsOpen(true);
+          setShowMessage(false);
+        }}
+        mb={showMessage ? 5 : 0}
+      >
+        Open Dialog
+      </Button>
+      {showMessage && (
+        <Message
+          ref={messageRef}
+          variant="error"
+          onDismiss={() => setShowMessage(false)}
+        >
+          Some custom message
+        </Message>
+      )}
+      <Dialog
+        open={isOpen}
+        onCancel={() => {
+          setIsOpen(false);
+          setShowMessage(true);
+          setTimeout(() => messageRef.current?.focus(), 1);
+        }}
+        title="Title"
+        subtitle="Subtitle"
+        restoreFocusOnClose={false}
+      >
+        <Form
+          stickyFooter
+          leftSideButtons={
+            <Button onClick={() => setIsOpen(false)}>Cancel</Button>
+          }
+          saveButton={
+            <Button buttonType="primary" type="submit">
+              Save
+            </Button>
+          }
+        >
+          <Typography>
+            This is an example of a dialog with a Form as content
+          </Typography>
+          <Textbox label="First Name" />
+          <Textbox label="Middle Name" />
+          <Textbox label="Surname" />
+          <Textbox label="Birth Place" />
+          <Textbox label="Favourite Colour" />
+          <Textbox label="Address" />
+          <Textbox label="First Name" />
+          <Textbox label="Middle Name" />
+          <Textbox label="Surname" />
+          <Textbox label="Birth Place" />
+          <Textbox label="Favourite Colour" />
+          <Textbox label="Address" />
+        </Form>
+      </Dialog>
+    </>
+  );
+};
+RestoreFocusOnCloseStory.storyName = "With Restore Focus On Close";
+RestoreFocusOnCloseStory.parameters = { chromatic: { disableSnapshot: true } };
+
 export const MaxSize: Story = {
   ...DefaultStory,
   name: "With Max Size",
diff --git a/src/components/modal/modal.component.tsx b/src/components/modal/modal.component.tsx
index b8904f6a4..3012aa50e 100644
--- a/src/components/modal/modal.component.tsx
+++ b/src/components/modal/modal.component.tsx
@@ -33,6 +33,10 @@ export interface ModalProps extends Omit<TagProps, "data-component"> {
   timeout?: number;
   /** Manually override the internal modal stacking order to set this as top */
   topModalOverride?: boolean;
+  /** Enables the automatic restoration of focus to the element that invoked
+   * the modal when the modal is closed.
+   */
+  restoreFocusOnClose?: boolean;
 }
 
 const Modal = ({
@@ -46,6 +50,7 @@ const Modal = ({
   enableBackgroundUI = false,
   timeout = 300,
   topModalOverride,
+  restoreFocusOnClose = true,
   ...rest
 }: ModalProps) => {
   if (!deprecatedClassNameWarningShown && rest.className) {
@@ -98,7 +103,9 @@ const Modal = ({
     modalRef: ref,
     setTriggerRefocusFlag,
     topModalOverride,
-    focusCallToActionElement: document.activeElement as HTMLElement,
+    focusCallToActionElement: restoreFocusOnClose
+      ? (document.activeElement as HTMLElement)
+      : undefined,
   });
 
   let background;
diff --git a/src/components/sidebar/sidebar.component.tsx b/src/components/sidebar/sidebar.component.tsx
index 6dfd83596..2ec8c4af6 100644
--- a/src/components/sidebar/sidebar.component.tsx
+++ b/src/components/sidebar/sidebar.component.tsx
@@ -23,7 +23,7 @@ export interface SidebarProps
   extends PaddingProps,
     Omit<TagProps, "data-component">,
     WidthProps,
-    Pick<ModalProps, "topModalOverride"> {
+    Pick<ModalProps, "topModalOverride" | "restoreFocusOnClose"> {
   /** Prop to specify the aria-describedby property of the component */
   "aria-describedby"?: string;
   /**
@@ -111,6 +111,7 @@ export const Sidebar = React.forwardRef<HTMLDivElement, SidebarProps>(
       width,
       headerPadding = {},
       topModalOverride,
+      restoreFocusOnClose = true,
       ...rest
     }: SidebarProps,
     ref,
@@ -192,6 +193,7 @@ export const Sidebar = React.forwardRef<HTMLDivElement, SidebarProps>(
         disableEscKey={disableEscKey}
         enableBackgroundUI={enableBackgroundUI}
         topModalOverride={topModalOverride}
+        restoreFocusOnClose={restoreFocusOnClose}
       >
         {enableBackgroundUI ? (
           sidebar
diff --git a/src/components/sidebar/sidebar.mdx b/src/components/sidebar/sidebar.mdx
index 3c01ca19b..7e60b395f 100644
--- a/src/components/sidebar/sidebar.mdx
+++ b/src/components/sidebar/sidebar.mdx
@@ -49,6 +49,13 @@ to ensure behaviour is consistent across all browsers.
 
 <Canvas of={SidebarStories.DefaultStory} />
 
+### Preventing focus from being restored when Sidebar closes
+
+When the `restoreFocusOnClose` prop is `false`, focus will not be restored to the element that was focused before the `Sidebar` was opened.
+Focus can instead be programmatically applied to another element if appropriate. 
+
+<Canvas of={SidebarStories.RestoreFocusOnCloseStory} />
+
 ### Custom padding around content
 
 <Canvas of={SidebarStories.CustomPaddingAroundContent} />
diff --git a/src/components/sidebar/sidebar.pw.tsx b/src/components/sidebar/sidebar.pw.tsx
index 636e615f4..4ea855246 100644
--- a/src/components/sidebar/sidebar.pw.tsx
+++ b/src/components/sidebar/sidebar.pw.tsx
@@ -337,6 +337,25 @@ test.describe("Prop tests for Sidebar component", () => {
     await expect(firstButton).toBeFocused();
   });
 
+  test("when Sidebar is opened and then closed, with the `restoreFocusOnClose` prop passed as `false`, the call to action element should not be focused", async ({
+    mount,
+    page,
+  }) => {
+    await mount(<Default open={false} restoreFocusOnClose={false} />);
+
+    const button = page.getByRole("button").filter({ hasText: "Open sidebar" });
+    const sidebar = sidebarPreview(page);
+    await expect(button).not.toBeFocused();
+    await expect(sidebar).not.toBeVisible();
+
+    await button.click();
+    await expect(sidebar).toBeVisible();
+    const closeButton = page.getByLabel("Close");
+    await closeButton.click();
+    await expect(button).not.toBeFocused();
+    await expect(sidebar).not.toBeVisible();
+  });
+
   test("should call onCancel callback when a click event is triggered", async ({
     mount,
     page,
diff --git a/src/components/sidebar/sidebar.stories.tsx b/src/components/sidebar/sidebar.stories.tsx
index 9aca17b12..abaa22cb2 100644
--- a/src/components/sidebar/sidebar.stories.tsx
+++ b/src/components/sidebar/sidebar.stories.tsx
@@ -12,6 +12,7 @@ import Textbox from "../textbox";
 import Box from "../box";
 import Dialog from "../dialog";
 import DialogFullScreen from "../confirm";
+import Message from "../message";
 
 import Sidebar from ".";
 
@@ -84,6 +85,55 @@ export const DefaultStory: Story = () => {
 };
 DefaultStory.storyName = "Default";
 
+export const RestoreFocusOnCloseStory: Story = () => {
+  const [isOpen, setIsOpen] = useState(false);
+  const [showMessage, setShowMessage] = useState(false);
+  const messageRef = useRef<HTMLDivElement>(null);
+
+  return (
+    <>
+      <Button
+        onClick={() => {
+          setIsOpen(true);
+          setShowMessage(false);
+        }}
+        mb={showMessage ? 5 : 0}
+      >
+        Open sidebar
+      </Button>
+      {showMessage && (
+        <Message
+          ref={messageRef}
+          variant="error"
+          onDismiss={() => setShowMessage(false)}
+        >
+          Some custom message
+        </Message>
+      )}
+      <Sidebar
+        aria-label="sidebar"
+        open={isOpen}
+        onCancel={() => {
+          setIsOpen(false);
+          setShowMessage(true);
+          setTimeout(() => messageRef.current?.focus(), 1);
+        }}
+        restoreFocusOnClose={false}
+      >
+        <Box mb={2}>
+          <Button buttonType="primary">Test</Button>
+          <Button buttonType="secondary" ml={2}>
+            Last
+          </Button>
+        </Box>
+        Main Content
+      </Sidebar>
+    </>
+  );
+};
+RestoreFocusOnCloseStory.storyName = "With Restore Focus On Close";
+RestoreFocusOnCloseStory.parameters = { chromatic: { disableSnapshot: true } };
+
 export const CustomPaddingAroundContent: Story = () => {
   const [isOpen, setIsOpen] = useState(defaultOpenState);
   return (
diff --git a/src/components/vertical-menu/vertical-menu-full-screen/vertical-menu-full-screen.component.tsx b/src/components/vertical-menu/vertical-menu-full-screen/vertical-menu-full-screen.component.tsx
index c3aab8cee..214c3e3ef 100644
--- a/src/components/vertical-menu/vertical-menu-full-screen/vertical-menu-full-screen.component.tsx
+++ b/src/components/vertical-menu/vertical-menu-full-screen/vertical-menu-full-screen.component.tsx
@@ -61,6 +61,7 @@ export const VerticalMenuFullScreen = ({
     closeModal: handleKeyDown,
     modalRef: menuWrapperRef,
     topModalOverride: true,
+    focusCallToActionElement: document.activeElement as HTMLElement,
   });
 
   // TODO remove this as part of FE-5650
diff --git a/src/components/vertical-menu/vertical-menu.pw.tsx b/src/components/vertical-menu/vertical-menu.pw.tsx
index cd85830f4..a8a9e1a98 100644
--- a/src/components/vertical-menu/vertical-menu.pw.tsx
+++ b/src/components/vertical-menu/vertical-menu.pw.tsx
@@ -20,6 +20,7 @@ import {
   checkGoldenOutline,
   assertCssValueIsApproximately,
   checkAccessibility,
+  waitForAnimationEnd,
 } from "../../../playwright/support/helper";
 import {
   verticalMenuComponent,
@@ -316,6 +317,42 @@ test.describe("with beforeEach for VerticalMenuFullScreen", () => {
     await expect(verticalMenuItem(page).first()).toBeVisible();
   });
 
+  test("when a Vertical Menu Fullscreen is opened and then closed, the call to action element should be focused", async ({
+    mount,
+    page,
+  }) => {
+    await mount(<VerticalMenuFullScreenCustom />);
+
+    const item = page.getByRole("button").filter({ hasText: "Menu" });
+    await item.click();
+    await waitForAnimationEnd(verticalMenuFullScreen(page));
+    const closeButton = page.getByLabel("Close");
+    await closeButton.click();
+    await expect(item).toBeFocused();
+  });
+
+  test("when Vertical Menu Fullscreen is open on render, then closed, opened and then closed again, the call to action element should be focused", async ({
+    mount,
+    page,
+  }) => {
+    await mount(<VerticalMenuFullScreenCustom isOpen />);
+
+    await waitForAnimationEnd(verticalMenuFullScreen(page));
+    await expect(verticalMenuFullScreen(page)).toBeVisible();
+    const closeButton = page.getByLabel("Close");
+    await closeButton.click();
+
+    const item = page.getByRole("button").filter({ hasText: "Menu" });
+    await expect(item).not.toBeFocused();
+    await expect(verticalMenuFullScreen(page)).not.toBeVisible();
+
+    await item.click();
+    await waitForAnimationEnd(verticalMenuFullScreen(page));
+    await expect(verticalMenuFullScreen(page)).toBeVisible();
+    await closeButton.click();
+    await expect(item).toBeFocused();
+  });
+
   test(`should verify that Vertical Menu Fullscreen has no effect on the tab order when isOpen prop is false`, async ({
     mount,
     page,


Test patch:
diff --git a/src/components/dialog/components.test-pw.tsx b/src/components/dialog/components.test-pw.tsx
index 770acd2c9..3437c4409 100644
--- a/src/components/dialog/components.test-pw.tsx
+++ b/src/components/dialog/components.test-pw.tsx
@@ -212,8 +212,10 @@ export const DialogComponentFocusableSelectors = (
 
 export const DefaultStory = ({
   open = defaultOpenState,
+  restoreFocusOnClose,
 }: {
   open?: boolean;
+  restoreFocusOnClose?: boolean;
 }) => {
   const [isOpen, setIsOpen] = useState(open);
   return (
@@ -224,6 +226,7 @@ export const DefaultStory = ({
         onCancel={() => setIsOpen(false)}
         title="Title"
         subtitle="Subtitle"
+        restoreFocusOnClose={restoreFocusOnClose}
       >
         <Form
           stickyFooter
diff --git a/src/components/modal/modal.test.tsx b/src/components/modal/modal.test.tsx
index b576cfd62..2b5d3f4ed 100644
--- a/src/components/modal/modal.test.tsx
+++ b/src/components/modal/modal.test.tsx
@@ -1,4 +1,4 @@
-import React from "react";
+import React, { useState } from "react";
 import { render, screen } from "@testing-library/react";
 
 import userEvent from "@testing-library/user-event";
@@ -19,6 +19,27 @@ mockedUseScrollBlock.mockReturnValue({
   blockScroll,
 });
 
+const MockModal = ({
+  restoreFocusOnClose,
+}: {
+  restoreFocusOnClose: boolean;
+}) => {
+  const [isOpen, setIsOpen] = useState(false);
+
+  return (
+    <>
+      <button type="button" onClick={() => setIsOpen(true)}>
+        Open Modal
+      </button>
+      <Modal open={isOpen} restoreFocusOnClose={restoreFocusOnClose}>
+        <button type="button" onClick={() => setIsOpen(false)}>
+          Close Modal
+        </button>
+      </Modal>
+    </>
+  );
+};
+
 test("renders background overlay when enableBackgroundUI is false", () => {
   render(<Modal onCancel={() => {}} open enableBackgroundUI={false} />);
 
@@ -180,3 +201,29 @@ test("throws a deprecation warning if the 'className' prop is set", () => {
 
   loggerSpy.mockRestore();
 });
+
+test("should restore focus to the call to action element when `restoreFocusOnClose` is true", async () => {
+  render(<MockModal restoreFocusOnClose />);
+
+  const user = userEvent.setup();
+  const button = screen.getByRole("button", { name: "Open Modal" });
+  await user.click(button);
+
+  const closeButton = screen.getByRole("button", { name: "Close Modal" });
+  await user.click(closeButton);
+
+  expect(button).toHaveFocus();
+});
+
+test("should not restore focus to the call to action element when `restoreFocusOnClose` is false", async () => {
+  render(<MockModal restoreFocusOnClose={false} />);
+
+  const user = userEvent.setup();
+  const button = screen.getByRole("button", { name: "Open Modal" });
+  await user.click(button);
+
+  const closeButton = screen.getByRole("button", { name: "Close Modal" });
+  await user.click(closeButton);
+
+  expect(button).not.toHaveFocus();
+});
diff --git a/src/components/sidebar/components.test-pw.tsx b/src/components/sidebar/components.test-pw.tsx
index ce0d49ff1..dcbb03a0a 100644
--- a/src/components/sidebar/components.test-pw.tsx
+++ b/src/components/sidebar/components.test-pw.tsx
@@ -8,7 +8,13 @@ import Toast from "../toast";
 import Textbox from "../textbox";
 import Dialog from "../dialog";
 
-export const Default = ({ open = true }: { open?: boolean }) => {
+export const Default = ({
+  open = true,
+  restoreFocusOnClose,
+}: {
+  open?: boolean;
+  restoreFocusOnClose?: boolean;
+}) => {
   const [isOpen, setIsOpen] = useState(open);
   const onCancel = () => {
     setIsOpen(false);
@@ -16,7 +22,12 @@ export const Default = ({ open = true }: { open?: boolean }) => {
   return (
     <>
       <Button onClick={() => setIsOpen(true)}>Open sidebar</Button>
-      <Sidebar aria-label="sidebar" open={isOpen} onCancel={onCancel}>
+      <Sidebar
+        aria-label="sidebar"
+        open={isOpen}
+        onCancel={onCancel}
+        restoreFocusOnClose={restoreFocusOnClose}
+      >
         <Box mb={2}>
           <Button buttonType="primary">Test</Button>
           <Button buttonType="secondary" ml={2}>
diff --git a/src/components/vertical-menu/components.test-pw.tsx b/src/components/vertical-menu/components.test-pw.tsx
index 450730ed3..e1ce6a1c7 100644
--- a/src/components/vertical-menu/components.test-pw.tsx
+++ b/src/components/vertical-menu/components.test-pw.tsx
@@ -186,10 +186,11 @@ export const VerticalMenuTriggerCustom = (
   );
 };
 
-export const VerticalMenuFullScreenCustom = (
-  props: Partial<VerticalMenuFullScreenProps>,
-) => {
-  const [isMenuOpen, setIsMenuOpen] = useState(false);
+export const VerticalMenuFullScreenCustom = ({
+  isOpen = false,
+  ...props
+}: Partial<VerticalMenuFullScreenProps> & { isOpen?: boolean }) => {
+  const [isMenuOpen, setIsMenuOpen] = useState(isOpen);
 
   const fullscreenViewBreakPoint = useMediaQuery("(max-width: 1200px)");