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'); + }); + }); });