Repository: Sage/carbon (ts)

Issue:
[ActionPopover] - Add aria-describedby support for menu button 
### Description

When using Action Popover default button or an IconButton component to open the menu, it can require to attach a text to provide context. Adding support for aria-describedby will provide the option to link the button to a descriptive text.

### Suggested solution

Adding support for aria-describedby

### Demo URL

_No response_

### Alternatives

_No response_

### Additional context

_No response_

### Confidentiality

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

Pull request:
feat(action-popover): add aria-labelledby and aria-describedby props
fix #7107

### 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.
-->

- Add aria-labelledby and aria-describedby props to ActionPopover. 
- Ensure that aria attributes are set on ActionPopoverMenuButton when `ariaAttributes` are passed using `renderButton` prop. 

### 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 does not support aria-labelledby and aria-describedby attributes. 
- When `ariaAttributes` is passed to ActionPopoverMenuButton using the `renderButton` prop, the aria attributes are not set on the button. 

### 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
- [ ] 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>
-->

- See new test story "With aria attributes"


Patch:
diff --git a/src/components/action-popover/action-popover-menu-button/action-popover-menu-button.component.tsx b/src/components/action-popover/action-popover-menu-button/action-popover-menu-button.component.tsx
index e9d1551b0..32de52052 100644
--- a/src/components/action-popover/action-popover-menu-button/action-popover-menu-button.component.tsx
+++ b/src/components/action-popover/action-popover-menu-button/action-popover-menu-button.component.tsx
@@ -12,6 +12,8 @@ import { IconType } from "../../icon";
 export type ActionPopoverMenuButtonAria = {
   "aria-haspopup": string;
   "aria-label": string;
+  "aria-labelledby"?: string;
+  "aria-describedby"?: string;
   "aria-controls": string;
   "aria-expanded": string;
 };
@@ -41,6 +43,7 @@ export const ActionPopoverMenuButton = ({
   iconPosition,
   size,
   children,
+  ariaAttributes,
   ...props
 }: ActionPopoverMenuButtonProps) => (
   <MenuButtonOverrideWrapper>
@@ -49,6 +52,7 @@ export const ActionPopoverMenuButton = ({
       iconType={iconType}
       iconPosition={iconPosition}
       size={size}
+      {...ariaAttributes}
       {...props}
     >
       {children}
diff --git a/src/components/action-popover/action-popover.component.tsx b/src/components/action-popover/action-popover.component.tsx
index d427fe246..e97b5afdf 100644
--- a/src/components/action-popover/action-popover.component.tsx
+++ b/src/components/action-popover/action-popover.component.tsx
@@ -36,6 +36,8 @@ export interface RenderButtonProps {
   ariaAttributes: {
     "aria-haspopup": string;
     "aria-label": string;
+    "aria-labelledby"?: string;
+    "aria-describedby"?: string;
     "aria-controls": string;
     "aria-expanded": string;
   };
@@ -62,6 +64,10 @@ export interface ActionPopoverProps extends MarginProps {
   rightAlignMenu?: boolean;
   /** Prop to specify an aria-label for the component */
   "aria-label"?: string;
+  /** Prop to specify an aria-labelledby for the component */
+  "aria-labelledby"?: string;
+  /** Prop to specify an aria-describedby for the component */
+  "aria-describedby"?: string;
 }
 
 const onOpenDefault = () => {};
@@ -78,6 +84,8 @@ export const ActionPopover = ({
   horizontalAlignment = "left",
   submenuPosition = "left",
   "aria-label": ariaLabel,
+  "aria-labelledby": ariaLabelledBy,
+  "aria-describedby": ariaDescribedBy,
   ...rest
 }: ActionPopoverProps) => {
   const l = useLocale();
@@ -233,6 +241,8 @@ export const ActionPopover = ({
         ariaAttributes: {
           "aria-haspopup": "true",
           "aria-label": ariaLabel || l.actionPopover.ariaLabel(),
+          "aria-labelledby": ariaLabelledBy,
+          "aria-describedby": ariaDescribedBy,
           "aria-controls": menuID,
           "aria-expanded": `${isOpen}`,
         },
@@ -244,6 +254,8 @@ export const ActionPopover = ({
         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}
diff --git a/src/components/action-popover/action-popover.mdx b/src/components/action-popover/action-popover.mdx
index 58f9f2fa2..b86d812bc 100644
--- a/src/components/action-popover/action-popover.mdx
+++ b/src/components/action-popover/action-popover.mdx
@@ -90,7 +90,16 @@ Menu items can also be displayed without icons.
 ### With custom menu button
 
 It is possible to use the `renderButton` prop to override the default button used to trigger the opening and closing of
-ActionPopoverMenu. Expand the code example below for how to use this.
+ActionPopoverMenu.
+
+The `renderButton` prop is a function that allows consumers to pass `tabIndex`, `data-element` and the provided aria attribute props to 
+`ActionPopoverMenuButton` or a custom button component.
+
+A default `aria-label` will be provided to the button, but this can be overridden by passing the `ariaLabel` prop to the 
+`ActionPopover` or making use of the `actionPopover.ariaLabel` translation key. Please ensure to set this to `undefined` if your 
+button contains visible text, as this will prevent screen readers from reading the visible label. 
+
+See the example below for an example of how to use the `renderButton` prop:
 
 <Canvas of={ActionPopoverStories.CustomMenuButton} />
 
diff --git a/src/components/action-popover/action-popover.stories.tsx b/src/components/action-popover/action-popover.stories.tsx
index 34ed29197..0c6ad9798 100644
--- a/src/components/action-popover/action-popover.stories.tsx
+++ b/src/components/action-popover/action-popover.stories.tsx
@@ -219,6 +219,7 @@ export const CustomMenuButton: Story = () => {
             tabIndex={tabIndex}
             data-element={dataElement}
             ariaAttributes={ariaAttributes}
+            aria-label={undefined}
           >
             More
           </ActionPopoverMenuButton>
@@ -232,6 +233,31 @@ export const CustomMenuButton: Story = () => {
           Delete
         </ActionPopoverItem>
       </ActionPopover>
+      <ActionPopover
+        renderButton={({
+          tabIndex,
+          "data-element": dataElement,
+          ariaAttributes,
+        }) => (
+          <ActionPopoverMenuButton
+            buttonType="tertiary"
+            iconType="dropdown"
+            iconPosition="after"
+            size="small"
+            tabIndex={tabIndex}
+            data-element={dataElement}
+            ariaAttributes={ariaAttributes}
+          />
+        )}
+      >
+        <ActionPopoverItem icon="email" onClick={() => {}}>
+          Email Invoice
+        </ActionPopoverItem>
+        <ActionPopoverDivider />
+        <ActionPopoverItem onClick={() => {}} icon="delete">
+          Delete
+        </ActionPopoverItem>
+      </ActionPopover>
       <ActionPopover
         renderButton={({ "data-element": dataElement }) => (
           <Link onClick={() => {}} data-element={dataElement}>


Test patch:
diff --git a/src/components/action-popover/action-popover-test.stories.tsx b/src/components/action-popover/action-popover-test.stories.tsx
index 40c6653e1..9cb9f3137 100644
--- a/src/components/action-popover/action-popover-test.stories.tsx
+++ b/src/components/action-popover/action-popover-test.stories.tsx
@@ -8,6 +8,7 @@ import {
   ActionPopoverItem,
   ActionPopoverMenu,
   ActionPopoverProps,
+  ActionPopoverMenuButton,
 } from ".";
 import {
   FlatTable,
@@ -21,7 +22,7 @@ import Box from "../box";
 
 export default {
   title: "Action Popover/Test",
-  includeStories: ["Default", "LongMenuExample"],
+  includeStories: ["Default", "LongMenuExample", "WithAriaAttributes"],
   parameters: {
     info: { disable: true },
     chromatic: {
@@ -800,3 +801,52 @@ export const LongMenuExample = () => {
     </Box>
   );
 };
+
+export const WithAriaAttributes = () => {
+  return (
+    <>
+      <span id="test-label-id">
+        Instead of "actions", this should be the label
+      </span>
+      <br />
+      <span id="test-description-id">This should be the description</span>
+
+      <ActionPopover
+        aria-labelledby="test-label-id"
+        aria-describedby="test-description-id"
+      >
+        <ActionPopoverItem>example item</ActionPopoverItem>
+      </ActionPopover>
+
+      <span id="foo-label-id">Instead of "Foo", this should be the label </span>
+      <br />
+      <span id="foo-description-id">
+        This should be the description for Foo
+      </span>
+
+      <ActionPopover
+        aria-labelledby="foo-label-id"
+        aria-describedby="foo-description-id"
+        renderButton={({
+          tabIndex,
+          "data-element": dataElement,
+          ariaAttributes,
+        }) => (
+          <ActionPopoverMenuButton
+            buttonType="tertiary"
+            iconType="dropdown"
+            iconPosition="after"
+            size="small"
+            tabIndex={tabIndex}
+            data-element={dataElement}
+            ariaAttributes={ariaAttributes}
+          >
+            Foo
+          </ActionPopoverMenuButton>
+        )}
+      >
+        <ActionPopoverItem onClick={() => {}}>foo</ActionPopoverItem>
+      </ActionPopover>
+    </>
+  );
+};
diff --git a/src/components/action-popover/action-popover.test.tsx b/src/components/action-popover/action-popover.test.tsx
index 131b86f04..49dc114f8 100644
--- a/src/components/action-popover/action-popover.test.tsx
+++ b/src/components/action-popover/action-popover.test.tsx
@@ -140,6 +140,32 @@ test("uses the aria-label prop if provided", () => {
   expect(screen.getByRole("button")).toHaveAccessibleName("test aria label");
 });
 
+test("renders with the provided aria-labelledby prop", () => {
+  render(
+    <>
+      <span id="test-label-id">test label</span>
+      <ActionPopover aria-labelledby="test-label-id">
+        <ActionPopoverItem>example item</ActionPopoverItem>
+      </ActionPopover>
+    </>,
+  );
+  expect(screen.getByRole("button")).toHaveAccessibleName("test label");
+});
+
+test("renders with the provided aria-describedby prop", () => {
+  render(
+    <>
+      <span id="test-description-id">test description</span>
+      <ActionPopover aria-describedby="test-description-id">
+        <ActionPopoverItem>example item</ActionPopoverItem>
+      </ActionPopover>
+    </>,
+  );
+  expect(screen.getByRole("button")).toHaveAccessibleDescription(
+    "test description",
+  );
+});
+
 test("renders with the menu closed by default", () => {
   render(
     <ActionPopover>
@@ -1825,7 +1851,7 @@ test("an error is thrown, with appropriate error message, if an invalid element
   globalConsoleSpy.mockRestore();
 });
 
-test("an error is thrown, with appropriate error message, if a submenu has incorrecr children", async () => {
+test("an error is thrown, with appropriate error message, if a submenu has incorrect children", async () => {
   const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
 
   const globalConsoleSpy = jest
@@ -1911,6 +1937,62 @@ describe("when the renderButton prop is passed", () => {
 
     expect(menuButton).toHaveAttribute("tabindex", "-1");
   });
+
+  it("renders the menu button with the provided aria-label", () => {
+    render(
+      <ActionPopover
+        renderButton={(props) => (
+          <ActionPopoverMenuButton
+            buttonType="tertiary"
+            iconType="dropdown"
+            iconPosition="after"
+            size="small"
+            aria-label="test label"
+            {...props}
+          >
+            Foo
+          </ActionPopoverMenuButton>
+        )}
+      >
+        <ActionPopoverItem onClick={jest.fn()}>foo</ActionPopoverItem>
+      </ActionPopover>,
+    );
+
+    const menuButton = screen.getByRole("button");
+    expect(menuButton).toBeVisible();
+    expect(menuButton).toHaveAccessibleName("test label");
+  });
+
+  it("renders the menu button with the provided aria-labelledby and aria-describedby", () => {
+    render(
+      <>
+        <span id="test-label-id">test label</span>
+        <span id="test-description-id">test description</span>
+        <ActionPopover
+          aria-labelledby="test-label-id"
+          aria-describedby="test-description-id"
+          renderButton={(props) => (
+            <ActionPopoverMenuButton
+              buttonType="tertiary"
+              iconType="dropdown"
+              iconPosition="after"
+              size="small"
+              {...props}
+            >
+              Foo
+            </ActionPopoverMenuButton>
+          )}
+        >
+          <ActionPopoverItem onClick={() => {}}>foo</ActionPopoverItem>
+        </ActionPopover>
+      </>,
+    );
+
+    const menuButton = screen.getByRole("button");
+    expect(menuButton).toBeVisible();
+    expect(menuButton).toHaveAccessibleName("test label");
+    expect(menuButton).toHaveAccessibleDescription("test description");
+  });
 });
 
 describe("When ActionPopoverMenu contains multiple disabled items", () => {