From 6829f068ba1020ca37fe2752992ebcf9d78e3707 Mon Sep 17 00:00:00 2001 From: Erick Singh Sandhu Date: Fri, 6 Mar 2026 13:45:15 -0600 Subject: [PATCH 1/3] fix: apply hover state on Tags with href (#9750) * bring back hasAction state in TagAria * fix Tag with action being disabled on hover * add new unit test for hover on Tag with href --- packages/@react-aria/tag/src/useTag.ts | 2 +- .../react-aria-components/src/TagGroup.tsx | 2 +- .../test/TagGroup.test.js | 23 +++++++++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/packages/@react-aria/tag/src/useTag.ts b/packages/@react-aria/tag/src/useTag.ts index 7fddd3a956c..266e0d95128 100644 --- a/packages/@react-aria/tag/src/useTag.ts +++ b/packages/@react-aria/tag/src/useTag.ts @@ -24,7 +24,7 @@ import {useGridListItem} from '@react-aria/gridlist'; import {useLocalizedStringFormatter} from '@react-aria/i18n'; -export interface TagAria extends Omit { +export interface TagAria extends SelectableItemStates { /** Props for the tag row element. */ rowProps: DOMAttributes, /** Props for the tag cell element. */ diff --git a/packages/react-aria-components/src/TagGroup.tsx b/packages/react-aria-components/src/TagGroup.tsx index 7df9f7b338e..7aad63d886d 100644 --- a/packages/react-aria-components/src/TagGroup.tsx +++ b/packages/react-aria-components/src/TagGroup.tsx @@ -249,7 +249,7 @@ export const Tag = /*#__PURE__*/ createLeafComponent(ItemNode, (props: TagProps, let {rowProps, gridCellProps, removeButtonProps, ...states} = useTag({item}, state, ref); let {hoverProps, isHovered} = useHover({ - isDisabled: !states.allowsSelection, + isDisabled: !states.allowsSelection && !states.hasAction, onHoverStart: item.props.onHoverStart, onHoverChange: item.props.onHoverChange, onHoverEnd: item.props.onHoverEnd diff --git a/packages/react-aria-components/test/TagGroup.test.js b/packages/react-aria-components/test/TagGroup.test.js index fad4a3aad13..27fc24066ee 100644 --- a/packages/react-aria-components/test/TagGroup.test.js +++ b/packages/react-aria-components/test/TagGroup.test.js @@ -176,6 +176,29 @@ describe('TagGroup', () => { expect(onHoverEnd).not.toHaveBeenCalled(); }); + it('should show hover state when tag has an href even without selectionMode', async () => { + let onHoverStart = jest.fn(); + let onHoverChange = jest.fn(); + let onHoverEnd = jest.fn(); + let {getAllByRole} = renderTagGroup({}, {}, {href: '/', className: ({isHovered}) => isHovered ? 'hover' : '', onHoverStart, onHoverChange, onHoverEnd}); + let row = getAllByRole('row')[0]; + + expect(row).not.toHaveAttribute('data-hovered'); + expect(row).not.toHaveClass('hover'); + + await user.hover(row); + expect(row).toHaveAttribute('data-hovered', 'true'); + expect(row).toHaveClass('hover'); + expect(onHoverStart).toHaveBeenCalledTimes(1); + expect(onHoverChange).toHaveBeenCalledTimes(1); + + await user.unhover(row); + expect(row).not.toHaveAttribute('data-hovered'); + expect(row).not.toHaveClass('hover'); + expect(onHoverEnd).toHaveBeenCalledTimes(1); + expect(onHoverChange).toHaveBeenCalledTimes(2); + }); + it('should support focus ring', async () => { let onFocus = jest.fn(); let onFocusChange = jest.fn(); From 80ef0d833d85f3897cfbab2b00291bf85f42e46a Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Fri, 6 Mar 2026 13:14:47 -0800 Subject: [PATCH 2/3] chore: stop skipping tests in taggroup (#9753) --- packages/react-aria-components/test/TagGroup.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-aria-components/test/TagGroup.test.js b/packages/react-aria-components/test/TagGroup.test.js index 27fc24066ee..37201eb6bf2 100644 --- a/packages/react-aria-components/test/TagGroup.test.js +++ b/packages/react-aria-components/test/TagGroup.test.js @@ -611,7 +611,7 @@ describe('TagGroup', () => { }); describe('press events', () => { - it.only.each` + it.each` interactionType ${'mouse'} ${'keyboard'} From e2414021454df4847d0f211dd98b44607b976917 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Fri, 6 Mar 2026 16:28:38 -0800 Subject: [PATCH 3/3] fix: allow user to move into Autocomplete wrapped 2d grid via ArrowLeft/Right (#9755) also prevents internally tracked focused key from sticking if the inner collection of a Autocomplete is switched/changed to another collection --- .../autocomplete/src/useAutocomplete.ts | 27 +++++++++++++- .../selection/src/useSelectableCollection.ts | 4 +-- .../stories/Autocomplete.stories.tsx | 36 +++++++++++++++++++ 3 files changed, 64 insertions(+), 3 deletions(-) diff --git a/packages/@react-aria/autocomplete/src/useAutocomplete.ts b/packages/@react-aria/autocomplete/src/useAutocomplete.ts index e826dac7dcc..9ff0d4fd2b0 100644 --- a/packages/@react-aria/autocomplete/src/useAutocomplete.ts +++ b/packages/@react-aria/autocomplete/src/useAutocomplete.ts @@ -231,6 +231,14 @@ export function useAutocomplete(props: AriaAutocompleteOptions, state: Aut } let focusedNodeId = queuedActiveDescendant.current; + if (focusedNodeId !== null && getOwnerDocument(inputRef.current).getElementById(focusedNodeId) == null) { + // if the focused id doesn't exist in document, then we need to clear the tracked focused node, otherwise + // we will be attempting to fire key events on a non-existing node instead of trying to focus the newly swapped wrapped collection. + // This can happen if you are swapping out the Autocomplete wrapped collection component like in the docs search. + queuedActiveDescendant.current = null; + focusedNodeId = null; + } + switch (e.key) { case 'a': if (isCtrlKeyPressed(e)) { @@ -260,11 +268,28 @@ export function useAutocomplete(props: AriaAutocompleteOptions, state: Aut case 'PageDown': case 'PageUp': case 'ArrowUp': - case 'ArrowDown': { + case 'ArrowDown': + case 'ArrowRight': + case 'ArrowLeft': { if ((e.key === 'Home' || e.key === 'End') && focusedNodeId == null && e.shiftKey) { return; } + // If there is text within the input field, we'll want continue propagating events down + // to the wrapped collection if there is a focused node so that a user can continue moving the + // virtual focus. However, if the user doesn't have a focus in the collection, just move the text + // cursor instead. They can move focus down into the collection via down/up arrow if need be + if ((e.key === 'ArrowRight' || e.key === 'ArrowLeft') && state.inputValue.length > 0) { + if (focusedNodeId == null) { + if (!e.isPropagationStopped()) { + e.stopPropagation(); + } + return; + } + + break; + } + // Prevent these keys from moving the text cursor in the input e.preventDefault(); // Move virtual focus into the wrapped collection diff --git a/packages/@react-aria/selection/src/useSelectableCollection.ts b/packages/@react-aria/selection/src/useSelectableCollection.ts index 796d7dcaa60..8646efb427c 100644 --- a/packages/@react-aria/selection/src/useSelectableCollection.ts +++ b/packages/@react-aria/selection/src/useSelectableCollection.ts @@ -201,7 +201,7 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions } case 'ArrowLeft': { if (delegate.getKeyLeftOf) { - let nextKey: Key | undefined | null = manager.focusedKey != null ? delegate.getKeyLeftOf?.(manager.focusedKey) : null; + let nextKey: Key | undefined | null = manager.focusedKey != null ? delegate.getKeyLeftOf?.(manager.focusedKey) : delegate.getFirstKey?.(); if (nextKey == null && shouldFocusWrap) { nextKey = direction === 'rtl' ? delegate.getFirstKey?.(manager.focusedKey) : delegate.getLastKey?.(manager.focusedKey); } @@ -214,7 +214,7 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions } case 'ArrowRight': { if (delegate.getKeyRightOf) { - let nextKey: Key | undefined | null = manager.focusedKey != null ? delegate.getKeyRightOf?.(manager.focusedKey) : null; + let nextKey: Key | undefined | null = manager.focusedKey != null ? delegate.getKeyRightOf?.(manager.focusedKey) : delegate.getFirstKey?.(); if (nextKey == null && shouldFocusWrap) { nextKey = direction === 'rtl' ? delegate.getLastKey?.(manager.focusedKey) : delegate.getFirstKey?.(manager.focusedKey); } diff --git a/packages/react-aria-components/stories/Autocomplete.stories.tsx b/packages/react-aria-components/stories/Autocomplete.stories.tsx index bab3529f813..5bc6de445fe 100644 --- a/packages/react-aria-components/stories/Autocomplete.stories.tsx +++ b/packages/react-aria-components/stories/Autocomplete.stories.tsx @@ -1208,3 +1208,39 @@ export const AutocompleteUserCustomFiltering: AutocompleteStory = { } } }; + +export function AutocompleteGrid() { + return ( + +
+ + + + Please select an option below. + + + 1,1 + 1,2 + 1,3 + 2,1 + 2,2 + 2,3 + 3,1 + 3,2 + 3,3 + +
+
+ ); +};