From 0fe40dcf4478493e6454e8fe408f223159e34041 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Fri, 10 Jan 2025 14:47:47 +0800 Subject: [PATCH 1/2] Add test --- packages/react/src/tabs/root/TabsRoot.test.tsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/react/src/tabs/root/TabsRoot.test.tsx b/packages/react/src/tabs/root/TabsRoot.test.tsx index 2233008e34..b1e88bf2d8 100644 --- a/packages/react/src/tabs/root/TabsRoot.test.tsx +++ b/packages/react/src/tabs/root/TabsRoot.test.tsx @@ -184,6 +184,21 @@ describe('', () => { expect(handleChange.firstCall.args[0]).to.equal(1); }); + it('should not call onValueChange on non-primary button clicks', async () => { + const handleChange = spy(); + const { getAllByRole } = await render( + + + + + + , + ); + + fireEvent.click(getAllByRole('tab')[1], { button: 2 }); + expect(handleChange.callCount).to.equal(0); + }); + it('should not call onValueChange when already selected', async () => { const handleChange = spy(); const { getAllByRole } = await render( From 504cda3ed2ecbba9353169ef117712fb2e024b7c Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Fri, 10 Jan 2025 23:36:03 +0800 Subject: [PATCH 2/2] Fix tab activating on focus triggered by non-primary button clicks --- packages/react/src/tabs/tab/useTabsTab.ts | 66 +++++++++++++++-------- 1 file changed, 45 insertions(+), 21 deletions(-) diff --git a/packages/react/src/tabs/tab/useTabsTab.ts b/packages/react/src/tabs/tab/useTabsTab.ts index 16e285d224..b4de1b86d3 100644 --- a/packages/react/src/tabs/tab/useTabsTab.ts +++ b/packages/react/src/tabs/tab/useTabsTab.ts @@ -1,6 +1,7 @@ 'use client'; import * as React from 'react'; import { mergeReactProps } from '../../utils/mergeReactProps'; +import { ownerDocument } from '../../utils/owner'; import { useEnhancedEffect } from '../../utils/useEnhancedEffect'; import { useForkRef } from '../../utils/useForkRef'; import { useBaseUiId } from '../../utils/useBaseUiId'; @@ -75,32 +76,56 @@ function useTabsTab(parameters: useTabsTab.Parameters): useTabsTab.ReturnValue { const tabPanelId = index > -1 ? getTabPanelIdByTabValueOrIndex(valueParam, index) : undefined; + const isPressingRef = React.useRef(false); + const isPrimaryButtonRef = React.useRef(false); + const getRootProps = React.useCallback( (externalProps = {}) => { return mergeReactProps<'button'>( externalProps, - mergeReactProps<'button'>( - { - role: 'tab', - 'aria-controls': tabPanelId, - 'aria-selected': selected, - id, - ref: handleRef, - onClick(event) { + { + role: 'tab', + 'aria-controls': tabPanelId, + 'aria-selected': selected, + id, + ref: handleRef, + onClick(event) { + if (selected) { + return; + } + + onTabActivation(tabValue, event.nativeEvent); + }, + onFocus(event) { + if (!activateOnFocus || selected) { + return; + } + + if (!isPressingRef.current || (isPressingRef.current && isPrimaryButtonRef.current)) { onTabActivation(tabValue, event.nativeEvent); - }, - onFocus(event) { - if (!activateOnFocus) { - return; - } - - if (selectedTabValue !== tabValue) { - onTabActivation(tabValue, event.nativeEvent); - } - }, + } + }, + onPointerDown(event) { + if (selected) { + return; + } + + isPressingRef.current = true; + + function handlePointerUp() { + isPressingRef.current = false; + isPrimaryButtonRef.current = false; + } + + if (!event.button || event.button === 0) { + isPrimaryButtonRef.current = true; + + const doc = ownerDocument(event.currentTarget); + doc.addEventListener('pointerup', handlePointerUp, { once: true }); + } }, - mergeReactProps(getItemProps(), getButtonProps()), - ), + }, + mergeReactProps(getItemProps(), getButtonProps()), ); }, [ @@ -111,7 +136,6 @@ function useTabsTab(parameters: useTabsTab.Parameters): useTabsTab.ReturnValue { id, onTabActivation, selected, - selectedTabValue, tabPanelId, tabValue, ],