Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 677
Adds FormControl's "Auto-Wiring" to ActionMenu Component#4372
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
6ffb7f9123c69bbb023a06dba24ccda649a14f60feFile filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| 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. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<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), | ||
| @@ -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}) | ||
ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Followed the pattern suggested in this slack thread: https://github.slack.com/archives/CSGAVNZ19/p1709843761424779?thread_ts=1709836721.563139&cid=CSGAVNZ19 | ||
| } | ||
| } | ||
| return null | ||
| @@ -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 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -22,20 +22,6 @@ exports[`Token components AvatarToken renders all sizes 1`] = ` | ||
| border-width: 0; | ||
| } | ||
| .c3 { | ||
ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I updated snapshots as detailed in the contributor docs: https://github.com/primer/react/blob/cb54f4298700d369e616aaefb2e7df4830231736/contributor-docs/testing.md#snapshots Not sure why these classes had to be rearranged as it's not clear to me how | ||
| display: inline-block; | ||
| overflow: hidden; | ||
| line-height: 1; | ||
| vertical-align: middle; | ||
| border-radius: 50%; | ||
| box-shadow: 0 0 0 1px var(--avatar-borderColor,var(--color-avatar-border,rgba(31,35,40,0.15))); | ||
| height: var(--avatar-size); | ||
| width: var(--avatar-size); | ||
| --avatar-size: 16px; | ||
| width: 100%; | ||
| height: 100%; | ||
| } | ||
| .c0 { | ||
| -webkit-align-items: center; | ||
| -webkit-box-align: center; | ||
| @@ -155,6 +141,20 @@ exports[`Token components AvatarToken renders all sizes 1`] = ` | ||
| bottom: 0; | ||
| } | ||
| .c3 { | ||
| display: inline-block; | ||
| overflow: hidden; | ||
| line-height: 1; | ||
| vertical-align: middle; | ||
| border-radius: 50%; | ||
| box-shadow: 0 0 0 1px var(--avatar-borderColor,var(--color-avatar-border,rgba(31,35,40,0.15))); | ||
| height: var(--avatar-size); | ||
| width: var(--avatar-size); | ||
| --avatar-size: 16px; | ||
| width: 100%; | ||
| height: 100%; | ||
| } | ||
| .c2 { | ||
| --spacing: calc(4px * 2); | ||
| display: block; | ||
| @@ -248,20 +248,6 @@ exports[`Token components AvatarToken renders all sizes 2`] = ` | ||
| border-width: 0; | ||
| } | ||
| .c3 { | ||
| display: inline-block; | ||
| overflow: hidden; | ||
| line-height: 1; | ||
| vertical-align: middle; | ||
| border-radius: 50%; | ||
| box-shadow: 0 0 0 1px var(--avatar-borderColor,var(--color-avatar-border,rgba(31,35,40,0.15))); | ||
| height: var(--avatar-size); | ||
| width: var(--avatar-size); | ||
| --avatar-size: 20px; | ||
| width: 100%; | ||
| height: 100%; | ||
| } | ||
| .c0 { | ||
| -webkit-align-items: center; | ||
| -webkit-box-align: center; | ||
| @@ -381,6 +367,20 @@ exports[`Token components AvatarToken renders all sizes 2`] = ` | ||
| bottom: 0; | ||
| } | ||
| .c3 { | ||
| display: inline-block; | ||
| overflow: hidden; | ||
| line-height: 1; | ||
| vertical-align: middle; | ||
| border-radius: 50%; | ||
| box-shadow: 0 0 0 1px var(--avatar-borderColor,var(--color-avatar-border,rgba(31,35,40,0.15))); | ||
| height: var(--avatar-size); | ||
| width: var(--avatar-size); | ||
| --avatar-size: 20px; | ||
| width: 100%; | ||
| height: 100%; | ||
| } | ||
| .c2 { | ||
| --spacing: calc(4px * 2); | ||
| display: block; | ||
| @@ -474,20 +474,6 @@ exports[`Token components AvatarToken renders all sizes 3`] = ` | ||
| border-width: 0; | ||
| } | ||
| .c3 { | ||
| display: inline-block; | ||
| overflow: hidden; | ||
| line-height: 1; | ||
| vertical-align: middle; | ||
| border-radius: 50%; | ||
| box-shadow: 0 0 0 1px var(--avatar-borderColor,var(--color-avatar-border,rgba(31,35,40,0.15))); | ||
| height: var(--avatar-size); | ||
| width: var(--avatar-size); | ||
| --avatar-size: 24px; | ||
| width: 100%; | ||
| height: 100%; | ||
| } | ||
| .c0 { | ||
| -webkit-align-items: center; | ||
| -webkit-box-align: center; | ||
| @@ -607,6 +593,20 @@ exports[`Token components AvatarToken renders all sizes 3`] = ` | ||
| bottom: 0; | ||
| } | ||
| .c3 { | ||
| display: inline-block; | ||
| overflow: hidden; | ||
| line-height: 1; | ||
| vertical-align: middle; | ||
| border-radius: 50%; | ||
| box-shadow: 0 0 0 1px var(--avatar-borderColor,var(--color-avatar-border,rgba(31,35,40,0.15))); | ||
| height: var(--avatar-size); | ||
| width: var(--avatar-size); | ||
| --avatar-size: 24px; | ||
| width: 100%; | ||
| height: 100%; | ||
| } | ||
| .c2 { | ||
| --spacing: calc(4px * 2); | ||
| display: block; | ||
| @@ -700,20 +700,6 @@ exports[`Token components AvatarToken renders all sizes 4`] = ` | ||
| border-width: 0; | ||
| } | ||
| .c3 { | ||
| display: inline-block; | ||
| overflow: hidden; | ||
| line-height: 1; | ||
| vertical-align: middle; | ||
| border-radius: 50%; | ||
| box-shadow: 0 0 0 1px var(--avatar-borderColor,var(--color-avatar-border,rgba(31,35,40,0.15))); | ||
| height: var(--avatar-size); | ||
| width: var(--avatar-size); | ||
| --avatar-size: 32px; | ||
| width: 100%; | ||
| height: 100%; | ||
| } | ||
| .c0 { | ||
| -webkit-align-items: center; | ||
| -webkit-box-align: center; | ||
| @@ -835,6 +821,20 @@ exports[`Token components AvatarToken renders all sizes 4`] = ` | ||
| bottom: 0; | ||
| } | ||
| .c3 { | ||
| display: inline-block; | ||
| overflow: hidden; | ||
| line-height: 1; | ||
| vertical-align: middle; | ||
| border-radius: 50%; | ||
| box-shadow: 0 0 0 1px var(--avatar-borderColor,var(--color-avatar-border,rgba(31,35,40,0.15))); | ||
| height: var(--avatar-size); | ||
| width: var(--avatar-size); | ||
| --avatar-size: 32px; | ||
| width: 100%; | ||
| height: 100%; | ||
| } | ||
| .c2 { | ||
| --spacing: calc(4px * 2); | ||
| display: block; | ||
| @@ -915,20 +915,6 @@ exports[`Token components AvatarToken renders isSelected 1`] = ` | ||
| margin-right: 4px; | ||
| } | ||
| .c3 { | ||
| display: inline-block; | ||
| overflow: hidden; | ||
| line-height: 1; | ||
| vertical-align: middle; | ||
| border-radius: 50%; | ||
| box-shadow: 0 0 0 1px var(--avatar-borderColor,var(--color-avatar-border,rgba(31,35,40,0.15))); | ||
| height: var(--avatar-size); | ||
| width: var(--avatar-size); | ||
| --avatar-size: 20px; | ||
| width: 100%; | ||
| height: 100%; | ||
| } | ||
| .c0 { | ||
| -webkit-align-items: center; | ||
| -webkit-box-align: center; | ||
| @@ -998,6 +984,20 @@ exports[`Token components AvatarToken renders isSelected 1`] = ` | ||
| bottom: 0; | ||
| } | ||
| .c3 { | ||
| display: inline-block; | ||
| overflow: hidden; | ||
| line-height: 1; | ||
| vertical-align: middle; | ||
| border-radius: 50%; | ||
| box-shadow: 0 0 0 1px var(--avatar-borderColor,var(--color-avatar-border,rgba(31,35,40,0.15))); | ||
| height: var(--avatar-size); | ||
| width: var(--avatar-size); | ||
| --avatar-size: 20px; | ||
| width: 100%; | ||
| height: 100%; | ||
| } | ||
| .c2 { | ||
| --spacing: calc(4px * 2); | ||
| display: block; | ||
| @@ -1056,20 +1056,6 @@ exports[`Token components AvatarToken renders with a remove button 1`] = ` | ||
| border-width: 0; | ||
| } | ||
| .c3 { | ||
| display: inline-block; | ||
| overflow: hidden; | ||
| line-height: 1; | ||
| vertical-align: middle; | ||
| border-radius: 50%; | ||
| box-shadow: 0 0 0 1px var(--avatar-borderColor,var(--color-avatar-border,rgba(31,35,40,0.15))); | ||
| height: var(--avatar-size); | ||
| width: var(--avatar-size); | ||
| --avatar-size: 20px; | ||
| width: 100%; | ||
| height: 100%; | ||
| } | ||
| .c0 { | ||
| -webkit-align-items: center; | ||
| -webkit-box-align: center; | ||
| @@ -1189,6 +1175,20 @@ exports[`Token components AvatarToken renders with a remove button 1`] = ` | ||
| bottom: 0; | ||
| } | ||
| .c3 { | ||
| display: inline-block; | ||
| overflow: hidden; | ||
| line-height: 1; | ||
| vertical-align: middle; | ||
| border-radius: 50%; | ||
| box-shadow: 0 0 0 1px var(--avatar-borderColor,var(--color-avatar-border,rgba(31,35,40,0.15))); | ||
| height: var(--avatar-size); | ||
| width: var(--avatar-size); | ||
| --avatar-size: 20px; | ||
| width: 100%; | ||
| height: 100%; | ||
| } | ||
| .c2 { | ||
| --spacing: calc(4px * 2); | ||
| display: block; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 ( | ||
| <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, | ||
| @@ -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) | ||
| }) | ||
| }) | ||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using the hook recently created by this PR: #3632. This hook is also being used in the
InlineAutocompletedraft component:react/packages/react/src/drafts/InlineAutocomplete/InlineAutocomplete.tsx
Line 118 in cb54f42