diff --git a/.changeset/sour-games-tap.md b/.changeset/sour-games-tap.md
new file mode 100644
index 00000000000..3fe24af9519
--- /dev/null
+++ b/.changeset/sour-games-tap.md
@@ -0,0 +1,5 @@
+---
+'@primer/react': minor
+---
+
+ActionMenu: Automatically wires the ActionMenu component to the accessibility and validation provided by the FormControl component it's nested within. If ActionMenu isn't nested within FormControl, nothing changes.
diff --git a/packages/react/src/ActionMenu/ActionMenu.examples.stories.tsx b/packages/react/src/ActionMenu/ActionMenu.examples.stories.tsx
index d636102cd86..6db449fc2be 100644
--- a/packages/react/src/ActionMenu/ActionMenu.examples.stories.tsx
+++ b/packages/react/src/ActionMenu/ActionMenu.examples.stories.tsx
@@ -483,3 +483,20 @@ export const OnlyInactiveItems = () => (
)
+
+export const WithinForm = () => (
+
+ Action Menu within FormControl
+
+ Open menu
+
+
+ alert('Copy link clicked')}>
+ Copy link
+ ⌘C
+
+
+
+
+
+)
diff --git a/packages/react/src/ActionMenu/ActionMenu.tsx b/packages/react/src/ActionMenu/ActionMenu.tsx
index 24b53b7025c..3f6349fffad 100644
--- a/packages/react/src/ActionMenu/ActionMenu.tsx
+++ b/packages/react/src/ActionMenu/ActionMenu.tsx
@@ -12,6 +12,7 @@ import {useId} from '../hooks/useId'
import type {MandateProps} from '../utils/types'
import type {ForwardRefComponent as PolymorphicForwardRefComponent} from '../utils/polymorphic'
import {Tooltip} from '../TooltipV2/Tooltip'
+import {useFormControlForwardedProps} from '../FormControl'
export type MenuContextProps = Pick<
AnchoredOverlayProps,
@@ -43,10 +44,12 @@ const Menu: React.FC> = ({
open,
onOpenChange,
children,
+ ...externalInputProps
}: ActionMenuProps) => {
const [combinedOpenState, setCombinedOpenState] = useProvidedStateOrCreate(open, onOpenChange, false)
const onOpen = React.useCallback(() => setCombinedOpenState(true), [setCombinedOpenState])
const onClose = React.useCallback(() => setCombinedOpenState(false), [setCombinedOpenState])
+ const inputProps = useFormControlForwardedProps(externalInputProps)
const menuButtonChild = React.Children.toArray(children).find(
child => React.isValidElement(child) && (child.type === MenuButton || child.type === Anchor),
@@ -69,7 +72,7 @@ const Menu: React.FC> = ({
renderAnchor = anchorProps => {
// We need to attach the anchor props to the tooltip trigger (ActionMenu.Button's grandchild) not the tooltip itself.
const triggerButton = React.cloneElement(anchorChildren, {...anchorProps})
- return React.cloneElement(child, {children: triggerButton, ref: anchorRef})
+ return React.cloneElement(child, {children: triggerButton, ref: anchorRef, ...inputProps})
}
}
return null
@@ -84,15 +87,15 @@ const Menu: React.FC> = ({
// We need to attach the anchor props to the tooltip trigger not the tooltip itself.
const tooltipTriggerEl = React.cloneElement(tooltipTrigger, {...anchorProps})
const tooltip = React.cloneElement(anchorChildren, {children: tooltipTriggerEl})
- return React.cloneElement(child, {children: tooltip, ref: anchorRef})
+ return React.cloneElement(child, {children: tooltip, ref: anchorRef, ...inputProps})
}
}
} else {
- renderAnchor = anchorProps => React.cloneElement(child, anchorProps)
+ renderAnchor = anchorProps => React.cloneElement(child, {...anchorProps, ...inputProps})
}
return null
} else if (child.type === MenuButton) {
- renderAnchor = anchorProps => React.cloneElement(child, anchorProps)
+ renderAnchor = anchorProps => React.cloneElement(child, {...anchorProps, ...inputProps})
return null
} else {
return child
diff --git a/packages/react/src/__tests__/ActionMenu.test.tsx b/packages/react/src/__tests__/ActionMenu.test.tsx
index e2c871c931f..64739921e85 100644
--- a/packages/react/src/__tests__/ActionMenu.test.tsx
+++ b/packages/react/src/__tests__/ActionMenu.test.tsx
@@ -3,7 +3,7 @@ import userEvent from '@testing-library/user-event'
import {axe} from 'jest-axe'
import React from 'react'
import theme from '../theme'
-import {ActionMenu, ActionList, BaseStyles, ThemeProvider, SSRProvider, Tooltip, Button} from '..'
+import {ActionMenu, ActionList, BaseStyles, ThemeProvider, SSRProvider, Tooltip, Button, FormControl} from '..'
import {Tooltip as TooltipV2} from '../TooltipV2/Tooltip'
import {behavesAsComponent, checkExports} from '../utils/testing'
import {SingleSelect} from '../ActionMenu/ActionMenu.features.stories'
@@ -77,6 +77,24 @@ function ExampleWithTooltipV2(actionMenuTrigger: React.ReactElement): JSX.Elemen
)
}
+function ExampleWithForm(): JSX.Element {
+ return (
+
+ Action Menu Label
+
+
+ )
+}
+
+function ExampleWithTooltipWithForm(): JSX.Element {
+ return (
+
+ Action Menu Label
+
+
+ )
+}
+
describe('ActionMenu', () => {
behavesAsComponent({
Component: ActionList,
@@ -398,4 +416,20 @@ describe('ActionMenu', () => {
expect(button.id).toBe(buttonId)
})
+
+ it('MenuButton within FormControl should be labelled by FormControl.Label', async () => {
+ const component = HTMLRender()
+ const button = component.getByLabelText('Action Menu Label')
+ expect(button).toBeVisible()
+ const buttonByRole = component.getByRole('button')
+ expect(button.id).toBe(buttonByRole.id)
+ })
+
+ it('MenuButton wrapped by Tooltip within FormControl should be labelled by FormControl.Label', async () => {
+ const component = HTMLRender()
+ const button = component.getByLabelText('Action Menu Label')
+ expect(button).toBeVisible()
+ const buttonByRole = component.getByRole('button')
+ expect(button.id).toBe(buttonByRole.id)
+ })
})