From ae422a71a64df75369e0fd58cba1d5945b15021c Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Thu, 23 Apr 2026 12:00:53 +1000 Subject: [PATCH] fix: onSelectionChange should not be called for tabs that are links --- packages/dev/s2-docs/pages/react-aria/Tabs.mdx | 2 ++ .../stories/Tabs.stories.tsx | 4 ++-- packages/react-aria-components/test/Tabs.test.js | 4 +++- .../src/selection/useSelectableItem.ts | 16 ++++++++++++---- packages/react-aria/src/tabs/useTab.ts | 4 +++- 5 files changed, 22 insertions(+), 8 deletions(-) diff --git a/packages/dev/s2-docs/pages/react-aria/Tabs.mdx b/packages/dev/s2-docs/pages/react-aria/Tabs.mdx index 8783fae31bc..d58e7c8c5ee 100644 --- a/packages/dev/s2-docs/pages/react-aria/Tabs.mdx +++ b/packages/dev/s2-docs/pages/react-aria/Tabs.mdx @@ -187,6 +187,8 @@ function Example() { ### Links Use the `href` prop on a `` to create a link. By default, links are rendered as an `` element. Use the `render` prop to integrate your framework's link component. +By default, links perform native browser navigation. However, you'll usually want to synchronize the selected tab with the URL from your client side router or the `window.location`. +Tabs that are links do not participate in selection, so onSelectionChange will not be called when a link is selected. ```tsx ; -export const TabsExample: TabsStory = () => { +export const TabsExample: TabsStory = (props) => { let [url, setUrl] = useState('/FoR'); return ( - + Founding of Rome Monarchy and Republic diff --git a/packages/react-aria-components/test/Tabs.test.js b/packages/react-aria-components/test/Tabs.test.js index 7982487d543..b9b6495ca24 100644 --- a/packages/react-aria-components/test/Tabs.test.js +++ b/packages/react-aria-components/test/Tabs.test.js @@ -480,7 +480,8 @@ describe('Tabs', () => { }); it('should support tabs as links', async function () { - let {getAllByRole} = render(); + let onSelectionChange = jest.fn(); + let {getAllByRole} = render(); let tabs = getAllByRole('tab'); expect(tabs[0].tagName).toBe('A'); @@ -496,6 +497,7 @@ describe('Tabs', () => { fireEvent.keyDown(tabs[1], {key: 'ArrowRight'}); expect(tabs[2]).toHaveAttribute('aria-selected', 'true'); + expect(onSelectionChange).toHaveBeenCalledTimes(0); }); it('should navigate linked tabs on click rather than mouse down', async function () { diff --git a/packages/react-aria/src/selection/useSelectableItem.ts b/packages/react-aria/src/selection/useSelectableItem.ts index bc90da7cb89..1608b6ef1e4 100644 --- a/packages/react-aria/src/selection/useSelectableItem.ts +++ b/packages/react-aria/src/selection/useSelectableItem.ts @@ -76,7 +76,12 @@ export interface SelectableItemOptions extends DOMProps { * - 'none': links are disabled for both selection and actions (e.g. handled elsewhere). * @default 'action' */ - linkBehavior?: 'action' | 'selection' | 'override' | 'none' + linkBehavior?: 'action' | 'selection' | 'override' | 'none', + /** + * Whether to skip resetting the selection when a link is selected. + * @default false + */ + shouldSkipResetSelection?: boolean } export interface SelectableItemStates { @@ -126,7 +131,8 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte isDisabled, onAction, allowsDifferentPressOrigin, - linkBehavior = 'action' + linkBehavior = 'action', + shouldSkipResetSelection = false } = options; let router = useRouter(); id = useId(id); @@ -142,8 +148,10 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte if (linkBehavior === 'selection' && ref.current) { let itemProps = manager.getItemProps(key); router.open(ref.current, e, itemProps.href, itemProps.routerOptions); - // Always set selected keys back to what they were so that select and combobox close. - manager.setSelectedKeys(manager.selectedKeys); + if (!shouldSkipResetSelection) { + // Always set selected keys back to what they were so that select and combobox close. + manager.setSelectedKeys(manager.selectedKeys); + } return; } else if (linkBehavior === 'override' || linkBehavior === 'none') { return; diff --git a/packages/react-aria/src/tabs/useTab.ts b/packages/react-aria/src/tabs/useTab.ts index f1e7ae5c3f2..77d011d9319 100644 --- a/packages/react-aria/src/tabs/useTab.ts +++ b/packages/react-aria/src/tabs/useTab.ts @@ -64,7 +64,9 @@ export function useTab( // This avoids reopening beforeunload dialogs when browsers replay // queued pointer enter/leave events after cancellation. shouldSelectOnPressUp: shouldSelectOnPressUp ?? item?.props.href != null, - linkBehavior: 'selection' + linkBehavior: 'selection', + // Tabs that are links do not participate in selection, so onSelectionChange will not be called. + shouldSkipResetSelection: true }); let tabId = generateId(state, key, 'tab');