Repository: Sage/carbon (ts)

Issue:
Voiceover not reading elements for nested DialogFullScreen component in Safari browser
### Current behaviour

When a child `DialogFullScreen` component is nested within a parent `DialogFullScreen` component, Voiceover does not read out elements in the child `DialogFullScreen` component using Safari browser.

### Expected behaviour

When a child `DialogFullScreen` component is nested within a parent `DialogFullScreen` component, expect Voiceover to read out elements in the child `DialogFullScreen` component using Safari browser.

### CodeSandbox or Storybook URL

https://codesandbox.io/s/voiceover-dialog-i64x57?file=/src/index.js

### JIRA Ticket (Sage Only)

SBS-31787

### Suggested Solution

_No response_

### Carbon Version

109.0.0

### Design Tokens Version

_No response_

### What browsers are you seeing the problem on?

Safari

### What Operating System are you seeing the problem on?

MacOS

### Anything else we should know?

_No response_

### Confidentiality

- [X] I confirm there is no confidential or commercially sensitive information included.

Pull request:
fix(dialog, sidebar): ensure aria-modal is only added to top modal
Currently we add aria-modal=true to the container div of each modal component (dialog, dialog-full-screen, sidebar). This ensures screenreaders do not announce content outside the modal. However when opening one modal from another, VoiceOver on Safari refuses to read the content on the new modal while the old one still has aria-modal=true. The workaround is to ensure aria-modal=true only appears on whatever the current top modal is. This requires keeping track of the current top modal in a context which has been added to CarbonProvider, as well as keeping a global list of "top modal setters" in the window object to ensure that multiple Carbon Providers in the same application (potentially even from different versions of Carbon) still update each other's context correctly.

fix #5303

### Proposed behaviour

aria-modal=true should only be present on at most one modal (dialog/dialogfullscreen/sidebar) at a time - whichever one is currently on top of a stack of modals

VoiceOver on Safari should correctly announce a new modal when opened,  whether from another modal or not.



<!--
A clear and concise description of what changes this PR makes.

If applicable, add screenshots of a codesandbox to help explain your request. You can paste these directly into GitHub.

Please DO NOT share screenshots or the source code of your project.

You can create a codesandbox to show the behaviour before/after this pull request by forking this template https://codesandbox.io/s/carbon-quickstart-j5pb2

If you include a CodeSandbox link, the bot will fork it with the new built version of carbon.
If you have a commit that includes fixes #XXXX and issue #XXXX has a CodeSandbox link in the body, the bot will fork
it with the new built version of carbon.
-->

### Current behaviour

aria-modal=true is placed on all modals. VoiceOver on Safari will not inform the user about a modal opening when opened from another modal.

<!--
A clear and concise description of the behaviour before this change.

If applicable, add screenshots. You can paste these directly into GitHub.
-->

### 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
- [X] All themes are supported if required
- [X] Unit tests added or updated if required
- [X] Cypress automation tests added or updated if required
- [X] Storybook added or updated if required
- [X] Translations added or updated (including creating or amending translation keys table in storybook) if required
- [X] Typescript `d.ts` file added or updated if required

#### QA

- [ ] Tested in CodeSandbox/storybook
- [ ] Add new Cypress 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

Test with all supported browser/screenreader combinations, as well as without a screenreader, to ensure that situations with multiple dialog/sidebar components work as expected.

If possible, test with multiple CarbonProviders wrapping different parts of an application and ensure that everything still works correctly, with aria-modal only ever applied to one modal (but is always applied to any just-opened modal).

<!-- How can a reviewer test this PR? -->

The following CodeSandbox is an example of the broken behaviour.
You can see the new behaviour by looking at the version in the comment by `codesandbox[bot]`.

<!-- Add CodeSandbox here -->


Patch:
diff --git a/src/components/carbon-provider/carbon-provider.component.tsx b/src/components/carbon-provider/carbon-provider.component.tsx
index a244600cd..16f993567 100644
--- a/src/components/carbon-provider/carbon-provider.component.tsx
+++ b/src/components/carbon-provider/carbon-provider.component.tsx
@@ -6,6 +6,8 @@ import CarbonScopedTokensProvider from "../../style/design-tokens/carbon-scoped-
 
 import { ThemeObject } from "../../style/themes/base";
 
+import { TopModalContextProvider } from "./top-modal-context";
+
 export interface CarbonProviderProps {
   theme?: Partial<ThemeObject>;
   children: React.ReactNode;
@@ -24,7 +26,7 @@ export const CarbonProvider = ({
   <ThemeProvider theme={theme}>
     <CarbonScopedTokensProvider>
       <NewValidationContext.Provider value={{ validationRedesignOptIn }}>
-        {children}
+        <TopModalContextProvider>{children}</TopModalContextProvider>
       </NewValidationContext.Provider>
     </CarbonScopedTokensProvider>
   </ThemeProvider>
diff --git a/src/components/carbon-provider/top-modal-context.spec.tsx b/src/components/carbon-provider/top-modal-context.spec.tsx
new file mode 100644
index 000000000..5303d9279
--- /dev/null
+++ b/src/components/carbon-provider/top-modal-context.spec.tsx
@@ -0,0 +1,34 @@
+import React from "react";
+import { render, RenderResult } from "@testing-library/react";
+import { TopModalContextProvider } from "./top-modal-context";
+
+const MockComponent = ({ providerOpen }: { providerOpen: boolean }) => (
+  <>
+    {providerOpen && (
+      <TopModalContextProvider>some children</TopModalContextProvider>
+    )}
+  </>
+);
+
+describe("TopModalContextProvider", () => {
+  let rerender: RenderResult["rerender"];
+
+  beforeEach(() => {
+    window.__CARBON_INTERNALS_MODAL_SETTER_LIST = [];
+    ({ rerender } = render(<MockComponent providerOpen />));
+  });
+
+  it("adds a setter function to the global list when mounted", () => {
+    expect(window.__CARBON_INTERNALS_MODAL_SETTER_LIST?.length).toBe(1);
+  });
+
+  it("adds a second setter to the global list if a second provider is mounted", () => {
+    render(<MockComponent providerOpen />);
+    expect(window.__CARBON_INTERNALS_MODAL_SETTER_LIST?.length).toBe(2);
+  });
+
+  it("removes the setter function when unmounted", () => {
+    rerender(<MockComponent providerOpen={false} />);
+    expect(window.__CARBON_INTERNALS_MODAL_SETTER_LIST?.length).toBe(0);
+  });
+});
diff --git a/src/components/carbon-provider/top-modal-context.tsx b/src/components/carbon-provider/top-modal-context.tsx
new file mode 100644
index 000000000..c90e246ba
--- /dev/null
+++ b/src/components/carbon-provider/top-modal-context.tsx
@@ -0,0 +1,53 @@
+import React, {
+  createContext,
+  useState,
+  useEffect,
+  useRef,
+  ReactNode,
+} from "react";
+
+type TopModalContextProps = {
+  topModal: HTMLElement | null;
+};
+
+const TopModalContext = createContext<TopModalContextProps>({
+  topModal: null,
+});
+
+export const TopModalContextProvider = ({
+  children,
+}: {
+  children: ReactNode;
+}) => {
+  const [topModal, setTopModal] = useState<HTMLElement | null>(null);
+
+  // can't add the setter to the global list inside useEffect because that doesn't run until
+  // after the render. We use a ref to ensure it only runs once.
+  const isFirstRender = useRef(true);
+
+  if (isFirstRender.current) {
+    if (!window.__CARBON_INTERNALS_MODAL_SETTER_LIST) {
+      window.__CARBON_INTERNALS_MODAL_SETTER_LIST = [];
+    }
+
+    window.__CARBON_INTERNALS_MODAL_SETTER_LIST.push(setTopModal);
+
+    isFirstRender.current = false;
+  }
+
+  useEffect(() => {
+    return () => {
+      window.__CARBON_INTERNALS_MODAL_SETTER_LIST = window.__CARBON_INTERNALS_MODAL_SETTER_LIST?.filter(
+        (setter) => setter !== setTopModal
+      );
+    };
+  }, []);
+
+  return (
+    <TopModalContext.Provider value={{ topModal }}>
+      {children}
+    </TopModalContext.Provider>
+  );
+};
+
+export default TopModalContext;
diff --git a/src/components/dialog-full-screen/dialog-full-screen.component.js b/src/components/dialog-full-screen/dialog-full-screen.component.js
index c1a899a9c..77a280557 100644
--- a/src/components/dialog-full-screen/dialog-full-screen.component.js
+++ b/src/components/dialog-full-screen/dialog-full-screen.component.js
@@ -1,4 +1,4 @@
-import React, { useRef } from "react";
+import React, { useRef, useContext } from "react";
 import PropTypes from "prop-types";
 
 import createGuid from "../../__internal__/utils/helpers/guid";
@@ -12,6 +12,7 @@ import IconButton from "../icon-button";
 import Icon from "../icon";
 import useLocale from "../../hooks/__internal__/useLocale";
 import useIsStickyFooterForm from "../../hooks/__internal__/useIsStickyFooterForm";
+import TopModalContext from "../carbon-provider/top-modal-context";
 
 const DialogFullScreen = ({
   "aria-describedby": ariaDescribedBy,
@@ -43,6 +44,8 @@ const DialogFullScreen = ({
   const { current: subtitleId } = useRef(createGuid());
   const hasStickyFooter = useIsStickyFooterForm(children);
 
+  const { topModal } = useContext(TopModalContext);
+
   const closeIcon = () => {
     if (!showCloseIcon || !onCancel) return null;
 
@@ -104,7 +107,11 @@ const DialogFullScreen = ({
         additionalWrapperRefs={focusableContainers}
       >
         <StyledDialogFullScreen
-          aria-modal={role === "dialog" ? true : undefined}
+          aria-modal={
+            role === "dialog" && topModal?.contains(dialogRef.current)
+              ? true
+              : undefined
+          }
           {...ariaProps}
           ref={dialogRef}
           data-element="dialog-full-screen"
diff --git a/src/components/dialog-full-screen/dialog-full-screen.spec.js b/src/components/dialog-full-screen/dialog-full-screen.spec.js
index 1f4fbb1a8..568f6da54 100644
--- a/src/components/dialog-full-screen/dialog-full-screen.spec.js
+++ b/src/components/dialog-full-screen/dialog-full-screen.spec.js
@@ -16,6 +16,7 @@ import Form from "../form";
 
 import { assertStyleMatch } from "../../__spec_helper__/test-utils";
 import guid from "../../__internal__/utils/helpers/guid";
+import CarbonProvider from "../carbon-provider";
 
 jest.mock("../../__internal__/utils/helpers/guid");
 
@@ -34,16 +35,18 @@ describe("DialogFullScreen", () => {
   it("should have aria-modal attribute on the dialog container", () => {
     onCancel = jasmine.createSpy("cancel");
     wrapper = mount(
-      <DialogFullScreen
-        onCancel={onCancel}
-        className="foo"
-        open
-        title="my title"
-        subtitle="my subtitle"
-      >
-        <Button>Button</Button>
-        <Button>Button</Button>
-      </DialogFullScreen>
+      <CarbonProvider>
+        <DialogFullScreen
+          onCancel={onCancel}
+          className="foo"
+          open
+          title="my title"
+          subtitle="my subtitle"
+        >
+          <Button>Button</Button>
+          <Button>Button</Button>
+        </DialogFullScreen>
+      </CarbonProvider>
     );
 
     expect(
@@ -58,10 +61,12 @@ describe("DialogFullScreen", () => {
 
   it("should not have aria-modal attribute on the dialog container if role is not `dialog`", () => {
     wrapper = mount(
-      <DialogFullScreen onCancel={onCancel} open title="my title" role="main">
-        <Button>Button</Button>
-        <Button>Button</Button>
-      </DialogFullScreen>
+      <CarbonProvider>
+        <DialogFullScreen onCancel={onCancel} open title="my title" role="main">
+          <Button>Button</Button>
+          <Button>Button</Button>
+        </DialogFullScreen>
+      </CarbonProvider>
     );
 
     expect(
@@ -522,7 +527,9 @@ describe("DialogFullScreen", () => {
   describe("ARIA attributes", () => {
     it("by default, set role to `dialog` and aria-modal to true", () => {
       wrapper = mount(
-        <DialogFullScreen open onCancel={() => {}} onConfirm={() => {}} />
+        <CarbonProvider>
+          <DialogFullScreen open onCancel={() => {}} onConfirm={() => {}} />
+        </CarbonProvider>
       );
 
       expect(
diff --git a/src/components/dialog/dialog.component.tsx b/src/components/dialog/dialog.component.tsx
index 73297b714..34d802562 100644
--- a/src/components/dialog/dialog.component.tsx
+++ b/src/components/dialog/dialog.component.tsx
@@ -1,4 +1,10 @@
-import React, { useCallback, useEffect, useLayoutEffect, useRef } from "react";
+import React, {
+  useRef,
+  useEffect,
+  useLayoutEffect,
+  useCallback,
+  useContext,
+} from "react";
 
 import createGuid from "../../__internal__/utils/helpers/guid";
 import Modal, { ModalProps } from "../modal";
@@ -19,6 +25,7 @@ import IconButton from "../icon-button";
 import Icon from "../icon";
 import useLocale from "../../hooks/__internal__/useLocale";
 import useIsStickyFooterForm from "../../hooks/__internal__/useIsStickyFooterForm";
+import TopModalContext from "../carbon-provider/top-modal-context";
 
 const PADDING_VALUES = [0, 1, 2, 3, 4, 5, 6, 7, 8] as const;
 type PaddingValues = typeof PADDING_VALUES[number];
@@ -120,6 +127,8 @@ export const Dialog = ({
   const { current: subtitleId } = useRef(createGuid());
   const hasStickyFooter = useIsStickyFooterForm(children);
 
+  const { topModal } = useContext(TopModalContext);
+
   const centerDialog = useCallback(() => {
     /* istanbul ignore if */
     if (!dialogRef.current) {
@@ -266,7 +275,7 @@ export const Dialog = ({
         additionalWrapperRefs={focusableContainers}
       >
         <StyledDialog
-          aria-modal
+          aria-modal={topModal?.contains(dialogRef.current) ? true : undefined}
           ref={dialogRef}
           topMargin={TOP_MARGIN}
           {...dialogProps}
diff --git a/src/components/dialog/dialog.spec.tsx b/src/components/dialog/dialog.spec.tsx
index 50dfaec6a..93bcc6994 100644
--- a/src/components/dialog/dialog.spec.tsx
+++ b/src/components/dialog/dialog.spec.tsx
@@ -27,6 +27,7 @@ import Form from "../form";
 import { StyledFormContent, StyledFormFooter } from "../form/form.style";
 import IconButton from "../icon-button";
 import Help from "../help";
+import CarbonProvider from "../carbon-provider";
 
 jest.mock("../../hooks/__internal__/useResizeObserver");
 jest.mock("../../__internal__/utils/helpers/guid");
@@ -442,6 +443,30 @@ describe("Dialog", () => {
         ).toEqual(0);
       });
     });
+
+    it("should have aria-modal attribute on the dialog container", () => {
+      onCancel = jest.fn();
+      wrapper = mount(
+        <CarbonProvider>
+          <Dialog
+            onCancel={onCancel}
+            className="foo"
+            open
+            title="my title"
+            subtitle="my subtitle"
+          >
+            <Button>Button</Button>
+            <Button>Button</Button>
+          </Dialog>
+        </CarbonProvider>
+      );
+
+      expect(
+        wrapper.find(StyledDialog).getDOMNode().getAttribute("aria-modal")
+      ).toBe("true");
+
+      wrapper.unmount();
+    });
   });
 
   describe("when topMargin is passed to the StyledDialog", () => {
diff --git a/src/components/modal/__internal__/modal-manager.spec.ts b/src/components/modal/__internal__/modal-manager.spec.ts
index 8288cbeb8..c188107fa 100644
--- a/src/components/modal/__internal__/modal-manager.spec.ts
+++ b/src/components/modal/__internal__/modal-manager.spec.ts
@@ -142,4 +142,46 @@ describe("ModalManager", () => {
       expect(Manager.isTopmost(mockModal1)).toBe(true);
     });
   });
+
+  describe("when there are functions in the global top modal setter list", () => {
+    let f: jest.Mock<(topModal: HTMLElement | null) => void>;
+    let g: jest.Mock<(topModal: HTMLElement | null) => void>;
+
+    beforeEach(() => {
+      f = jest.fn();
+      g = jest.fn();
+      window.__CARBON_INTERNALS_MODAL_SETTER_LIST = [f, g];
+    });
+
+    it("when a modal is added, all functions are called with the new modal", () => {
+      const mockModal = document.createElement("div");
+      ModalManager.addModal(mockModal);
+      expect(f).toHaveBeenCalledWith(mockModal);
+      expect(g).toHaveBeenCalledWith(mockModal);
+    });
+
+    it("when a modal is removed, all functions are called with the new top modal", () => {
+      const mockModal = document.createElement("div");
+      const otherModal = document.createElement("div");
+      ModalManager.addModal(mockModal);
+      ModalManager.addModal(otherModal);
+      ModalManager.removeModal(otherModal);
+      expect(f).toHaveBeenCalledTimes(3);
+      expect(g).toHaveBeenCalledTimes(3);
+      expect(f.mock.calls[2][0]).toBe(mockModal);
+      expect(g.mock.calls[2][0]).toBe(mockModal);
+    });
+
+    it("when the modal list is cleared, all functions are called with the null", () => {
+      const mockModal = document.createElement("div");
+      const otherModal = document.createElement("div");
+      ModalManager.addModal(mockModal);
+      ModalManager.addModal(otherModal);
+      ModalManager.clearList();
+      expect(f).toHaveBeenCalledTimes(3);
+      expect(g).toHaveBeenCalledTimes(3);
+      expect(f.mock.calls[2][0]).toBe(null);
+      expect(g.mock.calls[2][0]).toBe(null);
+    });
+  });
 });
diff --git a/src/components/modal/__internal__/modal-manager.ts b/src/components/modal/__internal__/modal-manager.ts
index 714afae61..e7376644b 100644
--- a/src/components/modal/__internal__/modal-manager.ts
+++ b/src/components/modal/__internal__/modal-manager.ts
@@ -8,9 +8,11 @@ type ModalList = {
 declare global {
   interface Window {
     __CARBON_INTERNALS_MODAL_LIST?: ModalList;
+    __CARBON_INTERNALS_MODAL_SETTER_LIST?: ((
+      topModal: HTMLElement | null
+    ) => void)[];
   }
 }
-
 class ModalManagerInstance {
   private modalList: ModalList;
 
@@ -49,6 +51,7 @@ class ModalManagerInstance {
     }
 
     this.modalList.push({ modal, setTriggerRefocusFlag });
+    this.callTopModalSetters();
   };
 
   isTopmost(modal: HTMLElement | null) {
@@ -69,6 +72,7 @@ class ModalManagerInstance {
     }
 
     this.modalList.splice(modalIndex, 1);
+    this.callTopModalSetters();
 
     if (!this.modalList.length) {
       return;
@@ -84,6 +88,16 @@ class ModalManagerInstance {
   clearList() {
     window.__CARBON_INTERNALS_MODAL_LIST = [];
     this.modalList = window.__CARBON_INTERNALS_MODAL_LIST;
+    this.callTopModalSetters();
+  }
+
+  callTopModalSetters() {
+    if (window.__CARBON_INTERNALS_MODAL_SETTER_LIST) {
+      const topModal = this.getTopModal()?.modal || null;
+      for (const setTopModal of window.__CARBON_INTERNALS_MODAL_SETTER_LIST) {
+        setTopModal(topModal);
+      }
+    }
   }
 }
 
diff --git a/src/components/sidebar/sidebar.component.tsx b/src/components/sidebar/sidebar.component.tsx
index 81a2bbc3c..818df6bf8 100644
--- a/src/components/sidebar/sidebar.component.tsx
+++ b/src/components/sidebar/sidebar.component.tsx
@@ -1,4 +1,4 @@
-import React, { useRef, useCallback } from "react";
+import React, { useRef, useCallback, useContext } from "react";
 import { PaddingProps } from "styled-system";
 
 import Modal from "../modal";
@@ -13,6 +13,7 @@ import useLocale from "../../hooks/__internal__/useLocale";
 import { filterStyledSystemPaddingProps } from "../../style/utils";
 import useIsStickyFooterForm from "../../hooks/__internal__/useIsStickyFooterForm";
 import { TagProps } from "../../__internal__/utils/helpers/tags/tags";
+import TopModalContext from "../carbon-provider/top-modal-context";
 
 // TODO FE-5408 will investigate why React.RefObject<T> produces a failed prop type when current = null
 type CustomRefObject<T> = {
@@ -106,6 +107,8 @@ export const Sidebar = React.forwardRef<HTMLDivElement, SidebarProps>(
       [ref]
     );
 
+    const { topModal } = useContext(TopModalContext);
+
     const closeIcon = () => {
       if (!onCancel) return null;
       return (
@@ -127,7 +130,9 @@ export const Sidebar = React.forwardRef<HTMLDivElement, SidebarProps>(
 
     const sidebar = (
       <StyledSidebar
-        aria-modal={!enableBackgroundUI}
+        aria-modal={
+          !enableBackgroundUI && topModal?.contains(sidebarRef.current)
+        }
         aria-describedby={ariaDescribedBy}
         aria-label={ariaLabel}
         aria-labelledby={
diff --git a/src/components/sidebar/sidebar.spec.tsx b/src/components/sidebar/sidebar.spec.tsx
index 6d40dd2df..0cc0bffbd 100644
--- a/src/components/sidebar/sidebar.spec.tsx
+++ b/src/components/sidebar/sidebar.spec.tsx
@@ -15,6 +15,8 @@ import {
   testStyledSystemPadding,
 } from "../../__spec_helper__/test-utils";
 import guid from "../../__internal__/utils/helpers/guid";
+import Button from "../button";
+import CarbonProvider from "../carbon-provider";
 
 jest.mock("../../__internal__/utils/helpers/guid");
 
@@ -34,6 +36,25 @@ describe("Sidebar", () => {
   });
 
   describe("render", () => {
+    describe("when sidebar is open", () => {
+      it("has aria-modal attribute", () => {
+        wrapper = mount(
+          <CarbonProvider>
+            <Sidebar onCancel={spy} open>
+              <Button>Button</Button>
+              <Button>Button</Button>
+            </Sidebar>
+          </CarbonProvider>
+        );
+
+        expect(
+          wrapper.find(StyledSidebar).getDOMNode().getAttribute("aria-modal")
+        ).toBe("true");
+
+        wrapper.unmount();
+      });
+    });
+
     describe("when sidebar is closed", () => {
       it("sets all the correct classes", () => {
         wrapper = mount(<Sidebar open={false} onCancel={spy} />);
@@ -96,6 +117,23 @@ describe("Sidebar", () => {
           wrapper.find('[data-element="modal-background"]').length
         ).toEqual(0);
       });
+
+      it("does not have aria-modal attribute", () => {
+        wrapper = mount(
+          <CarbonProvider>
+            <Sidebar enableBackgroundUI onCancel={spy} open>
+              <Button>Button</Button>
+              <Button>Button</Button>
+            </Sidebar>
+          </CarbonProvider>
+        );
+
+        expect(
+          wrapper.find(StyledSidebar).getDOMNode().getAttribute("aria-modal")
+        ).toBe("false");
+
+        wrapper.unmount();
+      });
     });
 
     describe("when there is no onCancel prop", () => {


Test patch:
diff --git a/src/components/dialog-full-screen/dialog-full-screen.test.js b/src/components/dialog-full-screen/dialog-full-screen.test.js
index d8002e0e0..c37878d3f 100644
--- a/src/components/dialog-full-screen/dialog-full-screen.test.js
+++ b/src/components/dialog-full-screen/dialog-full-screen.test.js
@@ -6,6 +6,8 @@ import Button from "../button";
 import Textbox from "../textbox";
 import Pill from "../pill";
 import Form from "../form";
+import Box from "../box";
+import CarbonProvider from "../carbon-provider";
 import {
   dialogTitle,
   dialogSubtitle,
@@ -22,6 +24,7 @@ import {
   getDataElementByValue,
   tooltipPreview,
   getComponent,
+  getElement,
 } from "../../../cypress/locators/index";
 import CypressMountWithProviders from "../../../cypress/support/component-helper/cypress-mount";
 import { contentElement } from "../../../cypress/locators/content/index";
@@ -98,6 +101,35 @@ const NestedDialog = () => {
   );
 };
 
+const MultipleDialogsInDifferentProviders = () => {
+  const [isModal1Open, setIsModal1Open] = React.useState(false);
+  const [isModal2Open, setIsModal2Open] = React.useState(false);
+  return (
+    <>
+      <CarbonProvider>
+        <Box>
+          <Button onClick={() => setIsModal1Open(true)}>Open Modal 1</Button>
+          <DialogFullScreen
+            title="Full Screen Dialog"
+            open={isModal1Open}
+            onCancel={() => setIsModal1Open(false)}
+          >
+            This is Modal 1
+            <Button onClick={() => setIsModal2Open(true)}>Open Modal 2</Button>
+          </DialogFullScreen>
+        </Box>
+      </CarbonProvider>
+      <CarbonProvider>
+        <Box>
+          <Dialog open={isModal2Open} onCancel={() => setIsModal2Open(false)}>
+            This is Modal 2
+          </Dialog>
+        </Box>
+      </CarbonProvider>
+    </>
+  );
+};
+
 const DialogFullScreenWithHeaderChildren = () => {
   const [isOpen, setIsOpen] = React.useState(true);
   const HeaderChildren = (
@@ -255,6 +287,38 @@ context("Testing DialogFullScreen component", () => {
       dialogFullScreenPreview().should("not.exist");
     });
 
+    it("should render nested dialogs with the aria-modal property only set on the top one", () => {
+      CypressMountWithProviders(<NestedDialog />);
+
+      openDialogByName(`Open ${mainDialogTitle}`).click();
+
+      getElement("dialog-full-screen")
+        .should("have.attr", "aria-modal")
+        .and("eq", "true");
+
+      openDialogByName(`Open ${nestedDialogTitle}`).click();
+
+      getElement("dialog-full-screen").should("not.have.attr", "aria-modal");
+
+      getElement("dialog").should("have.attr", "aria-modal").and("eq", "true");
+    });
+
+    it("should render nested dialogs with the aria-modal property only set on the top one, even when the dialogs are wrapped in separate CarbonProviders", () => {
+      cy.mount(<MultipleDialogsInDifferentProviders />);
+
+      openDialogByName("Open Modal 1").click();
+
+      getElement("dialog-full-screen")
+        .should("have.attr", "aria-modal")
+        .and("eq", "true");
+
+      openDialogByName("Open Modal 2").click();
+
+      getElement("dialog-full-screen").should("not.have.attr", "aria-modal");
+
+      getElement("dialog").should("have.attr", "aria-modal").and("eq", "true");
+    });
+
     it("should render DialogFullScreen component with aria-describedby", () => {
       CypressMountWithProviders(
         <DialogFullScreenComponent aria-describedby={testAria} />