diff --git a/packages/react/src/number-field/decrement/NumberFieldDecrement.test.tsx b/packages/react/src/number-field/decrement/NumberFieldDecrement.test.tsx
index e246cbdda1..ce9209c563 100644
--- a/packages/react/src/number-field/decrement/NumberFieldDecrement.test.tsx
+++ b/packages/react/src/number-field/decrement/NumberFieldDecrement.test.tsx
@@ -287,4 +287,32 @@ describe('', () => {
expect(input).to.have.value('99');
});
+
+ it('always decrements on quick touch (touchend that occurs before TOUCH_TIMEOUT)', async () => {
+ await render(
+
+
+
+ ,
+ );
+
+ const button = screen.getByRole('button');
+ const input = screen.getByRole('textbox');
+
+ fireEvent.touchStart(button);
+ fireEvent.mouseEnter(button);
+ fireEvent.pointerDown(button, { pointerType: 'touch' });
+ fireEvent.touchEnd(button);
+ fireEvent.click(button, { detail: 1 });
+
+ expect(input).to.have.value('-1');
+
+ fireEvent.touchStart(button);
+ // No mouseenter occurs after the first focus
+ fireEvent.pointerDown(button, { pointerType: 'touch' });
+ fireEvent.touchEnd(button);
+ fireEvent.click(button, { detail: 1 });
+
+ expect(input).to.have.value('-2');
+ });
});
diff --git a/packages/react/src/number-field/increment/NumberFieldIncrement.test.tsx b/packages/react/src/number-field/increment/NumberFieldIncrement.test.tsx
index a322f2c575..3152466e2c 100644
--- a/packages/react/src/number-field/increment/NumberFieldIncrement.test.tsx
+++ b/packages/react/src/number-field/increment/NumberFieldIncrement.test.tsx
@@ -287,4 +287,32 @@ describe('', () => {
expect(input).to.have.value('101');
});
+
+ it('always increments on quick touch (touchend that occurs before TOUCH_TIMEOUT)', async () => {
+ await render(
+
+
+
+ ,
+ );
+
+ const button = screen.getByRole('button');
+ const input = screen.getByRole('textbox');
+
+ fireEvent.touchStart(button);
+ fireEvent.mouseEnter(button);
+ fireEvent.pointerDown(button, { pointerType: 'touch' });
+ fireEvent.click(button, { detail: 1 });
+ fireEvent.touchEnd(button);
+
+ expect(input).to.have.value('1');
+
+ fireEvent.touchStart(button);
+ // No mouseenter occurs after the first focus
+ fireEvent.pointerDown(button, { pointerType: 'touch' });
+ fireEvent.click(button, { detail: 1 });
+ fireEvent.touchEnd(button);
+
+ expect(input).to.have.value('2');
+ });
});
diff --git a/packages/react/src/number-field/root/useNumberFieldRoot.ts b/packages/react/src/number-field/root/useNumberFieldRoot.ts
index 00f9780049..98eef85a07 100644
--- a/packages/react/src/number-field/root/useNumberFieldRoot.ts
+++ b/packages/react/src/number-field/root/useNumberFieldRoot.ts
@@ -127,6 +127,8 @@ export function useNumberFieldRoot(
const unsubscribeFromGlobalContextMenuRef = React.useRef<() => void>(() => {});
const isTouchingButtonRef = React.useRef(false);
const hasTouchedInputRef = React.useRef(false);
+ const ignoreClickRef = React.useRef(false);
+ const pointerTypeRef = React.useRef<'mouse' | 'touch' | 'pen' | ''>('');
useEnhancedEffect(() => {
if (validityData.initialValue === null && value !== validityData.initialValue) {
@@ -431,7 +433,7 @@ export function useNumberFieldRoot(
event.defaultPrevented ||
isDisabled ||
// If it's not a keyboard/virtual click, ignore.
- event.detail !== 0
+ (pointerTypeRef.current === 'touch' ? ignoreClickRef.current : event.detail !== 0)
) {
return;
}
@@ -449,6 +451,8 @@ export function useNumberFieldRoot(
return;
}
+ pointerTypeRef.current = event.pointerType;
+ ignoreClickRef.current = false;
isPressedRef.current = true;
incrementDownCoordsRef.current = { x: event.clientX, y: event.clientY };
@@ -466,6 +470,7 @@ export function useNumberFieldRoot(
const moves = movesAfterTouchRef.current;
movesAfterTouchRef.current = 0;
if (moves < MAX_POINTER_MOVES_AFTER_TOUCH) {
+ ignoreClickRef.current = true;
startAutoChange(isIncrement);
} else {
stopAutoChange();