diff --git a/packages/@react-aria/menu/src/useMenuItem.ts b/packages/@react-aria/menu/src/useMenuItem.ts index 498ba53209a..30440abd5d7 100644 --- a/packages/@react-aria/menu/src/useMenuItem.ts +++ b/packages/@react-aria/menu/src/useMenuItem.ts @@ -111,7 +111,7 @@ export function useMenuItem(props: AriaMenuItemProps, state: TreeState, re closeOnSelect, isVirtualized, 'aria-haspopup': hasPopup, - onPressStart: pressStartProp, + onPressStart, onPressUp: pressUpProp, onPress, onPressChange: pressChangeProp, @@ -188,21 +188,18 @@ export function useMenuItem(props: AriaMenuItemProps, state: TreeState, re ariaProps['aria-setsize'] = getItemCount(state.collection); } - let onPressStart = (e: PressEvent) => { - // Trigger native click event on keydown unless this is a link (the browser will trigger onClick then). - if (e.pointerType === 'keyboard' && !selectionManager.isLink(key)) { - (e.target as HTMLElement).click(); - } - - pressStartProp?.(e); - }; let isPressedRef = useRef(false); let onPressChange = (isPressed: boolean) => { pressChangeProp?.(isPressed); isPressedRef.current = isPressed; }; + let interaction = useRef<{pointerType: string, key?: string} | null>(null); let onPressUp = (e: PressEvent) => { + if (e.pointerType !== 'keyboard') { + interaction.current = {pointerType: e.pointerType}; + } + // If interacting with mouse, allow the user to mouse down on the trigger button, // drag, and release over an item (matching native behavior). if (e.pointerType === 'mouse') { @@ -211,12 +208,6 @@ export function useMenuItem(props: AriaMenuItemProps, state: TreeState, re } } - // Pressing a menu item should close by default in single selection mode but not multiple - // selection mode, except if overridden by the closeOnSelect prop. - if (e.pointerType !== 'keyboard' && !isTrigger && onClose && (closeOnSelect ?? (selectionManager.selectionMode !== 'multiple' || selectionManager.isLink(key)))) { - onClose(); - } - pressUpProp?.(e); }; @@ -224,6 +215,19 @@ export function useMenuItem(props: AriaMenuItemProps, state: TreeState, re onClickProp?.(e); performAction(); handleLinkClick(e, router, item!.props.href, item?.props.routerOptions); + + let shouldClose = interaction.current?.pointerType === 'keyboard' + // Always close when pressing Enter key, or if item is not selectable. + ? interaction.current?.key === 'Enter' || selectionManager.selectionMode === 'none' || selectionManager.isLink(key) + // Close except if multi-select is enabled. + : selectionManager.selectionMode !== 'multiple' || selectionManager.isLink(key); + + shouldClose = closeOnSelect ?? shouldClose; + if (onClose && !isTrigger && shouldClose) { + onClose(); + } + + interaction.current = null; }; let {itemProps, isFocused} = useSelectableItem({ @@ -274,14 +278,15 @@ export function useMenuItem(props: AriaMenuItemProps, state: TreeState, re switch (e.key) { case ' ': - if (!isDisabled && selectionManager.selectionMode === 'none' && !isTrigger && closeOnSelect !== false && onClose) { - onClose(); - } + interaction.current = {pointerType: 'keyboard', key: ' '}; + (e.target as HTMLElement).click(); break; case 'Enter': - // The Enter key should always close on select, except if overridden. - if (!isDisabled && closeOnSelect !== false && !isTrigger && onClose) { - onClose(); + interaction.current = {pointerType: 'keyboard', key: 'Enter'}; + + // Trigger click unless this is a link. Links trigger click natively. + if ((e.target as HTMLElement).tagName !== 'A') { + (e.target as HTMLElement).click(); } break; default: diff --git a/packages/@react-aria/tag/src/useTagGroup.ts b/packages/@react-aria/tag/src/useTagGroup.ts index 745325bcaf2..11b32c9508a 100644 --- a/packages/@react-aria/tag/src/useTagGroup.ts +++ b/packages/@react-aria/tag/src/useTagGroup.ts @@ -114,7 +114,7 @@ export function useTagGroup(props: AriaTagGroupOptions, state: ListState { + describe.each(['mouse', 'Enter', 'Space'])('%s', (type) => { it.each(['none', 'single', 'multiple'])('with selectionMode = %s', async function (selectionMode) { let user = userEvent.setup({delay: null, pointerMap}); let onAction = jest.fn(); let onSelectionChange = jest.fn(); let tree = render( - - One - Two - + + + + One + Two + + ); + let button = tree.getByRole('button'); + if (type === 'mouse') { + await user.click(button); + } else { + await user.tab(); + await user.keyboard('{Enter}'); + } + let role = { none: 'menuitem', single: 'menuitemradio', @@ -820,9 +832,9 @@ describe('Menu', function () { if (type === 'mouse') { await user.click(items[1]); } else { - fireEvent.keyDown(items[1], {key: 'Enter'}); + fireEvent.keyDown(items[1], {key: type}); fireEvent.click(items[1]); - fireEvent.keyUp(items[1], {key: 'Enter'}); + fireEvent.keyUp(items[1], {key: type}); } expect(onAction).toHaveBeenCalledTimes(1); expect(onSelectionChange).not.toHaveBeenCalled(); @@ -830,5 +842,33 @@ describe('Menu', function () { window.removeEventListener('click', onClick); }); }); + + it('should support dragging and releasing', async () => { + let user = userEvent.setup({delay: null, pointerMap}); + let onAction = jest.fn(); + let tree = render( + + + + + One + Two + + + + ); + + let button = tree.getByRole('button'); + await user.pointer({target: button, keys: '[MouseLeft>]'}); + + let items = tree.getAllByRole('menuitem'); + let onClick = mockClickDefault({capture: true}); + + await user.pointer({target: items[0], keys: '[/MouseLeft]'}); + + expect(onAction).toHaveBeenCalledTimes(1); + expect(onClick).toHaveBeenCalledTimes(1); + window.removeEventListener('click', onClick); + }); }); }); diff --git a/packages/react-aria-components/stories/TagGroup.stories.tsx b/packages/react-aria-components/stories/TagGroup.stories.tsx index 3b925421906..caf662e9126 100644 --- a/packages/react-aria-components/stories/TagGroup.stories.tsx +++ b/packages/react-aria-components/stories/TagGroup.stories.tsx @@ -111,3 +111,13 @@ export const TagGroupExampleWithRemove: Story = { ) }; + +export const EmptyTagGroup: Story = { + render: (props: TagGroupProps) => ( + + 'No categories.'}> + {[]} + + + ) +}; diff --git a/packages/react-aria-components/test/TagGroup.test.js b/packages/react-aria-components/test/TagGroup.test.js index d227029294f..2dad220810a 100644 --- a/packages/react-aria-components/test/TagGroup.test.js +++ b/packages/react-aria-components/test/TagGroup.test.js @@ -306,6 +306,7 @@ describe('TagGroup', () => { let grid = getByTestId('list'); expect(grid).toHaveAttribute('data-empty', 'true'); expect(grid).toHaveTextContent('No results'); + expect(grid).toHaveAttribute('role', 'group'); }); it('supports tooltips', async function () {