diff --git a/packages/react-aria-components/src/Menu.tsx b/packages/react-aria-components/src/Menu.tsx index 357a73f7bcc..afe55ecc803 100644 --- a/packages/react-aria-components/src/Menu.tsx +++ b/packages/react-aria-components/src/Menu.tsx @@ -155,7 +155,7 @@ function MenuInner({props, collection, menuRef: ref}: MenuInne children: (item) => { switch (item.type) { case 'section': - return ; + return ; case 'separator': return ; case 'item': @@ -210,10 +210,11 @@ const _Menu = /*#__PURE__*/ (forwardRef as forwardRefType)(Menu); export {_Menu as Menu}; interface MenuSectionProps extends StyleProps { - section: Node + section: Node, + parentMenuRef: RefObject } -function MenuSection({section, className, style, ...otherProps}: MenuSectionProps) { +function MenuSection({section, className, style, parentMenuRef, ...otherProps}: MenuSectionProps) { let state = useContext(MenuStateContext)!; let [headingRef, heading] = useSlot(); let {headingProps, groupProps} = useMenuSection({ @@ -238,6 +239,8 @@ function MenuSection({section, className, style, ...otherProps}: MenuSectionP } case 'item': return ; + case 'submenutrigger': + return ; default: throw new Error('Unsupported element type in Section: ' + item.type); } diff --git a/packages/react-aria-components/stories/Menu.stories.tsx b/packages/react-aria-components/stories/Menu.stories.tsx index 0cc45dc7030..7de7fcd045a 100644 --- a/packages/react-aria-components/stories/Menu.stories.tsx +++ b/packages/react-aria-components/stories/Menu.stories.tsx @@ -188,6 +188,50 @@ export const SubmenuDisabledExample = (args) => ( ); +export const SubmenuSectionsExample = (args) => ( + + + + +
+
Section 1
+ Foo + + Bar + + +
+
Submenu Section 1
+ Submenu Foo + Submenu Bar + Submenu Baz + Google +
+ +
+
Submenu Section 2
+ Submenu Foo + Submenu Bar + Submenu Baz +
+
+
+
+ Baz + Google +
+ +
+
Section 2
+ Foo + Bar + Baz +
+
+
+
+); + let submenuArgs = { args: { delay: 200 diff --git a/packages/react-aria-components/test/Menu.test.js b/packages/react-aria-components/test/Menu.test.js index 482294cf338..50b4804d5bf 100644 --- a/packages/react-aria-components/test/Menu.test.js +++ b/packages/react-aria-components/test/Menu.test.js @@ -833,5 +833,108 @@ describe('Menu', () => { expect(menus).toHaveLength(0); expect(menu).not.toBeInTheDocument(); }); + it('should support sections', async () => { + let onAction = jest.fn(); + let {getByRole, getAllByRole} = render( + + + + +
+
Actions
+ Open + Rename… + Duplicate + + Share… + + +
+
Work
+ Email + SMS + Twitter +
+ +
+
Personal
+ Email + SMS + Twitter +
+
+
+
+ Delete… +
+ +
+
Settings
+ User Settings + System Settings +
+
+
+
+ ); + + let button = getByRole('button'); + expect(button).not.toHaveAttribute('data-pressed'); + + await user.click(button); + expect(button).toHaveAttribute('data-pressed'); + + let groups = getAllByRole('group'); + expect(groups).toHaveLength(2); + + expect(groups[0]).toHaveClass('react-aria-Section'); + expect(groups[1]).toHaveClass('react-aria-Section'); + + expect(groups[0]).toHaveAttribute('aria-labelledby'); + expect(document.getElementById(groups[0].getAttribute('aria-labelledby'))).toHaveTextContent('Actions'); + + expect(groups[1]).toHaveAttribute('aria-labelledby'); + expect(document.getElementById(groups[1].getAttribute('aria-labelledby'))).toHaveTextContent('Settings'); + + let menu = getAllByRole('menu')[0]; + expect(getAllByRole('menuitem')).toHaveLength(7); + + let popover = menu.closest('.react-aria-Popover'); + expect(popover).toBeInTheDocument(); + expect(popover).toHaveAttribute('data-trigger', 'MenuTrigger'); + + let triggerItem = getAllByRole('menuitem')[3]; + expect(triggerItem).toHaveTextContent('Share…'); + + // Open the submenu + await user.pointer({target: triggerItem}); + act(() => {jest.runAllTimers();}); + let submenu = getAllByRole('menu')[1]; + expect(submenu).toBeInTheDocument(); + + let submenuItems = within(submenu).getAllByRole('menuitem'); + expect(submenuItems).toHaveLength(6); + + let groupsInSubmenu = within(submenu).getAllByRole('group'); + expect(groupsInSubmenu).toHaveLength(2); + + expect(groupsInSubmenu[0]).toHaveClass('react-aria-Section'); + expect(groupsInSubmenu[1]).toHaveClass('react-aria-Section'); + + expect(groupsInSubmenu[0]).toHaveAttribute('aria-labelledby'); + expect(document.getElementById(groupsInSubmenu[0].getAttribute('aria-labelledby'))).toHaveTextContent('Work'); + + expect(groupsInSubmenu[1]).toHaveAttribute('aria-labelledby'); + expect(document.getElementById(groupsInSubmenu[1].getAttribute('aria-labelledby'))).toHaveTextContent('Personal'); + + await user.click(submenuItems[0]); + act(() => {jest.runAllTimers();}); + + expect(onAction).toHaveBeenCalledTimes(1); + expect(onAction).toHaveBeenLastCalledWith('email-work'); + + expect(submenu).not.toBeInTheDocument(); + expect(menu).not.toBeInTheDocument(); + }); }); });