Skip to content

Commit

Permalink
Merge cda649a into cb54f42
Browse files Browse the repository at this point in the history
  • Loading branch information
JeffreyKozik authored Mar 12, 2024
2 parents cb54f42 + cda649a commit 2cee533
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 5 deletions.
5 changes: 5 additions & 0 deletions .changeset/sour-games-tap.md
Original file line number Diff line number Diff line change
@@ -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.
17 changes: 17 additions & 0 deletions packages/react/src/ActionMenu/ActionMenu.examples.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -483,3 +483,20 @@ export const OnlyInactiveItems = () => (
</ActionMenu.Overlay>
</ActionMenu>
)

export const WithinForm = () => (
<FormControl>
<FormControl.Label>Action Menu within FormControl</FormControl.Label>
<ActionMenu>
<ActionMenu.Button>Open menu</ActionMenu.Button>
<ActionMenu.Overlay width="medium">
<ActionList>
<ActionList.Item onSelect={() => alert('Copy link clicked')}>
Copy link
<ActionList.TrailingVisual>⌘C</ActionList.TrailingVisual>
</ActionList.Item>
</ActionList>
</ActionMenu.Overlay>
</ActionMenu>
</FormControl>
)
11 changes: 7 additions & 4 deletions packages/react/src/ActionMenu/ActionMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -43,10 +44,12 @@ const Menu: React.FC<React.PropsWithChildren<ActionMenuProps>> = ({
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<ActionMenuButtonProps>(child) && (child.type === MenuButton || child.type === Anchor),
Expand All @@ -69,7 +72,7 @@ const Menu: React.FC<React.PropsWithChildren<ActionMenuProps>> = ({
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
Expand All @@ -84,15 +87,15 @@ const Menu: React.FC<React.PropsWithChildren<ActionMenuProps>> = ({
// 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
Expand Down
36 changes: 35 additions & 1 deletion packages/react/src/__tests__/ActionMenu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -77,6 +77,24 @@ function ExampleWithTooltipV2(actionMenuTrigger: React.ReactElement): JSX.Elemen
)
}

function ExampleWithForm(): JSX.Element {
return (
<FormControl>
<FormControl.Label>Action Menu Label</FormControl.Label>
<Example />
</FormControl>
)
}

function ExampleWithTooltipWithForm(): JSX.Element {
return (
<FormControl>
<FormControl.Label>Action Menu Label</FormControl.Label>
<ExampleWithTooltip />
</FormControl>
)
}

describe('ActionMenu', () => {
behavesAsComponent({
Component: ActionList,
Expand Down Expand Up @@ -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(<ExampleWithForm />)
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(<ExampleWithTooltipWithForm />)
const button = component.getByLabelText('Action Menu Label')
expect(button).toBeVisible()
const buttonByRole = component.getByRole('button')
expect(button.id).toBe(buttonByRole.id)
})
})

0 comments on commit 2cee533

Please sign in to comment.