Repository: Sage/carbon (ts)

Issue:
Toast - should focus container rather than close button when opened
### Desired behaviour

When opening a Toast, the focus should be placed on the toast container so that screenreader users hear the toast message. The container should not be focusable again with the keyboard and pressing tab should take the focus to the close button.

Note: even where the Toast does not have a close button, the focus should go to the toast container so that screenreader users have the message read out.

### Current behaviour

Focus goes to the close button where present, since Carbon version 109.3.0. In versions prior to that, and in all versions if the toast has no close button, the toast is not focused at all. When focus goes to the close button, screenreader users just hear "close button" but no information on what they are closing.

### Suggested Solution

As under "desired behaviour". Note: similar behaviour is already implemented in Dialog, DialogFullScreen, Sidebar and MenuFullScreen.

### CodeSandbox or Storybook URL

https://carbon.sage.com/?path=/docs/toast--dismissible

### Anything else we should know?

cc @tempertemper who requested it this morning

### Confidentiality

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

Pull request description:
fix(toast): ensure toast component has first focus when opened - FE-5281
fixes #5320 

### Proposed behaviour

Toast component should be first component to be focused


### Current behaviour

Currently Toast is not focusable

![toast_original](https://user-images.githubusercontent.com/56251247/182897070-73836c6d-9942-410f-a7a9-ea0bc31dd2f0.gif)

![toast-dissmissable-original](https://user-images.githubusercontent.com/56251247/182899376-6a6e117e-c28e-4c6d-98f0-4294045c483d.gif)

### 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
- [x] All themes are supported if required
- [x] Unit tests added or updated if required
- [x] Cypress automation tests added or updated if required
- [ ] 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

#### 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

N/A

### Testing instructions

Test that the Toast stories in Storybook work as expected with the new functionality and check for any regressions


https://codesandbox.io/s/carbon-quickstart-forked-j2p39d?file=/src/index.js

Patch:
diff --git a/src/__internal__/focus-trap/focus-trap.spec.tsx b/src/__internal__/focus-trap/focus-trap.spec.tsx
index 0fbb79900..4d9155c90 100644
--- a/src/__internal__/focus-trap/focus-trap.spec.tsx
+++ b/src/__internal__/focus-trap/focus-trap.spec.tsx
@@ -720,7 +720,7 @@ describe("FocusTrap", () => {
     it("should detect when additionalWrappers update and remove any elements no longer visible from focusableElements", async () => {
       const additionalRef = { current: null };
       const wrapperRef = { current: null };
-      const ADDITONAL_BUTTON = "Additional button";
+      const ADDITIONAL_BUTTON = "Additional button";
 
       const ChangingChild = () => {
         const [loading, setLoading] = useState(true);
@@ -732,7 +732,7 @@ describe("FocusTrap", () => {
         }, []);
 
         const children = loading ? (
-          <button type="button">{ADDITONAL_BUTTON}</button>
+          <button type="button">{ADDITIONAL_BUTTON}</button>
         ) : null;
 
         return <div ref={additionalRef}>{children}</div>;
@@ -756,7 +756,7 @@ describe("FocusTrap", () => {
           <ChangingChild />
         </ModalContext.Provider>
       );
-      const additionalButton = screen.getByText(ADDITONAL_BUTTON);
+      const additionalButton = screen.getByText(ADDITIONAL_BUTTON);
 
       tabPress();
       expect(screen.getByText(BUTTON_ONE)).toHaveFocus();
diff --git a/src/components/toast/toast.component.js b/src/components/toast/toast.component.js
index 904ec33d3..50b3b1ef0 100644
--- a/src/components/toast/toast.component.js
+++ b/src/components/toast/toast.component.js
@@ -1,4 +1,10 @@
-import React, { useCallback, useEffect, useMemo, useRef } from "react";
+import React, {
+  useCallback,
+  useEffect,
+  useMemo,
+  useRef,
+  useState,
+} from "react";
 import classNames from "classnames";
 import { TransitionGroup, CSSTransition } from "react-transition-group";
 import PropTypes from "prop-types";
@@ -44,6 +50,8 @@ const Toast = React.forwardRef(
 
     const focusedElementBeforeOpening = useRef();
 
+    const [tabIndex, setTabIndex] = useState(0);
+
     const refToPass = ref || toastRef;
 
     const componentClasses = useMemo(() => {
@@ -73,13 +81,14 @@ const Toast = React.forwardRef(
     }, [onDismiss, open, timeout]);
 
     useEffect(() => {
-      if (onDismiss && !disableAutoFocus) {
+      if (!disableAutoFocus) {
         if (open) {
           focusedElementBeforeOpening.current = document.activeElement;
-          closeIconRef.current?.focus();
+          toastContentNodeRef.current?.focus();
         } else if (focusedElementBeforeOpening.current) {
           focusedElementBeforeOpening.current.focus();
           focusedElementBeforeOpening.current = undefined;
+          setTabIndex(0);
         }
       }
     }, [open, onDismiss, disableAutoFocus]);
@@ -130,6 +139,10 @@ const Toast = React.forwardRef(
             {...tagComponent(restProps["data-component"] || "toast", restProps)}
             {...toastProps}
             ref={toastContentNodeRef}
+            {...(!disableAutoFocus && {
+              tabIndex,
+              onBlur: () => setTabIndex(undefined),
+            })}
           >
             {!isNotice && (
               <TypeIcon variant={toastProps.variant}>
diff --git a/src/components/toast/toast.spec.js b/src/components/toast/toast.spec.js
index 1fc496d5e..ece9985a0 100644
--- a/src/components/toast/toast.spec.js
+++ b/src/components/toast/toast.spec.js
@@ -1,5 +1,6 @@
 import React from "react";
 import { shallow, mount } from "enzyme";
+import { act } from "react-dom/test-utils";
 import guid from "../../__internal__/utils/helpers/guid";
 import Toast from "./toast.component";
 import {
@@ -173,10 +174,23 @@ describe("Toast", () => {
         expect(icon.exists()).toBe(true);
       });
 
-      it("auto focuses the close icon", () => {
-        const icon = wrapper.find("[data-element='close']").first();
+      it("auto focuses the toast component", () => {
+        const toast = wrapper.find(ToastStyle);
 
-        expect(icon).toBeFocused();
+        expect(toast).toBeFocused();
+      });
+
+      it("sets a tabIndex on the toast component and removes onBlur", () => {
+        const toast = wrapper.find(ToastStyle);
+
+        expect(toast.getDOMNode().hasAttribute("tabIndex")).toBe(true);
+
+        act(() => {
+          toast.simulate("blur");
+        });
+        wrapper.update();
+
+        expect(toast.getDOMNode().hasAttribute("tabIndex")).toBe(false);
       });
 
       it("when the toast closes, focus is returned to the element it was on before the toast opened", () => {
@@ -198,13 +212,41 @@ describe("Toast", () => {
         button.getDOMNode().focus();
 
         wrapper.setProps({ open: true });
-        const icon = wrapper.find("[data-element='close']").first();
-        expect(icon).toBeFocused();
+        const toast = wrapper.find("[data-component='toast']").first();
+        expect(toast).toBeFocused();
 
         wrapper.setProps({ open: false });
+        wrapper.update();
         expect(button).toBeFocused();
       });
 
+      it("when the toast closes and then re-opens the wrapper is focused again", () => {
+        const element = document.createElement("div");
+        const htmlElement = document.body.appendChild(element);
+        const WrapperComponent = (toastProps) => (
+          <>
+            <Button id="buttonId">A button</Button>
+            <Toast {...toastProps} />
+          </>
+        );
+
+        wrapper.unmount();
+        wrapper = mount(
+          <WrapperComponent onDismiss={onDismiss} open={false} />,
+          { attachTo: htmlElement }
+        );
+
+        wrapper.setProps({ open: true });
+        const toast = wrapper.find("[data-component='toast']").first();
+        expect(toast).toBeFocused();
+
+        wrapper.setProps({ open: false });
+        wrapper.update();
+
+        wrapper.setProps({ open: true });
+        expect(toast).toBeFocused();
+      });
+
       describe("calls onDismiss method when", () => {
         it("dismiss icon is clicked", () => {
           wrapper.find(IconButton).first().simulate("click");
@@ -243,14 +285,14 @@ describe("Toast", () => {
       });
 
       describe("with disableAutoFocus prop", () => {
-        it("does not auto focus the close icon", () => {
+        it("does not auto focus the Toast wrapper", () => {
           wrapper.unmount();
           wrapper = mount(
             <Toast open onDismiss={onDismiss} disableAutoFocus />
           );
-          const icon = wrapper.find("[data-element='close']").first();
-
-          expect(icon).not.toBeFocused();
+          const toast = wrapper.find(ToastStyle);
+          expect(toast.getDOMNode().hasAttribute("tabIndex")).toBe(false);
+          expect(toast).not.toBeFocused();
         });
 
         it("when the toast closes, focus is not returned to the element it was on before the toast opened", () => {
diff --git a/src/components/toast/toast.style.js b/src/components/toast/toast.style.js
index 61835ad76..55877e4fa 100644
--- a/src/components/toast/toast.style.js
+++ b/src/components/toast/toast.style.js
@@ -49,8 +49,11 @@ const ToastStyle = styled(MessageStyle)`
     margin-right: ${isCenter ? "auto" : "30px"};
   `}
 
-  &${animationName}-appear,
-  &${animationName}-enter {
+  :focus {
+    outline: none;
+  }
+
+  &${animationName}-appear, &${animationName}-enter {
     opacity: 0;
     transform: scale(0.5);
   }


Test patch:
diff --git a/src/components/dialog/dialog.test.js b/src/components/dialog/dialog.test.js
index b35cdf406..4258236f9 100644
--- a/src/components/dialog/dialog.test.js
+++ b/src/components/dialog/dialog.test.js
@@ -17,6 +17,8 @@ import {
   getComponent,
 } from "../../../cypress/locators/index";
 import CypressMountWithProviders from "../../../cypress/support/component-helper/cypress-mount";
+import Toast from "../toast";
+import toastComponent from "../../../cypress/locators/toast";
 
 const specialCharacters = ["mp150ú¿¡üßä", "!@#$%^*()_+-=~[];:.,?{}&\"'<>"];
 
@@ -48,6 +50,23 @@ const DialogComponent = ({ children, ...props }) => {
   );
 };
 
+const DialogComponentWithToast = () => {
+  const toastRef = React.useRef(null);
+  const [openToast, setOpenToast] = React.useState(false);
+  return (
+    <>
+      <Toast
+        ref={toastRef}
+        open={openToast}
+        onDismiss={() => setOpenToast(false)}
+      />
+      <Dialog additionalWrapperRefs={[toastRef]} open>
+        <Button onClick={() => setOpenToast(true)}>Open Toast</Button>
+      </Dialog>
+    </>
+  );
+};
+
 context("Testing Dialog component", () => {
   describe("should render Dialog component with props", () => {
     it.each([[0], [1], [100], [1000]])(
@@ -272,5 +291,16 @@ context("Testing Dialog component", () => {
       cy.get("body").tab({ shift: true });
       closeIconButton().should("be.focused");
     });
+
+    it("focuses Toast wrapper when ref passed to additionalWrapperRefs and focuses button that triggered opening on close", () => {
+      CypressMountWithProviders(<DialogComponentWithToast />);
+
+      buttonDataComponent().click();
+      toastComponent().should("be.focused");
+      closeIconButton().click();
+      buttonDataComponent().should("be.focused");
+      buttonDataComponent().click();
+      toastComponent().should("be.focused");
+    });
   });
 });
diff --git a/src/components/toast/toast.test.js b/src/components/toast/toast.test.js
index ca1a67ee7..0be0bd3f2 100644
--- a/src/components/toast/toast.test.js
+++ b/src/components/toast/toast.test.js
@@ -2,12 +2,12 @@ import * as React from "react";
 import Toast from ".";
 import { TOAST_COLORS } from "./toast.config";
 import CypressMountWithProviders from "../../../cypress/support/component-helper/cypress-mount";
+import { checkGoldenOutline } from "../../../cypress/support/component-helper/common-steps";
 
 import toastComponent from "../../../cypress/locators/toast";
 import { closeIconButton, getComponent } from "../../../cypress/locators/index";
 
-import { pressESCKey } from "../../../cypress/support/helper";
-import { checkGoldenOutline } from "../../../cypress/support/component-helper/common-steps";
+import { pressESCKey, pressTABKey } from "../../../cypress/support/helper";
 
 const specialCharacters = ["mp150ú¿¡üßä", "!@#$%^*()_+-=~[];:.,?{}&\"'<>"];
 const testData = "cypressData";
@@ -18,6 +18,7 @@ const colorTypes = [
   ["rgb(239, 103, 0)"],
 ];
 
+// eslint-disable-next-line react/prop-types
 const ToastComponent = ({ children, ...props }) => {
   const [isOpen, setIsOpen] = React.useState(true);
 
@@ -56,12 +57,10 @@ context("Testing Toast component", () => {
       toastComponent().should("not.exist");
     });
 
-    it("should render Toast component with focused close icon", () => {
+    it("should render Toast component with focus", () => {
       CypressMountWithProviders(<ToastComponent />);
 
-      closeIconButton().then(($el) => {
-        checkGoldenOutline($el);
-      });
+      toastComponent().should("be.focused");
     });
 
     it("should render Toast component with not focused close icon", () => {
@@ -174,6 +173,7 @@ context("Testing Toast component", () => {
     it("should render Toast component with notice variant with focused close icon", () => {
       CypressMountWithProviders(<ToastComponent variant="notice" open />);
 
+      pressTABKey(1);
       closeIconButton().then(($el) => {
         checkGoldenOutline($el);
       });