diff --git a/packages/react/src/tabs/root/TabsRoot.test.tsx b/packages/react/src/tabs/root/TabsRoot.test.tsx
index 2233008e34..cc902c7472 100644
--- a/packages/react/src/tabs/root/TabsRoot.test.tsx
+++ b/packages/react/src/tabs/root/TabsRoot.test.tsx
@@ -168,6 +168,23 @@ describe('', () => {
});
describe('prop: onValueChange', () => {
+ it('should call onValueChange on pointerdown', async () => {
+ const handleChange = spy();
+ const handlePointerDown = spy();
+ const { getAllByRole, user } = await render(
+
+
+
+
+
+ ,
+ );
+
+ await user.pointer({ keys: '[MouseLeft>]', target: getAllByRole('tab')[1] });
+ expect(handleChange.callCount).to.equal(1);
+ expect(handlePointerDown.callCount).to.equal(1);
+ });
+
it('should call onValueChange when clicking', async () => {
const handleChange = spy();
const { getAllByRole } = await render(
@@ -184,6 +201,21 @@ describe('', () => {
expect(handleChange.firstCall.args[0]).to.equal(1);
});
+ it('should not call onValueChange on non-main 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(
diff --git a/packages/react/src/tabs/tab/useTabsTab.ts b/packages/react/src/tabs/tab/useTabsTab.ts
index 16e285d224..f138948379 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 isMainButtonRef = 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 && isMainButtonRef.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;
+ isMainButtonRef.current = false;
+ }
+
+ if (!event.button || event.button === 0) {
+ isMainButtonRef.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,
],