diff --git a/cypress/e2e/ui/menuButton.cy.ts b/cypress/e2e/ui/menuButton.cy.ts index 9ed150cbe..6403cc4cd 100644 --- a/cypress/e2e/ui/menuButton.cy.ts +++ b/cypress/e2e/ui/menuButton.cy.ts @@ -77,4 +77,23 @@ context('Components/MenuButton', () => { cy.get('#menu-item-2').focus() cy.get('#menu-button[aria-expanded="true"]').should('exist') }) + + it('clicking should open/close menu (with selected items)', () => { + cy.visit('/frame/?path=/components/menu/selected-item') + + // click button + cy.get('#menu-button').click() + cy.get('#menu-button[aria-expanded="true"]').should('exist') + + // click the same button again + cy.get('#menu-button').click() + cy.get('#menu-button[aria-expanded="false"]').should('exist') + }) + + it('should show the selected menu item when opened', () => { + cy.visit('/frame/?path=/components/menu/selected-item') + + cy.get('#menu-button').click() + cy.get('#menu-item-2').should('be.focused') + }) }) diff --git a/src/core/components/menu/__workshop__/selectedItem.tsx b/src/core/components/menu/__workshop__/selectedItem.tsx index be3280a90..e1db160a8 100644 --- a/src/core/components/menu/__workshop__/selectedItem.tsx +++ b/src/core/components/menu/__workshop__/selectedItem.tsx @@ -16,8 +16,10 @@ const POPOVER_PROPS: MenuButtonProps['popover'] = { matchReferenceWidth: true, } +const INITIAL_INDEX = 1 + export default function SelectedItemStory() { - const [selectedIndex, setSelectedIndex] = useState(0) + const [selectedIndex, setSelectedIndex] = useState(INITIAL_INDEX) return ( @@ -26,12 +28,13 @@ export default function SelectedItemStory() { } - id="selected-item-example" + id="menu-button" menu={ setSelectedIndex(0)} pressed={selectedIndex === 0} selected={selectedIndex === 0} @@ -40,6 +43,7 @@ export default function SelectedItemStory() { setSelectedIndex(1)} pressed={selectedIndex === 1} selected={selectedIndex === 1} @@ -49,6 +53,7 @@ export default function SelectedItemStory() { setSelectedIndex(2)} pressed={selectedIndex === 2} selected={selectedIndex === 2} diff --git a/src/core/components/menu/menuButton.tsx b/src/core/components/menu/menuButton.tsx index 871ed846c..cf0d129b4 100644 --- a/src/core/components/menu/menuButton.tsx +++ b/src/core/components/menu/menuButton.tsx @@ -99,6 +99,16 @@ export const MenuButton = forwardRef(function MenuButton( setShouldFocus(null) }, []) + // Prevent mouse event propagation when the menu is open. + // This is to ensure that `handleBlur` isn't triggered when clicking the menu button whilst open, + // which can lead to `setOpen` being triggered multiple times (once by `handleBlur`, and again by `handleButtonClick`). + const handleMouseDown = useCallback( + (event: PointerEvent) => { + if (open) event.preventDefault() + }, + [open], + ) + const handleButtonKeyDown = useCallback((event: React.KeyboardEvent) => { // On `ArrowDown`, `Enter` and `Space` // - Opens menu and moves focus to first menuitem @@ -231,13 +241,14 @@ export const MenuButton = forwardRef(function MenuButton( id, onClick: handleButtonClick, onKeyDown: handleButtonKeyDown, + onMouseDown: handleMouseDown, 'aria-haspopup': true, 'aria-expanded': open, ref: setButtonRef, selected: open, }) : null, - [buttonProp, handleButtonClick, handleButtonKeyDown, id, open, setButtonRef], + [buttonProp, handleButtonClick, handleButtonKeyDown, handleMouseDown, id, open, setButtonRef], ) const popoverProps: MenuButtonProps['popover'] = useMemo( diff --git a/stories/components/MenuButton.stories.tsx b/stories/components/MenuButton.stories.tsx index 30e3d17bd..30de04338 100644 --- a/stories/components/MenuButton.stories.tsx +++ b/stories/components/MenuButton.stories.tsx @@ -1,11 +1,13 @@ import {ClockIcon, CommentIcon, ExpandIcon, SearchIcon} from '@sanity/icons' +import {expect} from '@storybook/jest' import type {Meta, StoryObj} from '@storybook/react' +import {userEvent, within} from '@storybook/testing-library' import {Menu, MenuButton, MenuDivider, MenuGroup, MenuItem} from '../../src/core/components' import {Button, Flex} from '../../src/core/primitives' const meta: Meta = { args: { - button: