From b7fd4272d8ebc2cdb1f9ce9ea979d902b38816a2 Mon Sep 17 00:00:00 2001 From: Ian Sanders Date: Wed, 13 Mar 2024 14:14:35 +0000 Subject: [PATCH 01/20] Close parent stack when an item is picked --- packages/react/src/ActionMenu/ActionMenu.tsx | 24 ++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/packages/react/src/ActionMenu/ActionMenu.tsx b/packages/react/src/ActionMenu/ActionMenu.tsx index 24b53b7025c..90628e30c2f 100644 --- a/packages/react/src/ActionMenu/ActionMenu.tsx +++ b/packages/react/src/ActionMenu/ActionMenu.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, {useContext} from 'react' import {TriangleDownIcon} from '@primer/octicons-react' import type {AnchoredOverlayProps} from '../AnchoredOverlay' import {AnchoredOverlay} from '../AnchoredOverlay' @@ -13,11 +13,13 @@ import type {MandateProps} from '../utils/types' import type {ForwardRefComponent as PolymorphicForwardRefComponent} from '../utils/polymorphic' import {Tooltip} from '../TooltipV2/Tooltip' +type MenuCloseHandler = (gesture: 'anchor-click' | 'click-outside' | 'escape' | 'tab' | 'item-select') => void + export type MenuContextProps = Pick< AnchoredOverlayProps, 'anchorRef' | 'renderAnchor' | 'open' | 'onOpen' | 'anchorId' > & { - onClose?: (gesture: 'anchor-click' | 'click-outside' | 'escape' | 'tab') => void + onClose?: MenuCloseHandler } const MenuContext = React.createContext({renderAnchor: null, open: false}) @@ -44,9 +46,23 @@ const Menu: React.FC> = ({ onOpenChange, children, }: ActionMenuProps) => { + const parentMenuContext = useContext(MenuContext) + const [combinedOpenState, setCombinedOpenState] = useProvidedStateOrCreate(open, onOpenChange, false) const onOpen = React.useCallback(() => setCombinedOpenState(true), [setCombinedOpenState]) - const onClose = React.useCallback(() => setCombinedOpenState(false), [setCombinedOpenState]) + const onClose: MenuCloseHandler = React.useCallback( + gesture => { + setCombinedOpenState(false) + + // Only close the parent stack when an item is selected or the user tabs out of the menu entirely + switch (gesture) { + case 'tab': + case 'item-select': + parentMenuContext.onClose?.(gesture) + } + }, + [setCombinedOpenState, parentMenuContext], + ) const menuButtonChild = React.Children.toArray(children).find( child => React.isValidElement(child) && (child.type === MenuButton || child.type === Anchor), @@ -167,7 +183,7 @@ const Overlay: React.FC> = ({ listRole: 'menu', listLabelledBy: ariaLabelledby || anchorId, selectionAttribute: 'aria-checked', // Should this be here? - afterSelect: onClose, + afterSelect: () => onClose?.('item-select'), }} > {children} From 8f9ccf92914397f390bd683473be6d588007d250 Mon Sep 17 00:00:00 2001 From: Ian Sanders Date: Wed, 13 Mar 2024 14:37:05 +0000 Subject: [PATCH 02/20] Add `MenuItemAnchor` component --- .../react/src/ActionMenu/ActionMenu.docs.json | 18 ++++++++- packages/react/src/ActionMenu/ActionMenu.tsx | 37 +++++++++++++++++-- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/packages/react/src/ActionMenu/ActionMenu.docs.json b/packages/react/src/ActionMenu/ActionMenu.docs.json index 398386f157b..e802013d6bc 100644 --- a/packages/react/src/ActionMenu/ActionMenu.docs.json +++ b/packages/react/src/ActionMenu/ActionMenu.docs.json @@ -10,7 +10,7 @@ "type": "React.ReactElement[]", "defaultValue": "", "required": true, - "description": "Recommended: `ActionMenu.Button` or `ActionMenu.Anchor` with `ActionMenu.Overlay`" + "description": "Recommended: `ActionMenu.Button`, `ActionMenu.MenuItemAnchor`, or `ActionMenu.Anchor`, with `ActionMenu.Overlay`" }, { "name": "open", @@ -48,6 +48,22 @@ "url": "/react/Button" } }, + { + "name": "ActionMenu.MenuItemAnchor", + "props": [ + { + "name": "children", + "type": "React.ReactElement", + "defaultValue": "", + "required": true, + "description": "" + } + ], + "passthrough": { + "element": "ActionList.Item", + "url": "/react/ActionList" + } + }, { "name": "ActionMenu.Anchor", "props": [ diff --git a/packages/react/src/ActionMenu/ActionMenu.tsx b/packages/react/src/ActionMenu/ActionMenu.tsx index 90628e30c2f..58212b6f129 100644 --- a/packages/react/src/ActionMenu/ActionMenu.tsx +++ b/packages/react/src/ActionMenu/ActionMenu.tsx @@ -1,9 +1,14 @@ import React, {useContext} from 'react' -import {TriangleDownIcon} from '@primer/octicons-react' +import {TriangleDownIcon, ChevronRightIcon} from '@primer/octicons-react' import type {AnchoredOverlayProps} from '../AnchoredOverlay' import {AnchoredOverlay} from '../AnchoredOverlay' import type {OverlayProps} from '../Overlay' -import {useProvidedRefOrCreate, useProvidedStateOrCreate, useMenuKeyboardNavigation} from '../hooks' +import { + useProvidedRefOrCreate, + useProvidedStateOrCreate, + useMenuKeyboardNavigation, + useRefObjectAsForwardedRef, +} from '../hooks' import {Divider} from '../ActionList/Divider' import {ActionListContainerContext} from '../ActionList/ActionListContainerContext' import type {ButtonProps} from '../Button' @@ -12,6 +17,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 {ActionList, type ActionListItemProps} from '../ActionList' type MenuCloseHandler = (gesture: 'anchor-click' | 'click-outside' | 'escape' | 'tab' | 'item-select') => void @@ -139,6 +145,31 @@ const MenuButton = React.forwardRef(({...props}, anchorRef) => { ) }) as PolymorphicForwardRefComponent<'button', ActionMenuButtonProps> +export type MenuItemAnchorProps = ActionListItemProps +const MenuItemAnchor = React.forwardRef(({children, onKeyDown: externalOnKeyDown, ...props}, forwardedRef) => { + const anchorRef = React.useRef(null) + useRefObjectAsForwardedRef(forwardedRef, anchorRef) + + /** Treat right arrow key press as click. */ + const onKeyDown: React.KeyboardEventHandler = event => { + externalOnKeyDown?.(event) + if (event.key === 'ArrowRight' && !event.defaultPrevented) anchorRef.current?.click() + } + + return ( + + + {/* Slots will grab the first TrailingVisual encountered. so by putting children first we allow the consumer + to override the chevron icon. */} + {children} + + + + + + ) +}) as PolymorphicForwardRefComponent<'li', MenuItemAnchorProps> + type MenuOverlayProps = Partial & Pick & { /** @@ -194,4 +225,4 @@ const Overlay: React.FC> = ({ } Menu.displayName = 'ActionMenu' -export const ActionMenu = Object.assign(Menu, {Button: MenuButton, Anchor, Overlay, Divider}) +export const ActionMenu = Object.assign(Menu, {Button: MenuButton, MenuItemAnchor, Anchor, Overlay, Divider}) From 32f58b699134bcd43ef50f70d81a0f2aa9be1184 Mon Sep 17 00:00:00 2001 From: Ian Sanders Date: Wed, 13 Mar 2024 14:54:58 +0000 Subject: [PATCH 03/20] Close submenus when left arrow is pressed --- packages/react/src/ActionMenu/ActionMenu.tsx | 35 ++++++++++++++----- .../src/hooks/useMenuKeyboardNavigation.ts | 31 ++++++++++++++-- 2 files changed, 55 insertions(+), 11 deletions(-) diff --git a/packages/react/src/ActionMenu/ActionMenu.tsx b/packages/react/src/ActionMenu/ActionMenu.tsx index 58212b6f129..23f776892b0 100644 --- a/packages/react/src/ActionMenu/ActionMenu.tsx +++ b/packages/react/src/ActionMenu/ActionMenu.tsx @@ -19,13 +19,16 @@ import type {ForwardRefComponent as PolymorphicForwardRefComponent} from '../uti import {Tooltip} from '../TooltipV2/Tooltip' import {ActionList, type ActionListItemProps} from '../ActionList' -type MenuCloseHandler = (gesture: 'anchor-click' | 'click-outside' | 'escape' | 'tab' | 'item-select') => void +export type MenuCloseHandler = ( + gesture: 'anchor-click' | 'click-outside' | 'escape' | 'tab' | 'item-select' | 'arrow-left', +) => void export type MenuContextProps = Pick< AnchoredOverlayProps, 'anchorRef' | 'renderAnchor' | 'open' | 'onOpen' | 'anchorId' > & { onClose?: MenuCloseHandler + isSubmenu?: boolean } const MenuContext = React.createContext({renderAnchor: null, open: false}) @@ -60,7 +63,7 @@ const Menu: React.FC> = ({ gesture => { setCombinedOpenState(false) - // Only close the parent stack when an item is selected or the user tabs out of the menu entirely + // Close the parent stack when an item is selected or the user tabs out of the menu entirely switch (gesture) { case 'tab': case 'item-select': @@ -122,7 +125,18 @@ const Menu: React.FC> = ({ }) return ( - + {contents} ) @@ -186,13 +200,18 @@ const Overlay: React.FC> = ({ }) => { // we typecast anchorRef as required instead of optional // because we know that we're setting it in context in Menu - const {anchorRef, renderAnchor, anchorId, open, onOpen, onClose} = React.useContext(MenuContext) as MandateProps< - MenuContextProps, - 'anchorRef' - > + const { + anchorRef, + renderAnchor, + anchorId, + open, + onOpen, + onClose, + isSubmenu = false, + } = React.useContext(MenuContext) as MandateProps const containerRef = React.useRef(null) - useMenuKeyboardNavigation(open, onClose, containerRef, anchorRef) + useMenuKeyboardNavigation(open, onClose, containerRef, anchorRef, isSubmenu) return ( , anchorRef: React.RefObject, + isSubmenu: boolean, ) => { useMenuInitialFocus(open, containerRef, anchorRef) useMnemonics(open, containerRef) useCloseMenuOnTab(open, onClose, containerRef, anchorRef) useMoveFocusToMenuItem(open, containerRef, anchorRef) + useCloseSubmenuOnArrow(open, isSubmenu, onClose, containerRef) } /** @@ -29,7 +31,7 @@ export const useMenuKeyboardNavigation = ( */ const useCloseMenuOnTab = ( open: boolean, - onClose: MenuContextProps['onClose'], + onClose: MenuCloseHandler | undefined, containerRef: React.RefObject, anchorRef: React.RefObject, ) => { @@ -50,6 +52,29 @@ const useCloseMenuOnTab = ( }, [open, onClose, containerRef, anchorRef]) } +/** + * Close submenu when left arrow key is pressed. + */ +const useCloseSubmenuOnArrow = ( + open: boolean, + isSubmenu: boolean, + onClose: MenuCloseHandler | undefined, + containerRef: React.RefObject, +) => { + React.useEffect(() => { + const container = containerRef.current + + const handler = (event: KeyboardEvent) => { + if (open && isSubmenu && event.key === 'ArrowLeft') onClose?.('arrow-left') + } + + container?.addEventListener('keydown', handler) + return () => { + container?.removeEventListener('keydown', handler) + } + }, [open, onClose, containerRef, isSubmenu]) +} + /** * When Arrow Keys are pressed and the focus is on the anchor, * focus should move to a menu item From 36cd9c7a9ec03e6901b643d1751fe56ab4643227 Mon Sep 17 00:00:00 2001 From: Ian Sanders Date: Wed, 13 Mar 2024 14:56:27 +0000 Subject: [PATCH 04/20] Align submenus to the right of anchors --- packages/react/src/ActionMenu/ActionMenu.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react/src/ActionMenu/ActionMenu.tsx b/packages/react/src/ActionMenu/ActionMenu.tsx index 23f776892b0..9f161234303 100644 --- a/packages/react/src/ActionMenu/ActionMenu.tsx +++ b/packages/react/src/ActionMenu/ActionMenu.tsx @@ -194,7 +194,7 @@ type MenuOverlayProps = Partial & const Overlay: React.FC> = ({ children, align = 'start', - side = 'outside-bottom', + side, 'aria-labelledby': ariaLabelledby, ...overlayProps }) => { @@ -222,7 +222,7 @@ const Overlay: React.FC> = ({ onOpen={onOpen} onClose={onClose} align={align} - side={side} + side={side ?? (isSubmenu ? 'outside-right' : 'outside-bottom')} overlayProps={overlayProps} focusZoneSettings={{focusOutBehavior: 'wrap'}} > From 11ba250a55256cab9677e5fd53f5065f2fd0370a Mon Sep 17 00:00:00 2001 From: Ian Sanders Date: Wed, 13 Mar 2024 16:11:13 +0000 Subject: [PATCH 05/20] Add story --- .../ActionMenu.examples.stories.tsx | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/packages/react/src/ActionMenu/ActionMenu.examples.stories.tsx b/packages/react/src/ActionMenu/ActionMenu.examples.stories.tsx index d636102cd86..3d05fa2c3f4 100644 --- a/packages/react/src/ActionMenu/ActionMenu.examples.stories.tsx +++ b/packages/react/src/ActionMenu/ActionMenu.examples.stories.tsx @@ -483,3 +483,38 @@ export const OnlyInactiveItems = () => ( ) + +export const WithSubmenus = () => ( + + Edit + + + alert('Copy link clicked')}> + Copy + ⌘C + + alert('Quote reply clicked')}> + Paste + ⌘V + + + + Paste as + + + + alert('Paste as plain text clicked')}> + Plain text + ⌘V + + alert('Paste as rich text clicked')}> + Rich text + ⌘V + + + + + + + +) From 4b588ec0d353e5c5039e0d01e7ba3ea15a86403a Mon Sep 17 00:00:00 2001 From: Ian Sanders Date: Wed, 13 Mar 2024 16:12:39 +0000 Subject: [PATCH 06/20] Fix open on right arrow key --- packages/react/src/ActionMenu/ActionMenu.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/react/src/ActionMenu/ActionMenu.tsx b/packages/react/src/ActionMenu/ActionMenu.tsx index 9f161234303..3bcffb688c8 100644 --- a/packages/react/src/ActionMenu/ActionMenu.tsx +++ b/packages/react/src/ActionMenu/ActionMenu.tsx @@ -116,7 +116,7 @@ const Menu: React.FC> = ({ renderAnchor = anchorProps => React.cloneElement(child, anchorProps) } return null - } else if (child.type === MenuButton) { + } else if (child.type === MenuButton || child.type === MenuItemAnchor) { renderAnchor = anchorProps => React.cloneElement(child, anchorProps) return null } else { @@ -164,10 +164,12 @@ const MenuItemAnchor = React.forwardRef(({children, onKeyDown: externalOnKeyDown const anchorRef = React.useRef(null) useRefObjectAsForwardedRef(forwardedRef, anchorRef) + const {onOpen} = React.useContext(MenuContext) + /** Treat right arrow key press as click. */ const onKeyDown: React.KeyboardEventHandler = event => { externalOnKeyDown?.(event) - if (event.key === 'ArrowRight' && !event.defaultPrevented) anchorRef.current?.click() + if (event.key === 'ArrowRight' && !event.defaultPrevented) onOpen?.('anchor-key-press') } return ( From 0ccdd5eb0cbf3aac8d8bf919263c5c72a77d7580 Mon Sep 17 00:00:00 2001 From: Ian Sanders Date: Wed, 13 Mar 2024 16:37:25 +0000 Subject: [PATCH 07/20] Add unit tests --- .../react/src/__tests__/ActionMenu.test.tsx | 165 +++++++++++++++++- 1 file changed, 164 insertions(+), 1 deletion(-) diff --git a/packages/react/src/__tests__/ActionMenu.test.tsx b/packages/react/src/__tests__/ActionMenu.test.tsx index e2c871c931f..ef178485a6a 100644 --- a/packages/react/src/__tests__/ActionMenu.test.tsx +++ b/packages/react/src/__tests__/ActionMenu.test.tsx @@ -1,4 +1,4 @@ -import {render as HTMLRender, waitFor, act} from '@testing-library/react' +import {render as HTMLRender, waitFor, act, within} from '@testing-library/react' import userEvent from '@testing-library/user-event' import {axe} from 'jest-axe' import React from 'react' @@ -77,6 +77,56 @@ function ExampleWithTooltipV2(actionMenuTrigger: React.ReactElement): JSX.Elemen ) } +function ExampleWithSubmenus(): JSX.Element { + return ( + + + + + Toggle Menu + + + New file + + Copy link + Edit file + + Paste + + Paste special + + + Paste plain text + Paste formulas + Paste with formatting + + Paste from + + + { + /*noop*/ + }} + > + Current clipboard + + History + Another device + + + + + + + + + + + + + ) +} + describe('ActionMenu', () => { behavesAsComponent({ Component: ActionList, @@ -398,4 +448,117 @@ describe('ActionMenu', () => { expect(button.id).toBe(buttonId) }) + + describe('submenus', () => { + it('sets `aria-haspopup` and `aria-expanded` on submenu anchors', async () => { + const component = HTMLRender() + const user = userEvent.setup() + + const baseAnchor = component.getByRole('button', {name: 'Toggle Menu'}) + await user.click(baseAnchor) + + const submenuAnchor = component.getByRole('menuitem', {name: 'Paste special'}) + expect(submenuAnchor).toHaveAttribute('aria-haspopup') + await user.click(submenuAnchor) + expect(submenuAnchor).toHaveAttribute('aria-expanded') + + const subSubmenuAnchor = component.getByRole('menuitem', {name: 'Paste from'}) + expect(subSubmenuAnchor).toHaveAttribute('aria-haspopup') + await user.click(subSubmenuAnchor) + expect(subSubmenuAnchor).toHaveAttribute('aria-expanded') + }) + + it('sets labels on submenus', async () => { + const component = HTMLRender() + const user = userEvent.setup() + + const baseAnchor = component.getByRole('button', {name: 'Toggle Menu'}) + await user.click(baseAnchor) + + const submenuAnchor = component.getByRole('menuitem', {name: 'Paste special'}) + await user.click(submenuAnchor) + const submenu = component.getByRole('menu', {name: 'Paste special'}) + expect(submenu).toBeVisible() + + const subSubmenuAnchor = within(submenu).getByRole('menuitem', {name: 'Paste from'}) + await user.click(subSubmenuAnchor) + const subSubmenu = component.getByRole('menu', {name: 'Paste from'}) + expect(subSubmenu).toBeVisible() + }) + + it('does not open top-level menu on right arrow key press', async () => { + const component = HTMLRender() + const user = userEvent.setup() + + const baseAnchor = component.getByRole('button', {name: 'Toggle Menu'}) + baseAnchor.focus() + + await user.keyboard('{ArrowRight}') + expect(component.queryByRole('menu')).not.toBeInTheDocument() + expect(baseAnchor).not.toHaveAttribute('aria-expanded') + }) + + it('opens submenus on enter or right arrow key press', async () => { + const component = HTMLRender() + const user = userEvent.setup() + + const baseAnchor = component.getByRole('button', {name: 'Toggle Menu'}) + await user.click(baseAnchor) + + const submenuAnchor = component.getByRole('menuitem', {name: 'Paste special'}) + expect(submenuAnchor).toHaveAttribute('aria-haspopup') + submenuAnchor.focus() + await user.keyboard('{Enter}') + expect(submenuAnchor).toHaveAttribute('aria-expanded') + + const subSubmenuAnchor = component.getByRole('menuitem', {name: 'Paste from'}) + subSubmenuAnchor.focus() + await user.keyboard('{ArrowRight}') + expect(subSubmenuAnchor).toHaveAttribute('aria-expanded') + }) + + it('closes top menu on escape or left arrow key press', async () => { + const component = HTMLRender() + const user = userEvent.setup() + + const baseAnchor = component.getByRole('button', {name: 'Toggle Menu'}) + await user.click(baseAnchor) + + const submenuAnchor = component.getByRole('menuitem', {name: 'Paste special'}) + await user.click(submenuAnchor) + + const subSubmenuAnchor = component.getByRole('menuitem', {name: 'Paste from'}) + await user.click(subSubmenuAnchor) + + expect(subSubmenuAnchor).toHaveAttribute('aria-expanded') + + await user.keyboard('{Escape}') + expect(subSubmenuAnchor).not.toHaveAttribute('aria-expanded') + expect(submenuAnchor).toHaveAttribute('aria-expanded') + + await user.keyboard('{ArrowLeft}') + expect(submenuAnchor).not.toHaveAttribute('aria-expanded') + + expect(baseAnchor).toHaveAttribute('aria-expanded') + }) + + it('closes all menus when an item is selected', async () => { + const component = HTMLRender() + const user = userEvent.setup() + + const baseAnchor = component.getByRole('button', {name: 'Toggle Menu'}) + await user.click(baseAnchor) + + const submenuAnchor = component.getByRole('menuitem', {name: 'Paste special'}) + await user.click(submenuAnchor) + + const subSubmenuAnchor = component.getByRole('menuitem', {name: 'Paste from'}) + await user.click(subSubmenuAnchor) + + const subSubmenuItem = component.getByRole('menuitem', {name: 'Current clipboard'}) + await user.click(subSubmenuItem) + + expect(baseAnchor).not.toHaveAttribute('aria-expanded') + }) + }) }) From ebf527413c045623db2c5cac7ee7044426270ec1 Mon Sep 17 00:00:00 2001 From: Ian Sanders Date: Wed, 13 Mar 2024 16:51:23 +0000 Subject: [PATCH 08/20] Move story and improve --- .../ActionMenu.examples.stories.tsx | 35 ------------------- .../ActionMenu.features.stories.tsx | 33 +++++++++++++++++ 2 files changed, 33 insertions(+), 35 deletions(-) diff --git a/packages/react/src/ActionMenu/ActionMenu.examples.stories.tsx b/packages/react/src/ActionMenu/ActionMenu.examples.stories.tsx index 3d05fa2c3f4..d636102cd86 100644 --- a/packages/react/src/ActionMenu/ActionMenu.examples.stories.tsx +++ b/packages/react/src/ActionMenu/ActionMenu.examples.stories.tsx @@ -483,38 +483,3 @@ export const OnlyInactiveItems = () => ( ) - -export const WithSubmenus = () => ( - - Edit - - - alert('Copy link clicked')}> - Copy - ⌘C - - alert('Quote reply clicked')}> - Paste - ⌘V - - - - Paste as - - - - alert('Paste as plain text clicked')}> - Plain text - ⌘V - - alert('Paste as rich text clicked')}> - Rich text - ⌘V - - - - - - - -) diff --git a/packages/react/src/ActionMenu/ActionMenu.features.stories.tsx b/packages/react/src/ActionMenu/ActionMenu.features.stories.tsx index cb65e4e8e6e..1d56286a19e 100644 --- a/packages/react/src/ActionMenu/ActionMenu.features.stories.tsx +++ b/packages/react/src/ActionMenu/ActionMenu.features.stories.tsx @@ -179,3 +179,36 @@ export const InactiveItems = () => ( ) + +export const Submenus = () => ( + + Edit + + + Cut + Copy + Paste + + Paste special + + + Paste plain text + Paste formulas + Paste with formatting + + Paste from + + + Current clipboard + History + Another device + + + + + + + + + +) From d7251214abe57cd8196a537e3db15858bb243bd8 Mon Sep 17 00:00:00 2001 From: Ian Sanders Date: Wed, 13 Mar 2024 12:55:39 -0400 Subject: [PATCH 09/20] Create actionmenu-submenus.md --- .changeset/wild-students-bow.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/wild-students-bow.md diff --git a/.changeset/wild-students-bow.md b/.changeset/wild-students-bow.md new file mode 100644 index 00000000000..9078808151b --- /dev/null +++ b/.changeset/wild-students-bow.md @@ -0,0 +1,6 @@ +--- +"@primer/react": feature +"docs": patch +--- + +Adds support for nested submenus to `ActionMenu`, along with a new `ActionMenu.MenuItemAnchor` component From e8223b18e9a69d28e4428606b5118939c5ed191a Mon Sep 17 00:00:00 2001 From: Ian Sanders Date: Tue, 19 Mar 2024 12:21:20 -0400 Subject: [PATCH 10/20] Update .changeset/wild-students-bow.md --- .changeset/wild-students-bow.md | 1 - 1 file changed, 1 deletion(-) diff --git a/.changeset/wild-students-bow.md b/.changeset/wild-students-bow.md index 9078808151b..68156c245c8 100644 --- a/.changeset/wild-students-bow.md +++ b/.changeset/wild-students-bow.md @@ -1,6 +1,5 @@ --- "@primer/react": feature -"docs": patch --- Adds support for nested submenus to `ActionMenu`, along with a new `ActionMenu.MenuItemAnchor` component From d0c3710a72074043d2c46680bb8948e61799f000 Mon Sep 17 00:00:00 2001 From: Ian Sanders Date: Thu, 21 Mar 2024 19:52:38 +0000 Subject: [PATCH 11/20] Refactor --- packages/react/src/ActionMenu/ActionMenu.tsx | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/react/src/ActionMenu/ActionMenu.tsx b/packages/react/src/ActionMenu/ActionMenu.tsx index 3bcffb688c8..99798d1c7d4 100644 --- a/packages/react/src/ActionMenu/ActionMenu.tsx +++ b/packages/react/src/ActionMenu/ActionMenu.tsx @@ -3,12 +3,7 @@ import {TriangleDownIcon, ChevronRightIcon} from '@primer/octicons-react' import type {AnchoredOverlayProps} from '../AnchoredOverlay' import {AnchoredOverlay} from '../AnchoredOverlay' import type {OverlayProps} from '../Overlay' -import { - useProvidedRefOrCreate, - useProvidedStateOrCreate, - useMenuKeyboardNavigation, - useRefObjectAsForwardedRef, -} from '../hooks' +import {useProvidedRefOrCreate, useProvidedStateOrCreate, useMenuKeyboardNavigation} from '../hooks' import {Divider} from '../ActionList/Divider' import {ActionListContainerContext} from '../ActionList/ActionListContainerContext' import type {ButtonProps} from '../Button' @@ -160,10 +155,7 @@ const MenuButton = React.forwardRef(({...props}, anchorRef) => { }) as PolymorphicForwardRefComponent<'button', ActionMenuButtonProps> export type MenuItemAnchorProps = ActionListItemProps -const MenuItemAnchor = React.forwardRef(({children, onKeyDown: externalOnKeyDown, ...props}, forwardedRef) => { - const anchorRef = React.useRef(null) - useRefObjectAsForwardedRef(forwardedRef, anchorRef) - +const MenuItemAnchor = React.forwardRef(({children, onKeyDown: externalOnKeyDown, ...props}, anchorRef) => { const {onOpen} = React.useContext(MenuContext) /** Treat right arrow key press as click. */ From fb103fb21f7cd06b75e6cb547e617b4a34fb9571 Mon Sep 17 00:00:00 2001 From: Ian Sanders Date: Fri, 22 Mar 2024 14:25:18 +0000 Subject: [PATCH 12/20] Replace `MenuItemAnchor` with auto wiring for `ActionList.Item` --- .changeset/wild-students-bow.md | 2 +- .../react/src/ActionMenu/ActionMenu.docs.json | 18 +----- .../ActionMenu.features.stories.tsx | 24 ++++++- packages/react/src/ActionMenu/ActionMenu.tsx | 63 ++++++++++--------- .../react/src/__tests__/ActionMenu.test.tsx | 8 ++- 5 files changed, 63 insertions(+), 52 deletions(-) diff --git a/.changeset/wild-students-bow.md b/.changeset/wild-students-bow.md index 68156c245c8..c0f1a6faedc 100644 --- a/.changeset/wild-students-bow.md +++ b/.changeset/wild-students-bow.md @@ -2,4 +2,4 @@ "@primer/react": feature --- -Adds support for nested submenus to `ActionMenu`, along with a new `ActionMenu.MenuItemAnchor` component +Adds support for nested submenus to `ActionMenu` diff --git a/packages/react/src/ActionMenu/ActionMenu.docs.json b/packages/react/src/ActionMenu/ActionMenu.docs.json index e802013d6bc..68856562d41 100644 --- a/packages/react/src/ActionMenu/ActionMenu.docs.json +++ b/packages/react/src/ActionMenu/ActionMenu.docs.json @@ -10,7 +10,7 @@ "type": "React.ReactElement[]", "defaultValue": "", "required": true, - "description": "Recommended: `ActionMenu.Button`, `ActionMenu.MenuItemAnchor`, or `ActionMenu.Anchor`, with `ActionMenu.Overlay`" + "description": "Recommended: `ActionMenu.Button`, or `ActionMenu.Anchor`, with `ActionMenu.Overlay`" }, { "name": "open", @@ -48,22 +48,6 @@ "url": "/react/Button" } }, - { - "name": "ActionMenu.MenuItemAnchor", - "props": [ - { - "name": "children", - "type": "React.ReactElement", - "defaultValue": "", - "required": true, - "description": "" - } - ], - "passthrough": { - "element": "ActionList.Item", - "url": "/react/ActionList" - } - }, { "name": "ActionMenu.Anchor", "props": [ diff --git a/packages/react/src/ActionMenu/ActionMenu.features.stories.tsx b/packages/react/src/ActionMenu/ActionMenu.features.stories.tsx index 1d56286a19e..7b62e337582 100644 --- a/packages/react/src/ActionMenu/ActionMenu.features.stories.tsx +++ b/packages/react/src/ActionMenu/ActionMenu.features.stories.tsx @@ -1,6 +1,15 @@ import React from 'react' import {ActionMenu, ActionList, Box} from '../' -import {WorkflowIcon, ArchiveIcon, GearIcon, CopyIcon, RocketIcon, CommentIcon, BookIcon} from '@primer/octicons-react' +import { + WorkflowIcon, + ArchiveIcon, + GearIcon, + CopyIcon, + RocketIcon, + CommentIcon, + BookIcon, + SparkleFillIcon, +} from '@primer/octicons-react' export default { title: 'Components/ActionMenu/Features', @@ -189,14 +198,23 @@ export const Submenus = () => ( Copy Paste - Paste special + + + + + + Paste special + + Paste plain text Paste formulas Paste with formatting - Paste from + + Paste from + Current clipboard diff --git a/packages/react/src/ActionMenu/ActionMenu.tsx b/packages/react/src/ActionMenu/ActionMenu.tsx index 5106ec5ff69..9d4907a784d 100644 --- a/packages/react/src/ActionMenu/ActionMenu.tsx +++ b/packages/react/src/ActionMenu/ActionMenu.tsx @@ -1,4 +1,4 @@ -import React, {useContext} from 'react' +import React, {useCallback, useContext} from 'react' import {TriangleDownIcon, ChevronRightIcon} from '@primer/octicons-react' import type {AnchoredOverlayProps} from '../AnchoredOverlay' import {AnchoredOverlay} from '../AnchoredOverlay' @@ -12,7 +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 {ActionList, type ActionListItemProps} from '../ActionList' +import {ActionList} from '../ActionList' export type MenuCloseHandler = ( gesture: 'anchor-click' | 'click-outside' | 'escape' | 'tab' | 'item-select' | 'arrow-left', @@ -111,7 +111,7 @@ const Menu: React.FC> = ({ renderAnchor = anchorProps => React.cloneElement(child, anchorProps) } return null - } else if (child.type === MenuButton || child.type === MenuItemAnchor) { + } else if (child.type === MenuButton) { renderAnchor = anchorProps => React.cloneElement(child, anchorProps) return null } else { @@ -139,7 +139,36 @@ const Menu: React.FC> = ({ export type ActionMenuAnchorProps = {children: React.ReactElement; id?: string} const Anchor = React.forwardRef(({children, ...anchorProps}, anchorRef) => { - return React.cloneElement(children, {...anchorProps, ref: anchorRef}) + const {onOpen, isSubmenu} = React.useContext(MenuContext) + + const openSubmenuOnRightArrow: React.KeyboardEventHandler = useCallback( + event => { + children.props.onKeyDown?.(event) + if (isSubmenu && event.key === 'ArrowRight' && !event.defaultPrevented) onOpen?.('anchor-key-press') + }, + [children, isSubmenu, onOpen], + ) + + // Add right chevron icon to submenu anchors (note that unlike other anchor logic this doesn't work if the menu + // item is wrapped in Tooltip [see logic in top-level `ActionMenu` component], which is OK here since tooltips are + // really not a good idea inside submenus) + const grandChildren = + isSubmenu && children.type === ActionList.Item + ? [ + children.props.children, + // eslint-disable-next-line primer-react/direct-slot-children + + + , + ] + : children.props.children + + return React.cloneElement(children, { + ...anchorProps, + ref: anchorRef, + onKeyDown: openSubmenuOnRightArrow, + children: grandChildren, + }) }) /** this component is syntactical sugar 🍭 */ @@ -154,30 +183,6 @@ const MenuButton = React.forwardRef(({...props}, anchorRef) => { ) }) as PolymorphicForwardRefComponent<'button', ActionMenuButtonProps> -export type MenuItemAnchorProps = ActionListItemProps -const MenuItemAnchor = React.forwardRef(({children, onKeyDown: externalOnKeyDown, ...props}, anchorRef) => { - const {onOpen} = React.useContext(MenuContext) - - /** Treat right arrow key press as click. */ - const onKeyDown: React.KeyboardEventHandler = event => { - externalOnKeyDown?.(event) - if (event.key === 'ArrowRight' && !event.defaultPrevented) onOpen?.('anchor-key-press') - } - - return ( - - - {/* Slots will grab the first TrailingVisual encountered. so by putting children first we allow the consumer - to override the chevron icon. */} - {children} - - - - - - ) -}) as PolymorphicForwardRefComponent<'li', MenuItemAnchorProps> - type MenuOverlayProps = Partial & Pick & { /** @@ -238,4 +243,4 @@ const Overlay: React.FC> = ({ } Menu.displayName = 'ActionMenu' -export const ActionMenu = Object.assign(Menu, {Button: MenuButton, MenuItemAnchor, Anchor, Overlay, Divider}) +export const ActionMenu = Object.assign(Menu, {Button: MenuButton, Anchor, Overlay, Divider}) diff --git a/packages/react/src/__tests__/ActionMenu.test.tsx b/packages/react/src/__tests__/ActionMenu.test.tsx index 60177dd8ffa..ef5bf2c67b1 100644 --- a/packages/react/src/__tests__/ActionMenu.test.tsx +++ b/packages/react/src/__tests__/ActionMenu.test.tsx @@ -94,14 +94,18 @@ function ExampleWithSubmenus(): JSX.Element { Paste - Paste special + + Paste special + Paste plain text Paste formulas Paste with formatting - Paste from + + Paste from + Date: Mon, 25 Mar 2024 17:01:49 +0000 Subject: [PATCH 13/20] Use context instead of manipulating children --- .../ActionList/ActionListContainerContext.tsx | 1 + packages/react/src/ActionList/Item.tsx | 9 +++-- packages/react/src/ActionMenu/ActionMenu.tsx | 40 +++++++++---------- 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/packages/react/src/ActionList/ActionListContainerContext.tsx b/packages/react/src/ActionList/ActionListContainerContext.tsx index 1127042aa56..57370225370 100644 --- a/packages/react/src/ActionList/ActionListContainerContext.tsx +++ b/packages/react/src/ActionList/ActionListContainerContext.tsx @@ -14,6 +14,7 @@ type ContextProps = { // eslint-disable-next-line @typescript-eslint/ban-types afterSelect?: Function enableFocusZone?: boolean + defaultTrailingVisual?: React.ReactElement } export const ActionListContainerContext = React.createContext({}) diff --git a/packages/react/src/ActionList/Item.tsx b/packages/react/src/ActionList/Item.tsx index 140ceeb6e20..0c1943dcb4d 100644 --- a/packages/react/src/ActionList/Item.tsx +++ b/packages/react/src/ActionList/Item.tsx @@ -73,6 +73,10 @@ export const Item = React.forwardRef( inlineDescription: [Description, props => props.variant !== 'block'], }) + const {container, afterSelect, selectionAttribute, defaultTrailingVisual} = + React.useContext(ActionListContainerContext) + const trailingVisual = slots.trailingVisual ?? defaultTrailingVisual + const { variant: listVariant, role: listRole, @@ -80,7 +84,6 @@ export const Item = React.forwardRef( selectionVariant: listSelectionVariant, } = React.useContext(ListContext) const {selectionVariant: groupSelectionVariant} = React.useContext(GroupContext) - const {container, afterSelect, selectionAttribute} = React.useContext(ActionListContainerContext) const inactive = Boolean(inactiveText) const showInactiveIndicator = inactive && container === undefined @@ -307,7 +310,7 @@ export const Item = React.forwardRef( sx={{display: 'flex', flexDirection: 'column', flexGrow: 1, minWidth: 0}} > ( ) : ( // If it's not inactive, or it has a leading visual that can be replaced, // just render the trailing visual slot. - slots.trailingVisual + trailingVisual ) } diff --git a/packages/react/src/ActionMenu/ActionMenu.tsx b/packages/react/src/ActionMenu/ActionMenu.tsx index 9d4907a784d..cd4d14f023a 100644 --- a/packages/react/src/ActionMenu/ActionMenu.tsx +++ b/packages/react/src/ActionMenu/ActionMenu.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useContext} from 'react' +import React, {useCallback, useContext, useMemo} from 'react' import {TriangleDownIcon, ChevronRightIcon} from '@primer/octicons-react' import type {AnchoredOverlayProps} from '../AnchoredOverlay' import {AnchoredOverlay} from '../AnchoredOverlay' @@ -12,7 +12,6 @@ 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 {ActionList} from '../ActionList' export type MenuCloseHandler = ( gesture: 'anchor-click' | 'click-outside' | 'escape' | 'tab' | 'item-select' | 'arrow-left', @@ -149,26 +148,23 @@ const Anchor = React.forwardRef(({children, [children, isSubmenu, onOpen], ) - // Add right chevron icon to submenu anchors (note that unlike other anchor logic this doesn't work if the menu - // item is wrapped in Tooltip [see logic in top-level `ActionMenu` component], which is OK here since tooltips are - // really not a good idea inside submenus) - const grandChildren = - isSubmenu && children.type === ActionList.Item - ? [ - children.props.children, - // eslint-disable-next-line primer-react/direct-slot-children - - - , - ] - : children.props.children - - return React.cloneElement(children, { - ...anchorProps, - ref: anchorRef, - onKeyDown: openSubmenuOnRightArrow, - children: grandChildren, - }) + // Add right chevron icon to submenu anchors rendered using `ActionList.Item` + const parentActionListContext = useContext(ActionListContainerContext) + const thisActionListContext = useMemo( + () => + isSubmenu ? {...parentActionListContext, defaultTrailingVisual: } : parentActionListContext, + [isSubmenu, parentActionListContext], + ) + + return ( + + {React.cloneElement(children, { + ...anchorProps, + ref: anchorRef, + onKeyDown: openSubmenuOnRightArrow, + })} + + ) }) /** this component is syntactical sugar 🍭 */ From 1c81b72682693273a29e1be63a826522c77ffcf9 Mon Sep 17 00:00:00 2001 From: Ian Sanders Date: Wed, 27 Mar 2024 15:03:58 -0400 Subject: [PATCH 14/20] Update packages/react/src/ActionMenu/ActionMenu.docs.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Armağan --- packages/react/src/ActionMenu/ActionMenu.docs.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/src/ActionMenu/ActionMenu.docs.json b/packages/react/src/ActionMenu/ActionMenu.docs.json index 68856562d41..398386f157b 100644 --- a/packages/react/src/ActionMenu/ActionMenu.docs.json +++ b/packages/react/src/ActionMenu/ActionMenu.docs.json @@ -10,7 +10,7 @@ "type": "React.ReactElement[]", "defaultValue": "", "required": true, - "description": "Recommended: `ActionMenu.Button`, or `ActionMenu.Anchor`, with `ActionMenu.Overlay`" + "description": "Recommended: `ActionMenu.Button` or `ActionMenu.Anchor` with `ActionMenu.Overlay`" }, { "name": "open", From 5ac52c8bd7c6b1f224e6571286f2fe9442b39cf7 Mon Sep 17 00:00:00 2001 From: Ian Sanders Date: Wed, 27 Mar 2024 19:44:40 +0000 Subject: [PATCH 15/20] Add wrapped component to storybook example --- .../react/src/ActionMenu/ActionMenu.features.stories.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/react/src/ActionMenu/ActionMenu.features.stories.tsx b/packages/react/src/ActionMenu/ActionMenu.features.stories.tsx index 670e3b49cc8..86c9bde8fa8 100644 --- a/packages/react/src/ActionMenu/ActionMenu.features.stories.tsx +++ b/packages/react/src/ActionMenu/ActionMenu.features.stories.tsx @@ -191,6 +191,9 @@ export const InactiveItems = () => ( ) +// Showing that it works with wrapped components +const PasteFromMenuItem = () => Paste from + export const Submenus = () => ( Edit @@ -215,7 +218,7 @@ export const Submenus = () => ( Paste with formatting - Paste from + From 6a00e67e8cdd3b072d88653a1307582f3896c924 Mon Sep 17 00:00:00 2001 From: Ian Sanders Date: Wed, 27 Mar 2024 19:45:30 +0000 Subject: [PATCH 16/20] Fix changeset --- .changeset/wild-students-bow.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/wild-students-bow.md b/.changeset/wild-students-bow.md index c0f1a6faedc..111e2d290e9 100644 --- a/.changeset/wild-students-bow.md +++ b/.changeset/wild-students-bow.md @@ -1,5 +1,5 @@ --- -"@primer/react": feature +"@primer/react": minor --- Adds support for nested submenus to `ActionMenu` From 8f185ab00a0067fbf9e32ae5b2d5f0cec7cf4ff4 Mon Sep 17 00:00:00 2001 From: Ian Sanders Date: Wed, 27 Mar 2024 19:57:11 +0000 Subject: [PATCH 17/20] Fix default trailing visual styling --- packages/react/src/ActionList/Item.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/src/ActionList/Item.tsx b/packages/react/src/ActionList/Item.tsx index cbe294a1c38..0cf2caa6923 100644 --- a/packages/react/src/ActionList/Item.tsx +++ b/packages/react/src/ActionList/Item.tsx @@ -76,7 +76,7 @@ export const Item = React.forwardRef( const {container, afterSelect, selectionAttribute, defaultTrailingVisual} = React.useContext(ActionListContainerContext) - const trailingVisual = slots.trailingVisual ?? defaultTrailingVisual + const trailingVisual = slots.trailingVisual ?? {defaultTrailingVisual} const { variant: listVariant, From 98d8a1119b9194ee61082a4b06af5e0657224617 Mon Sep 17 00:00:00 2001 From: Ian Sanders Date: Wed, 27 Mar 2024 19:59:14 +0000 Subject: [PATCH 18/20] Revert "Add wrapped component to storybook example" This reverts commit 5ac52c8bd7c6b1f224e6571286f2fe9442b39cf7. --- .../react/src/ActionMenu/ActionMenu.features.stories.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/react/src/ActionMenu/ActionMenu.features.stories.tsx b/packages/react/src/ActionMenu/ActionMenu.features.stories.tsx index 86c9bde8fa8..670e3b49cc8 100644 --- a/packages/react/src/ActionMenu/ActionMenu.features.stories.tsx +++ b/packages/react/src/ActionMenu/ActionMenu.features.stories.tsx @@ -191,9 +191,6 @@ export const InactiveItems = () => ( ) -// Showing that it works with wrapped components -const PasteFromMenuItem = () => Paste from - export const Submenus = () => ( Edit @@ -218,7 +215,7 @@ export const Submenus = () => ( Paste with formatting - + Paste from From 39568d4d5e5e35816113b98fe70aad7844b968c6 Mon Sep 17 00:00:00 2001 From: Ian Sanders Date: Thu, 28 Mar 2024 14:47:17 +0000 Subject: [PATCH 19/20] Don't render the trailing visual wrapper unless there actually is a default --- packages/react/src/ActionList/Item.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/react/src/ActionList/Item.tsx b/packages/react/src/ActionList/Item.tsx index 0cf2caa6923..640f4885541 100644 --- a/packages/react/src/ActionList/Item.tsx +++ b/packages/react/src/ActionList/Item.tsx @@ -76,7 +76,12 @@ export const Item = React.forwardRef( const {container, afterSelect, selectionAttribute, defaultTrailingVisual} = React.useContext(ActionListContainerContext) - const trailingVisual = slots.trailingVisual ?? {defaultTrailingVisual} + + // Be sure to avoid rendering the container unless there is a default + const wrappedDefaultTrailingVisual = defaultTrailingVisual ? ( + {defaultTrailingVisual} + ) : null + const trailingVisual = slots.trailingVisual ?? wrappedDefaultTrailingVisual const { variant: listVariant, From d207bf712680ce574c7eef0caa240c226912075a Mon Sep 17 00:00:00 2001 From: Ian Sanders Date: Thu, 28 Mar 2024 15:14:05 +0000 Subject: [PATCH 20/20] Fix submenu anchor closing menu on `Enter` press --- packages/react/src/ActionMenu/ActionMenu.tsx | 11 ++++++++-- .../react/src/__tests__/ActionMenu.test.tsx | 20 +++++++++---------- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/packages/react/src/ActionMenu/ActionMenu.tsx b/packages/react/src/ActionMenu/ActionMenu.tsx index cd4d14f023a..5ef5008e711 100644 --- a/packages/react/src/ActionMenu/ActionMenu.tsx +++ b/packages/react/src/ActionMenu/ActionMenu.tsx @@ -152,8 +152,15 @@ const Anchor = React.forwardRef(({children, const parentActionListContext = useContext(ActionListContainerContext) const thisActionListContext = useMemo( () => - isSubmenu ? {...parentActionListContext, defaultTrailingVisual: } : parentActionListContext, - [isSubmenu, parentActionListContext], + isSubmenu + ? { + ...parentActionListContext, + defaultTrailingVisual: , + // Default behavior is to close after selecting; we want to open the submenu instead + afterSelect: () => onOpen?.('anchor-click'), + } + : parentActionListContext, + [isSubmenu, onOpen, parentActionListContext], ) return ( diff --git a/packages/react/src/__tests__/ActionMenu.test.tsx b/packages/react/src/__tests__/ActionMenu.test.tsx index ef5bf2c67b1..5598e686de2 100644 --- a/packages/react/src/__tests__/ActionMenu.test.tsx +++ b/packages/react/src/__tests__/ActionMenu.test.tsx @@ -533,7 +533,7 @@ describe('ActionMenu', () => { await user.keyboard('{ArrowRight}') expect(component.queryByRole('menu')).not.toBeInTheDocument() - expect(baseAnchor).not.toHaveAttribute('aria-expanded') + expect(baseAnchor).not.toHaveAttribute('aria-expanded', 'true') }) it('opens submenus on enter or right arrow key press', async () => { @@ -544,15 +544,15 @@ describe('ActionMenu', () => { await user.click(baseAnchor) const submenuAnchor = component.getByRole('menuitem', {name: 'Paste special'}) - expect(submenuAnchor).toHaveAttribute('aria-haspopup') + expect(submenuAnchor).toHaveAttribute('aria-haspopup', 'true') submenuAnchor.focus() await user.keyboard('{Enter}') - expect(submenuAnchor).toHaveAttribute('aria-expanded') + expect(submenuAnchor).toHaveAttribute('aria-expanded', 'true') const subSubmenuAnchor = component.getByRole('menuitem', {name: 'Paste from'}) subSubmenuAnchor.focus() await user.keyboard('{ArrowRight}') - expect(subSubmenuAnchor).toHaveAttribute('aria-expanded') + expect(subSubmenuAnchor).toHaveAttribute('aria-expanded', 'true') }) it('closes top menu on escape or left arrow key press', async () => { @@ -568,16 +568,16 @@ describe('ActionMenu', () => { const subSubmenuAnchor = component.getByRole('menuitem', {name: 'Paste from'}) await user.click(subSubmenuAnchor) - expect(subSubmenuAnchor).toHaveAttribute('aria-expanded') + expect(subSubmenuAnchor).toHaveAttribute('aria-expanded', 'true') await user.keyboard('{Escape}') - expect(subSubmenuAnchor).not.toHaveAttribute('aria-expanded') - expect(submenuAnchor).toHaveAttribute('aria-expanded') + expect(subSubmenuAnchor).not.toHaveAttribute('aria-expanded', 'true') + expect(submenuAnchor).toHaveAttribute('aria-expanded', 'true') await user.keyboard('{ArrowLeft}') - expect(submenuAnchor).not.toHaveAttribute('aria-expanded') + expect(submenuAnchor).not.toHaveAttribute('aria-expanded', 'true') - expect(baseAnchor).toHaveAttribute('aria-expanded') + expect(baseAnchor).toHaveAttribute('aria-expanded', 'true') }) it('closes all menus when an item is selected', async () => { @@ -596,7 +596,7 @@ describe('ActionMenu', () => { const subSubmenuItem = component.getByRole('menuitem', {name: 'Current clipboard'}) await user.click(subSubmenuItem) - expect(baseAnchor).not.toHaveAttribute('aria-expanded') + expect(baseAnchor).not.toHaveAttribute('aria-expanded', 'true') }) }) })