Repository: Sage/carbon (ts)

Issue:
The TAB key sequense is lose when it is used in a Popover component with Radio buttons child
### Description

When I move using the TAB key into a Popover component the navigation jump to the View site information of the browser (side to the URL)

### Reproduction

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

### Steps to reproduce

Press `TAB` key until `With Radio children` button
Press `ENTER`
Press `TAB` key until the last element of the list
Press `TAB` key

### JIRA ticket numbers (Sage only)

SBS-113991

### Suggested solution

_No response_

### Carbon version

142.13.5

### Design tokens version

_No response_

### 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:
fix(popover-container): ensure that tab sequence is not lost when container has radio buttons
This fix ensures that if the last focusable element in a popover container is a radio button, that the tab sequence is not affected once focus is outside of the container

fixes #7067

### Proposed behaviour

https://github.com/user-attachments/assets/5343bd1b-345a-4576-95b1-3df40374833a

### Current behaviour

https://github.com/user-attachments/assets/7e741308-4f29-4d7e-96ae-984f19b466ff

### 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
- [ ] 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

N/A

### Testing instructions

**Steps to reproduce**
Using the new `With Radio Button` example:
- Press TAB key until you reach the `with Radio children` button.
- Press ENTER to open the popover container.
- Press TAB key until the last element of the list.
- Press TAB key.
- Focus should move to button next to the popover container's opening button.

There should also be no functional regressions or changes to the other `PopoverContainer` examples.

Patch:
diff --git a/src/__internal__/focus-trap/focus-trap.component.tsx b/src/__internal__/focus-trap/focus-trap.component.tsx
index 751d2e799..8432ba6b4 100644
--- a/src/__internal__/focus-trap/focus-trap.component.tsx
+++ b/src/__internal__/focus-trap/focus-trap.component.tsx
@@ -279,7 +279,7 @@ const FocusTrap = ({
     onFocus: updateCurrentFocusedElement,
   });
 
-  // passes focusProps, sets tabIndex and onBlur if no tabIndex has been expicitly set on child
+  // passes focusProps, sets tabIndex and onBlur if no tabIndex has been explicitly set on child
   const clonedChildren = React.Children.map(children, (child) => {
     const focusableChild = child as React.ReactElement;
     return React.cloneElement(
diff --git a/src/components/popover-container/popover-container.component.tsx b/src/components/popover-container/popover-container.component.tsx
index 5889c3b4a..5d707157d 100644
--- a/src/components/popover-container/popover-container.component.tsx
+++ b/src/components/popover-container/popover-container.component.tsx
@@ -30,6 +30,7 @@ import useFocusPortalContent from "../../hooks/__internal__/useFocusPortalConten
 import tagComponent, {
   TagProps,
 } from "../../__internal__/utils/helpers/tags/tags";
+import { defaultFocusableSelectors } from "../../__internal__/focus-trap/focus-trap-utils";
 
 export interface RenderOpenProps {
   tabIndex: number;
@@ -272,6 +273,38 @@ export const PopoverContainer = ({
     closePopover,
   );
 
+  const onFocusNextElement = useCallback((ev) => {
+    const allFocusableElements: HTMLElement[] = Array.from(
+      document.querySelectorAll(defaultFocusableSelectors) ||
+        /* istanbul ignore next */ [],
+    );
+    const filteredElements = allFocusableElements.filter(
+      (el) => el === openButtonRef.current || Number(el.tabIndex) !== -1,
+    );
+
+    const openButtonRefIndex = filteredElements.indexOf(
+      openButtonRef.current as HTMLElement,
+    );
+
+    filteredElements[openButtonRefIndex + 1].focus();
+    closePopover(ev);
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, []);
+
+  const handleFocusGuard = (
+    direction: "prev" | "next",
+    ev: React.FocusEvent<HTMLElement>,
+  ) => {
+    if (direction === "next" && onFocusNextElement) {
+      // Focus the next focusable element outside of the popover
+      onFocusNextElement(ev);
+      return;
+    }
+
+    // istanbul ignore else
+    if (direction === "prev") openButtonRef.current?.focus();
+  };
+
   const renderOpenComponentProps = {
     tabIndex: 0,
     "aria-expanded": isOpen,
@@ -335,7 +368,23 @@ export const PopoverContainer = ({
         </FocusTrap>
       </ModalContext.Provider>
     ) : (
-      popover()
+      <>
+        <div
+          data-element="tab-guard-top"
+          // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
+          tabIndex={0}
+          aria-hidden
+          onFocus={(ev) => handleFocusGuard("prev", ev)}
+        />
+        {popover()}
+        <div
+          data-element="tab-guard-bottom"
+          // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
+          tabIndex={0}
+          aria-hidden
+          onFocus={(ev) => handleFocusGuard("next", ev)}
+        />
+      </>
     );
 
   return (
diff --git a/src/components/popover-container/popover-container.pw.tsx b/src/components/popover-container/popover-container.pw.tsx
index 1b939d41f..1021f0e5c 100644
--- a/src/components/popover-container/popover-container.pw.tsx
+++ b/src/components/popover-container/popover-container.pw.tsx
@@ -34,6 +34,7 @@ import {
   WithRenderCloseButtonComponent,
   PopoverContainerComponentCoverButton,
   PopoverContainerFocusOrder,
+  WithRadioButtons,
 } from "../popover-container/components.test-pw";
 import Portrait from "../portrait";
 
@@ -624,6 +625,25 @@ test.describe("Check props of Popover Container component", () => {
     await expect(third).toBeFocused();
   });
 
+  test("should focus the next focusable element outside of the container once finished keyboard navigating through the container's content", async ({
+    mount,
+    page,
+  }) => {
+    await mount(<WithRadioButtons />);
+
+    const openButton = page.getByRole("button", { name: "open" });
+    const container = popoverContainerContent(page);
+    const additionalButton = page.getByRole("button", { name: "foo" });
+
+    await openButton.click();
+    await page.keyboard.press("Tab"); // focus on first radio button
+    await page.keyboard.press("Tab"); // focus on close icon
+    await page.keyboard.press("Tab"); // focus outside of container and on to additional button
+
+    await expect(container).not.toBeVisible();
+    await expect(additionalButton).toBeFocused();
+  });
+
   test.describe("Accessibility tests", () => {
     test("should check accessibility for Default example", async ({
       mount,


Test patch:
diff --git a/src/components/popover-container/components.test-pw.tsx b/src/components/popover-container/components.test-pw.tsx
index 4c624a366..646437080 100644
--- a/src/components/popover-container/components.test-pw.tsx
+++ b/src/components/popover-container/components.test-pw.tsx
@@ -7,6 +7,7 @@ import Pill from "../pill";
 import Badge from "../badge";
 import Box from "../box";
 import { Select, Option } from "../select";
+import RadioButton, { RadioButtonGroup } from "../radio-button";
 import PopoverContainer, {
   PopoverContainerProps,
 } from "./popover-container.component";
@@ -434,3 +435,31 @@ export const PopoverContainerFocusOrder = (
     </Box>
   );
 };
+
+export const WithRadioButtons = () => {
+  const [open1, setOpen1] = useState(false);
+  return (
+    <Box padding="25px" display="inline-flex">
+      <PopoverContainer
+        position="left"
+        onOpen={() => setOpen1(true)}
+        onClose={() => setOpen1(false)}
+        open={open1}
+        renderOpenComponent={({ ref, onClick }) => (
+          <Button aria-label="open" ref={ref} onClick={onClick}>
+            With Radio children
+          </Button>
+        )}
+        p={0}
+      >
+        <Box display="flex" justifyContent="space-between" p={2}>
+          <RadioButtonGroup name="bar">
+            <RadioButton value="1" label="radio 1" />
+            <RadioButton value="2" label="radio 2" />
+          </RadioButtonGroup>
+        </Box>
+      </PopoverContainer>
+      <Button>foo</Button>
+    </Box>
+  );
+};
diff --git a/src/components/popover-container/popover-container-test.stories.tsx b/src/components/popover-container/popover-container-test.stories.tsx
index 2286ca54f..338e758ce 100644
--- a/src/components/popover-container/popover-container-test.stories.tsx
+++ b/src/components/popover-container/popover-container-test.stories.tsx
@@ -11,6 +11,7 @@ import Typography from "../typography";
 import Search from "../search";
 import IconButton from "../icon-button";
 import Icon from "../icon";
+import RadioButton, { RadioButtonGroup } from "../radio-button";
 
 export default {
   title: "Popover Container/Test",
@@ -22,6 +23,7 @@ export default {
     "InsideMenu",
     "InsideMenuWithOpenButton",
     "WithFullWidthButton",
+    "WithRadioButtons",
   ],
   parameters: {
     info: { disable: true },
@@ -246,3 +248,32 @@ export const WithFullWidthButton = () => {
     </PopoverContainer>
   );
 };
+
+export const WithRadioButtons = () => {
+  const [open1, setOpen1] = useState(false);
+
+  return (
+    <Box padding="25px" display="inline-flex">
+      <PopoverContainer
+        position="left"
+        onOpen={() => setOpen1(true)}
+        onClose={() => setOpen1(false)}
+        open={open1}
+        renderOpenComponent={({ ref, onClick }) => (
+          <Button aria-label="Notifications" ref={ref} onClick={onClick}>
+            With Radio children
+          </Button>
+        )}
+        p={0}
+      >
+        <Box display="flex" justifyContent="space-between" p={2}>
+          <RadioButtonGroup name="bar">
+            <RadioButton value="1" label="radio 1" />
+            <RadioButton value="2" label="radio 2" />
+          </RadioButtonGroup>
+        </Box>
+      </PopoverContainer>
+      <Button>foo</Button>
+    </Box>
+  );
+};
diff --git a/src/components/popover-container/popover-container.test.tsx b/src/components/popover-container/popover-container.test.tsx
index e5efb7090..60b8eff0d 100644
--- a/src/components/popover-container/popover-container.test.tsx
+++ b/src/components/popover-container/popover-container.test.tsx
@@ -12,6 +12,8 @@ import { testStyledSystemPadding } from "../../__spec_helper__/__internal__/test
 import PopoverContainer from "./popover-container.component";
 import { Select, Option } from "../select";
 import useMediaQuery from "../../hooks/useMediaQuery";
+import Button from "../button";
+import RadioButton, { RadioButtonGroup } from "../radio-button";
 
 jest.mock("../../hooks/useMediaQuery");
 
@@ -531,6 +533,89 @@ describe("closing the popup", () => {
   });
 });
 
+test("when content is navigated via keyboard, the next focusable item should be focused and popup closed", async () => {
+  const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
+  render(
+    <>
+      <PopoverContainer
+        position="left"
+        renderOpenComponent={({ ref, onClick }) => (
+          <Button aria-label="open button" ref={ref} onClick={onClick}>
+            Open
+          </Button>
+        )}
+      >
+        <RadioButtonGroup name="bar">
+          <RadioButton value="1" label="radio 1" />
+          <RadioButton value="2" label="radio 2" />
+        </RadioButtonGroup>
+      </PopoverContainer>
+      <Button>Example Button</Button>
+    </>,
+  );
+
+  const openButton = screen.getByRole("button", { name: "open button" });
+  await user.click(openButton);
+  await user.tab(); // tab to close icon
+  await user.tab(); // tab to RadioButtonGroup
+  await user.tab(); // tab to Example Button (outside of popup)
+
+  const popup = await screen.findByRole("dialog");
+  await waitFor(() => expect(popup).not.toBeVisible());
+
+  const exampleButton = screen.getByRole("button", { name: "Example Button" });
+  expect(exampleButton).toHaveFocus();
+});
+
+test("when the popover is opened and shift tab key is pressed, the open button should be focused and popup closed", async () => {
+  const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
+  render(
+    <>
+      <PopoverContainer
+        position="left"
+        renderOpenComponent={({ ref, onClick }) => (
+          <Button aria-label="open button" ref={ref} onClick={onClick}>
+            Open
+          </Button>
+        )}
+      >
+        <RadioButtonGroup name="bar">
+          <RadioButton value="1" label="radio 1" />
+          <RadioButton value="2" label="radio 2" />
+        </RadioButtonGroup>
+      </PopoverContainer>
+      <Button>Example Button</Button>
+    </>,
+  );
+
+  const openButton = screen.getByRole("button", { name: "open button" });
+  await user.click(openButton);
+  await user.tab(); // tab to close icon
+  await user.tab(); // tab to content
+  await user.tab({ shift: true }); // shift tab back to close icon
+  await user.tab({ shift: true }); // shift tab back to the opening trigger element
+
+  const popup = await screen.findByRole("dialog");
+  await waitFor(() => expect(popup).not.toBeVisible());
+  expect(openButton).toHaveFocus();
+});
+
+test("if only the open trigger is the only focusable element on screen, when the popover is opened and tab key is used to navigate content, it should navigate back to the opening trigger", async () => {
+  const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
+  render(
+    <>
+      <PopoverContainer title="My popup" open />
+    </>,
+  );
+
+  const openButton = screen.getByRole("button", { name: "My popup" });
+  await user.click(openButton);
+  await user.tab(); // tab to close icon
+  await user.tab(); // tab back out of content to the opening trigger element
+
+  expect(openButton).toHaveFocus();
+});
+
 testStyledSystemPadding(
   (props) => (
     <PopoverContainer open title="My popup" {...props}>