Repository: Sage/carbon (ts)

Issue:
Allow for ActionPopover to be focusable
### Desired behaviour

I would like to be able to set focus on ActionPopover or ActionPopoverMenuButton by passing a ref to it and calling ref.current?.focus()

### Current behaviour

For both of the components there is no ref property, wrapping in div and setting a ref or an id does not work either.

### Suggested Solution

_No response_

### CodeSandbox or Storybook URL

_No response_

### Confidentiality

- [X] I confirm there is no confidential or commercially sensitive information included.

Pull request:
feat(action-popover): allow menu button to be focused programmatically
fix #5589

### 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.
-->

Expose `focusButton` method to programmatically focus on the menu button using ref. 

### 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.
-->

`ActionPopover` menu button cannot be focused programmatically. 

### 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
- [x] Unit tests added or updated if required
- [ ] 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/action-popover/action-popover.component.tsx b/src/components/action-popover/action-popover.component.tsx
index e97b5afdf..f1179c360 100644
--- a/src/components/action-popover/action-popover.component.tsx
+++ b/src/components/action-popover/action-popover.component.tsx
@@ -4,6 +4,8 @@ import React, {
   useMemo,
   useEffect,
   useRef,
+  useImperativeHandle,
+  forwardRef,
 } from "react";
 import { MarginProps } from "styled-system";
 import invariant from "invariant";
@@ -70,249 +72,273 @@ export interface ActionPopoverProps extends MarginProps {
   "aria-describedby"?: string;
 }
 
+export type ActionPopoverHandle = {
+  focusButton: () => void;
+} | null;
+
 const onOpenDefault = () => {};
 const onCloseDefault = () => {};
 
-export const ActionPopover = ({
-  children,
-  id,
-  onOpen = onOpenDefault,
-  onClose = onCloseDefault,
-  rightAlignMenu,
-  renderButton,
-  placement = "bottom",
-  horizontalAlignment = "left",
-  submenuPosition = "left",
-  "aria-label": ariaLabel,
-  "aria-labelledby": ariaLabelledBy,
-  "aria-describedby": ariaDescribedBy,
-  ...rest
-}: ActionPopoverProps) => {
-  const l = useLocale();
-  const [isOpen, setOpenState] = useState(false);
-  const [focusIndex, setFocusIndex] = useState(0);
-  const [guid] = useState(createGuid());
-  const buttonRef = useRef<HTMLDivElement>(null);
-  const menu = useRef<HTMLUListElement>(null);
-
-  const hasProperChildren = useMemo(() => {
-    const incorrectChild = React.Children.toArray(children).find(
-      (child: React.ReactNode) => {
-        if (!React.isValidElement(child)) {
-          return true;
-        }
+export const ActionPopover = forwardRef<
+  ActionPopoverHandle,
+  ActionPopoverProps
+>(
+  (
+    {
+      children,
+      id,
+      onOpen = onOpenDefault,
+      onClose = onCloseDefault,
+      rightAlignMenu,
+      renderButton,
+      placement = "bottom",
+      horizontalAlignment = "left",
+      submenuPosition = "left",
+      "aria-label": ariaLabel,
+      "aria-labelledby": ariaLabelledBy,
+      "aria-describedby": ariaDescribedBy,
+      ...rest
+    },
+    ref,
+  ) => {
+    const l = useLocale();
+    const [isOpen, setOpenState] = useState(false);
+    const [focusIndex, setFocusIndex] = useState(0);
+    const [guid] = useState(createGuid());
+    const buttonRef = useRef<HTMLDivElement>(null);
+    const menu = useRef<HTMLUListElement>(null);
+
+    const hasProperChildren = useMemo(() => {
+      const incorrectChild = React.Children.toArray(children).find(
+        (child: React.ReactNode) => {
+          if (!React.isValidElement(child)) {
+            return true;
+          }
+
+          return (
+            child.type !== ActionPopoverItem &&
+            child.type !== ActionPopoverDivider
+          );
+        },
+      );
 
-        return (
-          child.type !== ActionPopoverItem &&
-          child.type !== ActionPopoverDivider
-        );
-      },
+      return !incorrectChild;
+    }, [children]);
+
+    const items = useMemo(() => getItems(children), [children]);
+
+    const firstFocusableItem = findFirstFocusableItem(items);
+
+    const lastFocusableItem = findLastFocusableItem(items);
+
+    invariant(
+      hasProperChildren,
+      `ActionPopover only accepts children of type \`${ActionPopoverItem.displayName}\`` +
+        ` and \`${ActionPopoverDivider.displayName}\`.`,
     );
 
-    return !incorrectChild;
-  }, [children]);
+    const mappedPlacement = useMemo(() => {
+      if (placement === "top" && !rightAlignMenu) {
+        return "top-end";
+      }
 
-  const items = useMemo(() => getItems(children), [children]);
+      if (placement === "top" && rightAlignMenu) {
+        return "top-start";
+      }
 
-  const firstFocusableItem = findFirstFocusableItem(items);
+      if (placement === "bottom" && rightAlignMenu) {
+        return "bottom-start";
+      }
 
-  const lastFocusableItem = findLastFocusableItem(items);
+      return "bottom-end";
+    }, [placement, rightAlignMenu]);
 
-  invariant(
-    hasProperChildren,
-    `ActionPopover only accepts children of type \`${ActionPopoverItem.displayName}\`` +
-      ` and \`${ActionPopoverDivider.displayName}\`.`,
-  );
+    const setOpen = useCallback(
+      (value) => {
+        if (value && !isOpen) {
+          onOpen();
+        }
+        if (!value && isOpen) {
+          onClose();
+        }
+        setOpenState(value);
+      },
+      [isOpen, onOpen, onClose],
+    );
 
-  const mappedPlacement = useMemo(() => {
-    if (placement === "top" && !rightAlignMenu) {
-      return "top-end";
-    }
+    const focusButton = useCallback(() => {
+      const button = buttonRef.current?.querySelector<HTMLElement>(
+        "[data-element='action-popover-button']",
+      );
 
-    if (placement === "top" && rightAlignMenu) {
-      return "top-start";
-    }
+      button?.focus();
+    }, []);
 
-    if (placement === "bottom" && rightAlignMenu) {
-      return "bottom-start";
-    }
+    useImperativeHandle<ActionPopoverHandle, ActionPopoverHandle>(
+      ref,
+      () => ({
+        focusButton() {
+          focusButton();
+        },
+      }),
+      [focusButton],
+    );
 
-    return "bottom-end";
-  }, [placement, rightAlignMenu]);
+    const onButtonClick = useCallback(
+      (e) => {
+        e.stopPropagation();
+        const isOpening = !isOpen;
+        setFocusIndex(firstFocusableItem);
+        setOpen(isOpening);
+        if (!isOpening) {
+          // Closing the menu should focus the MenuButton
+          focusButton();
+        }
+      },
+      [isOpen, firstFocusableItem, setOpen, focusButton],
+    );
 
-  const setOpen = useCallback(
-    (value) => {
-      if (value && !isOpen) {
-        onOpen();
-      }
-      if (!value && isOpen) {
-        onClose();
-      }
-      setOpenState(value);
-    },
-    [isOpen, onOpen, onClose],
-  );
+    // Keyboard commands implemented as recommended by WAI-ARIA best practices
+    // https://www.w3.org/TR/wai-aria-practices/examples/menu-button/menu-button-actions.html
+
+    const onButtonKeyDown = useCallback(
+      (e) => {
+        if (
+          Events.isSpaceKey(e) ||
+          Events.isDownKey(e) ||
+          Events.isEnterKey(e) ||
+          Events.isUpKey(e)
+        ) {
+          e.preventDefault();
+          e.stopPropagation();
+          setFocusIndex(
+            Events.isUpKey(e) ? lastFocusableItem : firstFocusableItem,
+          );
+          setOpen(true);
+        }
+      },
+      [firstFocusableItem, lastFocusableItem, setOpen],
+    );
 
-  const focusButton = useCallback(() => {
-    const button = buttonRef.current?.querySelector<HTMLElement>(
-      "[data-element='action-popover-button']",
+    const handleEscapeKey = useCallback(
+      (e) => {
+        /* istanbul ignore else */
+        if (Events.isEscKey(e)) {
+          setOpen(false);
+          focusButton();
+        }
+      },
+      [setOpen, focusButton],
     );
 
-    button?.focus();
-  }, []);
-
-  const onButtonClick = useCallback(
-    (e) => {
-      e.stopPropagation();
-      const isOpening = !isOpen;
-      setFocusIndex(firstFocusableItem);
-      setOpen(isOpening);
-      if (!isOpening) {
-        // Closing the menu should focus the MenuButton
-        focusButton();
-      }
-    },
-    [isOpen, firstFocusableItem, setOpen, focusButton],
-  );
+    useModalManager({
+      open: isOpen,
+      closeModal: handleEscapeKey,
+      modalRef: buttonRef,
+    });
 
-  // Keyboard commands implemented as recommended by WAI-ARIA best practices
-  // https://www.w3.org/TR/wai-aria-practices/examples/menu-button/menu-button-actions.html
+    useEffect(() => {
+      const handler = ({ target }: MouseEvent) => {
+        // If the event didn't come from part of this component, close the menu.
+        // There will be multiple document click listeners but we cant prevent propagation because it will interfere with
+        // other instances on the same page
 
-  const onButtonKeyDown = useCallback(
-    (e) => {
-      if (Events.isSpaceKey(e) || Events.isDownKey(e) || Events.isEnterKey(e)) {
-        e.preventDefault();
-        e.stopPropagation();
-        setFocusIndex(firstFocusableItem);
-        setOpen(true);
-      } else if (Events.isUpKey(e)) {
-        e.preventDefault();
-        e.stopPropagation();
-        setFocusIndex(lastFocusableItem);
-        setOpen(true);
-      }
-    },
-    [firstFocusableItem, lastFocusableItem, setOpen],
-  );
-
-  const handleEscapeKey = useCallback(
-    (e) => {
-      /* istanbul ignore else */
-      if (Events.isEscKey(e)) {
-        setOpen(false);
-        focusButton();
-      }
-    },
-    [setOpen, focusButton],
-  );
-
-  useModalManager({
-    open: isOpen,
-    closeModal: handleEscapeKey,
-    modalRef: buttonRef,
-  });
-
-  useEffect(() => {
-    const handler = ({ target }: MouseEvent) => {
-      // If the event didn't come from part of this component, close the menu.
-      // There will be multiple document click listeners but we cant prevent propagation because it will interfere with
-      // other instances on the same page
-
-      const isInMenu = menu?.current?.contains(target as Node);
-      const isInButton = buttonRef?.current?.contains(target as Node);
-
-      if (!isInMenu && !isInButton) {
-        setOpen(false);
+        const isInMenu = menu?.current?.contains(target as Node);
+        const isInButton = buttonRef?.current?.contains(target as Node);
+
+        if (!isInMenu && !isInButton) {
+          setOpen(false);
+        }
+      };
+      const event = "click";
+      document.addEventListener(event, handler, { capture: true });
+
+      return function cleanup() {
+        document.removeEventListener(event, handler, { capture: true });
+      };
+    }, [setOpen]);
+
+    const menuButton = (menuID: string) => {
+      if (renderButton) {
+        return renderButton({
+          tabIndex: isOpen ? -1 : 0,
+          "data-element": "action-popover-button",
+          ariaAttributes: {
+            "aria-haspopup": "true",
+            "aria-label": ariaLabel || l.actionPopover.ariaLabel(),
+            "aria-labelledby": ariaLabelledBy,
+            "aria-describedby": ariaDescribedBy,
+            "aria-controls": menuID,
+            "aria-expanded": `${isOpen}`,
+          },
+        });
       }
+
+      return (
+        <StyledButtonIcon
+          role="button"
+          aria-haspopup="true"
+          aria-label={ariaLabel || l.actionPopover.ariaLabel()}
+          aria-labelledby={ariaLabelledBy}
+          aria-describedby={ariaDescribedBy}
+          aria-controls={menuID}
+          aria-expanded={isOpen}
+          tabIndex={isOpen ? -1 : 0}
+          data-element="action-popover-button"
+        >
+          <ButtonIcon type="ellipsis_vertical" />
+        </StyledButtonIcon>
+      );
     };
-    const event = "click";
-    document.addEventListener(event, handler, { capture: true });
 
-    return function cleanup() {
-      document.removeEventListener(event, handler, { capture: true });
+    const parentID = id || `ActionPopoverButton_${guid}`;
+    const menuID = `ActionPopoverMenu_${guid}`;
+    const menuProps = {
+      buttonRef,
+      parentID,
+      setFocusIndex,
+      focusIndex,
+      menuID,
+      isOpen,
+      setOpen,
+      rightAlignMenu,
+      placement,
+      horizontalAlignment,
     };
-  }, [setOpen]);
-
-  const menuButton = (menuID: string) => {
-    if (renderButton) {
-      return renderButton({
-        tabIndex: isOpen ? -1 : 0,
-        "data-element": "action-popover-button",
-        ariaAttributes: {
-          "aria-haspopup": "true",
-          "aria-label": ariaLabel || l.actionPopover.ariaLabel(),
-          "aria-labelledby": ariaLabelledBy,
-          "aria-describedby": ariaDescribedBy,
-          "aria-controls": menuID,
-          "aria-expanded": `${isOpen}`,
-        },
-      });
-    }
 
     return (
-      <StyledButtonIcon
-        role="button"
-        aria-haspopup="true"
-        aria-label={ariaLabel || l.actionPopover.ariaLabel()}
-        aria-labelledby={ariaLabelledBy}
-        aria-describedby={ariaDescribedBy}
-        aria-controls={menuID}
-        aria-expanded={isOpen}
-        tabIndex={isOpen ? -1 : 0}
-        data-element="action-popover-button"
+      <MenuButton
+        id={parentID}
+        data-component="action-popover-wrapper"
+        data-role="action-popover-wrapper"
+        {...{ onKeyDown: onButtonKeyDown, onClick: onButtonClick, isOpen }}
+        ref={buttonRef}
+        {...rest}
       >
-        <ButtonIcon type="ellipsis_vertical" />
-      </StyledButtonIcon>
+        {menuButton(menuID)}
+        <ActionPopoverContext.Provider
+          value={{
+            setOpenPopover: setOpen,
+            focusButton,
+            submenuPosition,
+            isOpenPopover: isOpen,
+          }}
+        >
+          {isOpen && (
+            <Popover placement={mappedPlacement} reference={buttonRef}>
+              <ActionPopoverMenu
+                data-component="action-popover"
+                ref={menu}
+                {...menuProps}
+              >
+                {children}
+              </ActionPopoverMenu>
+            </Popover>
+          )}
+        </ActionPopoverContext.Provider>
+      </MenuButton>
     );
-  };
-
-  const parentID = id || `ActionPopoverButton_${guid}`;
-  const menuID = `ActionPopoverMenu_${guid}`;
-  const menuProps = {
-    buttonRef,
-    parentID,
-    setFocusIndex,
-    focusIndex,
-    menuID,
-    isOpen,
-    setOpen,
-    rightAlignMenu,
-    placement,
-    horizontalAlignment,
-  };
-
-  return (
-    <MenuButton
-      id={parentID}
-      data-component="action-popover-wrapper"
-      data-role="action-popover-wrapper"
-      {...{ onKeyDown: onButtonKeyDown, onClick: onButtonClick, isOpen }}
-      ref={buttonRef}
-      {...rest}
-    >
-      {menuButton(menuID)}
-      <ActionPopoverContext.Provider
-        value={{
-          setOpenPopover: setOpen,
-          focusButton,
-          submenuPosition,
-          isOpenPopover: isOpen,
-        }}
-      >
-        {isOpen && (
-          <Popover placement={mappedPlacement} reference={buttonRef}>
-            <ActionPopoverMenu
-              data-component="action-popover"
-              ref={menu}
-              {...menuProps}
-            >
-              {children}
-            </ActionPopoverMenu>
-          </Popover>
-        )}
-      </ActionPopoverContext.Provider>
-    </MenuButton>
-  );
-};
+  },
+);
 
 export default ActionPopover;
diff --git a/src/components/action-popover/action-popover.mdx b/src/components/action-popover/action-popover.mdx
index b86d812bc..b9eacc6ee 100644
--- a/src/components/action-popover/action-popover.mdx
+++ b/src/components/action-popover/action-popover.mdx
@@ -190,6 +190,13 @@ It is possible to utilise the selected and onClick props on the FlatTableRow to
 
 <Canvas of={ActionPopoverStories.OpeningAModal} />
 
+### Focusing the Menu Button programmatically
+
+The component exposes a `focusButton` function that supports programmatically focusing the menu button which can be called 
+by passing a `ref` to the component.
+
+<Canvas of={ActionPopoverStories.FocusButtonProgrammatically} />
+
 ## Props
 
 ### ActionPopover Props
diff --git a/src/components/action-popover/action-popover.stories.tsx b/src/components/action-popover/action-popover.stories.tsx
index 0c6ad9798..c64921f3d 100644
--- a/src/components/action-popover/action-popover.stories.tsx
+++ b/src/components/action-popover/action-popover.stories.tsx
@@ -1,4 +1,4 @@
-import React, { useState } from "react";
+import React, { useState, useRef } from "react";
 import { Meta, StoryObj } from "@storybook/react";
 import {
   ActionPopover,
@@ -6,6 +6,8 @@ import {
   ActionPopoverItem,
   ActionPopoverMenu,
   ActionPopoverMenuButton,
+  RenderButtonProps,
+  ActionPopoverHandle,
 } from ".";
 import Link from "../link";
 import Box from "../box";
@@ -20,6 +22,7 @@ import {
 import Confirm from "../confirm";
 import { Accordion } from "../accordion";
 import Dialog from "../dialog";
+import Button from "../button";
 
 import generateStyledSystemProps from "../../../.storybook/utils/styled-system-props";
 
@@ -739,3 +742,60 @@ export const ActionPopoverNestedInDialog: Story = () => {
   );
 };
 ActionPopoverNestedInDialog.storyName = "Action Popover Nested in Dialog";
+
+export const FocusButtonProgrammatically = () => {
+  const ref = useRef<ActionPopoverHandle>(null);
+  const refMore = useRef<ActionPopoverHandle>(null);
+
+  const renderButton = (props: RenderButtonProps) => (
+    <ActionPopoverMenuButton
+      buttonType="tertiary"
+      iconType="ellipsis_vertical"
+      iconPosition="after"
+      size="small"
+      aria-label={undefined}
+      {...props}
+    >
+      More
+    </ActionPopoverMenuButton>
+  );
+
+  return (
+    <>
+      <Button
+        onClick={() => {
+          ref.current?.focusButton();
+        }}
+      >
+        Focus
+      </Button>
+      <ActionPopover ref={ref}>
+        <ActionPopoverItem icon="email" onClick={() => {}}>
+          Email Invoice
+        </ActionPopoverItem>
+        <ActionPopoverDivider />
+        <ActionPopoverItem onClick={() => {}} icon="delete">
+          Delete
+        </ActionPopoverItem>
+      </ActionPopover>
+
+      <Button
+        onClick={() => {
+          refMore.current?.focusButton();
+        }}
+      >
+        Focus More
+      </Button>
+      <ActionPopover renderButton={renderButton} ref={refMore} mt={3}>
+        <ActionPopoverItem icon="email" onClick={() => {}}>
+          Email Invoice
+        </ActionPopoverItem>
+        <ActionPopoverDivider />
+        <ActionPopoverItem onClick={() => {}} icon="delete">
+          Delete
+        </ActionPopoverItem>
+      </ActionPopover>
+    </>
+  );
+};
+FocusButtonProgrammatically.storyName = "Focus Button Programmatically";
diff --git a/src/components/action-popover/index.ts b/src/components/action-popover/index.ts
index 8c3525402..9167bc24c 100644
--- a/src/components/action-popover/index.ts
+++ b/src/components/action-popover/index.ts
@@ -8,3 +8,4 @@ export { default as ActionPopoverMenuButton } from "./action-popover-menu-button
 export type { ActionPopoverMenuButtonProps } from "./action-popover-menu-button/action-popover-menu-button.component";
 export { default as ActionPopoverDivider } from "./action-popover-divider/action-popover-divider.component";
 export type { RenderButtonProps } from "./action-popover.component";
+export type { ActionPopoverHandle } from "./action-popover.component";


Test patch:
diff --git a/src/components/action-popover/action-popover.test.tsx b/src/components/action-popover/action-popover.test.tsx
index 4725f1919..3df7bec4a 100644
--- a/src/components/action-popover/action-popover.test.tsx
+++ b/src/components/action-popover/action-popover.test.tsx
@@ -1,4 +1,4 @@
-import React from "react";
+import React, { useRef } from "react";
 import { ThemeProvider } from "styled-components";
 import * as floatingUi from "@floating-ui/dom";
 import { render, screen, act } from "@testing-library/react";
@@ -12,7 +12,9 @@ import {
   ActionPopoverMenu,
   ActionPopoverMenuButton,
   ActionPopoverProps,
+  ActionPopoverHandle,
 } from "./index";
+import Button from "../button";
 import iconUnicodes from "../icon/icon-unicodes";
 import guid from "../../__internal__/utils/helpers/guid";
 
@@ -902,6 +904,35 @@ test("an error is thrown, with appropriate error message, if invalid children ar
   globalConsoleSpy.mockRestore();
 });
 
+test("should call the exposed `focusButton` method and focus the toggle button", async () => {
+  const MockComponent = () => {
+    const ref = useRef<ActionPopoverHandle>(null);
+
+    return (
+      <>
+        <Button
+          onClick={() => {
+            ref.current?.focusButton();
+          }}
+        >
+          Focus
+        </Button>
+        <ActionPopover ref={ref}>
+          <ActionPopoverItem onClick={() => {}}>foo</ActionPopoverItem>
+        </ActionPopover>
+      </>
+    );
+  };
+
+  const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
+  render(<MockComponent />);
+
+  const button = screen.getByRole("button", { name: "Focus" });
+  await user.click(button);
+
+  expect(screen.getByRole("button", { name: "actions" })).toHaveFocus();
+});
+
 describe("when an item has a submenu with default (left) alignment", () => {
   it("renders the appropriate icon with the correct alignment", async () => {
     const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
@@ -1992,6 +2023,49 @@ describe("when the renderButton prop is passed", () => {
     expect(menuButton).toHaveAccessibleName("test label");
     expect(menuButton).toHaveAccessibleDescription("test description");
   });
+
+  it("should call the exposed `focusButton` method and focus the menu button", async () => {
+    const MockComponent = () => {
+      const ref = useRef<ActionPopoverHandle>(null);
+
+      return (
+        <>
+          <Button
+            onClick={() => {
+              ref.current?.focusButton();
+            }}
+          >
+            Focus
+          </Button>
+          <ActionPopover
+            ref={ref}
+            renderButton={(props) => (
+              <ActionPopoverMenuButton
+                buttonType="tertiary"
+                iconType="dropdown"
+                iconPosition="after"
+                size="small"
+                aria-label={undefined}
+                {...props}
+              >
+                Foo
+              </ActionPopoverMenuButton>
+            )}
+          >
+            <ActionPopoverItem onClick={() => {}}>foo</ActionPopoverItem>
+          </ActionPopover>
+        </>
+      );
+    };
+
+    const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
+    render(<MockComponent />);
+
+    const button = screen.getByRole("button", { name: "Focus" });
+    await user.click(button);
+
+    expect(screen.getByRole("button", { name: "Foo" })).toHaveFocus();
+  });
 });
 
 describe("When ActionPopoverMenu contains multiple disabled items", () => {