diff --git a/packages/@react-aria/interactions/src/usePress.ts b/packages/@react-aria/interactions/src/usePress.ts
index b24a383df12..a77e00b63e9 100644
--- a/packages/@react-aria/interactions/src/usePress.ts
+++ b/packages/@react-aria/interactions/src/usePress.ts
@@ -200,6 +200,10 @@ export function usePress(props: PressHookProps): PressResult {
pointerType: null,
disposables: []
});
+ let isDisabledRef = useRef(isDisabled);
+ useEffect(() => {
+ isDisabledRef.current = isDisabled;
+ }, [isDisabled]);
let {addGlobalListener, removeAllGlobalListeners} = useGlobalListeners();
@@ -577,7 +581,7 @@ export function usePress(props: PressHookProps): PressResult {
let clicked = false;
let timeout = setTimeout(() => {
if (state.isPressed && state.target instanceof HTMLElement) {
- if (clicked) {
+ if (clicked || (state.isPressed && isDisabledRef.current)) {
// eslint-disable-next-line react-hooks/rules-of-hooks
cancelEvent(e);
} else {
@@ -708,6 +712,11 @@ export function usePress(props: PressHookProps): PressResult {
if (state.target && nodeContains(state.target, getEventTarget(e) as Element) && state.pointerType != null) {
// Wait for onClick to fire onPress. This avoids browser issues when the DOM
// is mutated between onMouseUp and onClick, and is more compatible with third party libraries.
+ if (state.isPressed && isDisabledRef.current) {
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ cancelEvent(e);
+ return;
+ }
} else {
// eslint-disable-next-line react-hooks/rules-of-hooks
cancelEvent(e);
diff --git a/packages/react-aria-components/stories/NumberField.stories.tsx b/packages/react-aria-components/stories/NumberField.stories.tsx
index 1139db3641b..d9e413e3a6f 100644
--- a/packages/react-aria-components/stories/NumberField.stories.tsx
+++ b/packages/react-aria-components/stories/NumberField.stories.tsx
@@ -12,6 +12,8 @@
import {Button, FieldError, Group, I18nProvider, Input, Label, NumberField, NumberFieldProps} from 'react-aria-components';
import {Meta, StoryObj} from '@storybook/react';
+import Minus from 'lucide-react/dist/esm/icons/minus';
+import Plus from 'lucide-react/dist/esm/icons/plus';
import React, {useState} from 'react';
import './styles.css';
diff --git a/packages/react-aria-components/test/NumberField.test.js b/packages/react-aria-components/test/NumberField.test.js
index 1da1aafcc0a..42f21bea8ad 100644
--- a/packages/react-aria-components/test/NumberField.test.js
+++ b/packages/react-aria-components/test/NumberField.test.js
@@ -11,10 +11,10 @@
*/
jest.mock('@react-aria/live-announcer');
-import {act, pointerMap, render} from '@react-spectrum/test-utils-internal';
+import {act, fireEvent, pointerMap, render} from '@react-spectrum/test-utils-internal';
import {announce} from '@react-aria/live-announcer';
import {Button, FieldError, Form, Group, I18nProvider, Input, Label, NumberField, NumberFieldContext, Text} from '../';
-import React from 'react';
+import React, { useState } from 'react';
import userEvent from '@testing-library/user-event';
let TestNumberField = (props) => (
@@ -488,4 +488,45 @@ describe('NumberField', () => {
expect(input.validity.valid).toBe(true);
expect(input).not.toHaveAttribute('aria-describedby');
});
+
+ describe('auto spinning', () => {
+ beforeAll(() => {
+ jest.useFakeTimers();
+ });
+ afterEach(() => {
+ act(() => {jest.runAllTimers();});
+ });
+
+ it.only('stops spinning if the associated button is disabled', async () => {
+ function NumberFieldDisabledButtons({label}) {
+ const [value, setValue] = useState(4);
+
+ return (
+
+
+
+
+
+
+
+
+ );
+ }
+ let {getByRole} = render();
+ let input = getByRole('textbox');
+ let decrementButton = getByRole('button', {name: 'Decrease'});
+ let incrementButton = getByRole('button', {name: 'Increase'});
+ await user.click(incrementButton);
+ // manually fire these events because user.click will refuse to fire the up event if the button is disabled
+ fireEvent.mouseDown(decrementButton, {button: 0});
+ fireEvent.mouseUp(decrementButton, {button: 0});
+ await act(async () => jest.runAllTimers());
+ expect(decrementButton).toBeDisabled();
+ expect(input).toHaveValue('4');
+ });
+ });
});