From b3ff30e32b3493bef426322bd34a5f34a8668fa9 Mon Sep 17 00:00:00 2001 From: Wojciech Maj Date: Mon, 11 May 2026 17:13:23 +0200 Subject: [PATCH 1/4] fix: fix optimize-locales-plugin not "tree shaking" react-aria package (#10023) Fixes #10022 --- packages/dev/optimize-locales-plugin/LocalesPlugin.js | 2 +- .../dev/parcel-resolver-optimize-locales/LocalesResolver.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/dev/optimize-locales-plugin/LocalesPlugin.js b/packages/dev/optimize-locales-plugin/LocalesPlugin.js index 67a6e0ab0ab..b9c907cec9e 100644 --- a/packages/dev/optimize-locales-plugin/LocalesPlugin.js +++ b/packages/dev/optimize-locales-plugin/LocalesPlugin.js @@ -18,7 +18,7 @@ module.exports = createUnplugin(({locales}) => { name: 'locales-plugin', resolveId(specifier, sourcePath, options) { if ( - !/[/\\](@react-stately|@react-aria|@react-spectrum|react-aria-components)[/\\]/.test( + !/[/\\](@react-stately|@react-aria|@react-spectrum|react-aria|react-aria-components)[/\\]/.test( sourcePath ) || options?.ssr diff --git a/packages/dev/parcel-resolver-optimize-locales/LocalesResolver.js b/packages/dev/parcel-resolver-optimize-locales/LocalesResolver.js index a025fd03844..f97f96f5109 100644 --- a/packages/dev/parcel-resolver-optimize-locales/LocalesResolver.js +++ b/packages/dev/parcel-resolver-optimize-locales/LocalesResolver.js @@ -23,7 +23,7 @@ module.exports = new Resolver({ resolve({specifier, dependency, config}) { if ( !Array.isArray(config) || - !/[/\\](@react-stately|@react-aria|@react-spectrum|react-aria-components)[/\\]/.test( + !/[/\\](@react-stately|@react-aria|@react-spectrum|react-aria|react-aria-components)[/\\]/.test( dependency.sourcePath ) ) { From 3a4618bfd48249921f611ac2ef7b63a4746f270a Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Tue, 12 May 2026 02:53:41 +1000 Subject: [PATCH 2/4] fix: SpinButton spins forever if the first change results in a disabled button (#9813) * fix: spin button continues to spin after buttons are disabled * remove extra story, it's covered by a test * Alternative implementation * fix lint * Update packages/react-aria-components/test/NumberField.test.js * move to a cancel call in an effect * simplify fix * fix lint --------- Co-authored-by: Daniel Lu --- .../test/NumberField.test.js | 47 ++++++++++++++++++- .../react-aria/src/interactions/usePress.ts | 12 +++++ .../src/spinbutton/useSpinButton.ts | 4 ++ 3 files changed, 61 insertions(+), 2 deletions(-) diff --git a/packages/react-aria-components/test/NumberField.test.js b/packages/react-aria-components/test/NumberField.test.js index 8880df30f1a..959e08cb62a 100644 --- a/packages/react-aria-components/test/NumberField.test.js +++ b/packages/react-aria-components/test/NumberField.test.js @@ -11,7 +11,7 @@ */ jest.mock('react-aria/src/live-announcer/LiveAnnouncer'); -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/private/live-announcer/LiveAnnouncer'; import {Button} from '../src/Button'; import {FieldError} from '../src/FieldError'; @@ -21,7 +21,7 @@ import {I18nProvider} from 'react-aria/I18nProvider'; import {Input} from '../src/Input'; import {Label} from '../src/Label'; import {NumberField, NumberFieldContext} from '../src/NumberField'; -import React from 'react'; +import React, {useState} from 'react'; import {Text} from '../src/Text'; import userEvent from '@testing-library/user-event'; @@ -583,4 +583,47 @@ 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('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'); + }); + }); }); diff --git a/packages/react-aria/src/interactions/usePress.ts b/packages/react-aria/src/interactions/usePress.ts index bdd9a9869b4..2c79632c076 100644 --- a/packages/react-aria/src/interactions/usePress.ts +++ b/packages/react-aria/src/interactions/usePress.ts @@ -331,6 +331,18 @@ export function usePress(props: PressHookProps): PressResult { ); let cancelEvent = useEffectEvent(cancel); + useEffect(() => { + if (isDisabled && ref.current.isPressed) { + cancelEvent({ + currentTarget: ref.current.target, + shiftKey: false, + ctrlKey: false, + metaKey: false, + altKey: false + }); + } + }, [isDisabled]); + let cancelOnPointerExit = useCallback( (e: EventBase) => { if (shouldCancelOnPointerExit) { diff --git a/packages/react-aria/src/spinbutton/useSpinButton.ts b/packages/react-aria/src/spinbutton/useSpinButton.ts index 61ced0099a4..32d06a93c8b 100644 --- a/packages/react-aria/src/spinbutton/useSpinButton.ts +++ b/packages/react-aria/src/spinbutton/useSpinButton.ts @@ -219,6 +219,8 @@ export function useSpinButton(props: SpinButtonProps): SpinbuttonAria { onIncrementPressStartEvent(600); } else if (isIncrementPressed) { onIncrementPressStartEvent(400); + } else if (!isIncrementPressed) { + clearAsyncEvent(); } }, [isIncrementPressed]); @@ -228,6 +230,8 @@ export function useSpinButton(props: SpinButtonProps): SpinbuttonAria { onDecrementPressStartEvent(600); } else if (isDecrementPressed) { onDecrementPressStartEvent(400); + } else if (!isDecrementPressed) { + clearAsyncEvent(); } }, [isDecrementPressed]); From bbbbaf6ac0e978dcccb80274468d77c0e51772e1 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Mon, 11 May 2026 07:18:40 -1000 Subject: [PATCH 3/4] feat: Add SliderFill component and improve default SliderOutput formatting (#10021) * Add SliderFill component * add tests * fix color slider * Fix ColorSlider formatting --- .../@react-spectrum/s2/src/RangeSlider.tsx | 19 +--- packages/@react-spectrum/s2/src/Slider.tsx | 71 ++++-------- .../dev/s2-docs/pages/react-aria/Slider.mdx | 17 ++- .../react-aria-components/exports/Slider.ts | 6 +- .../react-aria-components/exports/index.ts | 4 + packages/react-aria-components/src/Slider.tsx | 86 +++++++++++++- .../react-aria-components/test/Slider.test.js | 105 ++++++++++++++++-- .../src/color/useColorSliderState.ts | 8 +- .../src/slider/useSliderState.ts | 35 +++++- starters/docs/src/Slider.css | 48 ++++---- starters/docs/src/Slider.tsx | 34 ++---- starters/tailwind/src/Slider.tsx | 45 +++----- 12 files changed, 314 insertions(+), 164 deletions(-) diff --git a/packages/@react-spectrum/s2/src/RangeSlider.tsx b/packages/@react-spectrum/s2/src/RangeSlider.tsx index af06d58df34..dadb9c31865 100644 --- a/packages/@react-spectrum/s2/src/RangeSlider.tsx +++ b/packages/@react-spectrum/s2/src/RangeSlider.tsx @@ -11,7 +11,6 @@ */ import {ContextValue} from 'react-aria-components/slots'; - import {createContext, forwardRef, useContext, useRef} from 'react'; import { filledTrack, @@ -25,12 +24,11 @@ import { } from './Slider'; import {FocusableRef, FocusableRefValue, RangeValue} from '@react-types/shared'; import {FormContext, useFormProps} from './Form'; -import intlMessages from '../intl/*.json'; // @ts-ignore +import intlMessages from '../intl/*.json'; import {pressScale} from './pressScale'; -import {SliderThumb, SliderTrack} from 'react-aria-components/Slider'; +import {SliderFill, SliderThumb, SliderTrack} from 'react-aria-components/Slider'; import {useFocusableRef} from './useDOMRef'; -import {useLocale} from 'react-aria/I18nProvider'; import {useLocalizedStringFormatter} from 'react-aria/useLocalizedStringFormatter'; import {useSpectrumContextProps} from './useSpectrumContextProps'; @@ -77,8 +75,6 @@ export const RangeSlider = /*#__PURE__*/ forwardRef(function RangeSlider( let inputRef = useRef(null); // TODO: need to pass inputRef to SliderThumb when we release the next version of RAC 1.3.0 let domRef = useFocusableRef(ref, inputRef); - let {direction} = useLocale(); - let cssDirection = direction === 'rtl' ? 'right' : 'left'; let defaultThumbValues: number[] | undefined = undefined; if (props.defaultValue != null) { defaultThumbValues = [props.defaultValue.start, props.defaultValue.end]; @@ -98,14 +94,9 @@ export const RangeSlider = /*#__PURE__*/ forwardRef(function RangeSlider( {({state, isDisabled}) => ( <> -
-
+
+ +
@@ -318,9 +316,10 @@ export let upperTrack = style<{ translateY: '-50%', width: 'full', boxSizing: 'border-box', - borderStyle: 'solid', - borderWidth: '[.5px]', - borderColor: { + outlineStyle: 'solid', + outlineWidth: '[.5px]', + outlineOffset: -0.5, + outlineColor: { default: 'transparent', forcedColors: { default: 'ButtonText', @@ -369,10 +368,8 @@ export function SliderBase( labelAlign = 'start', size = 'M', minValue = 0, - maxValue = 100, - formatOptions + maxValue = 100 } = props; - let formatter = useNumberFormatter(formatOptions); let {direction} = useLocale(); return ( @@ -387,26 +384,19 @@ export function SliderBase( ) }> {({state}) => { - let maxLabelLength = Math.max( - [...formatter.format(minValue)].length, - [...formatter.format(maxValue)].length - ); + let maxLabelLength = 0; switch (state.values.length) { case 1: + maxLabelLength = Math.max( + [...state.getFormattedValue(minValue)].length, + [...state.getFormattedValue(maxValue)].length + ); break; case 2: - // This should really use the NumberFormat#formatRange proposal... - // https://github.com/tc39/ecma402/issues/393 - // https://github.com/tc39/proposal-intl-numberformat-v3#formatrange-ecma-402-393 - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/formatRange - maxLabelLength = - 3 + - 2 * - Math.max( - maxLabelLength, - [...formatter.format(minValue)].length, - [...formatter.format(maxValue)].length - ); + maxLabelLength = Math.max( + [...state.getFormattedValue([minValue, minValue + (props.step || 1)])].length, + [...state.getFormattedValue([maxValue - (props.step || 1), maxValue])].length + ); break; default: throw new Error('Only sliders with 1 or 2 handles are supported!'); @@ -419,9 +409,8 @@ export function SliderBase( minWidth: `${maxLabelLength}ch`, fontVariantNumeric: 'tabular-nums' }} - className={output({direction, labelPosition, isInForm: !!formContext})}> - {({state}) => state.values.map((_, i) => state.getThumbValueLabel(i)).join(' – ')} - + className={output({direction, labelPosition, isInForm: !!formContext})} + /> ); return ( @@ -475,32 +464,20 @@ export const Slider = /*#__PURE__*/ forwardRef(function Slider( let thumbRef = useRef(null); let inputRef = useRef(null); // TODO: need to pass inputRef to SliderThumb when we release the next version of RAC 1.3.0 let domRef = useFocusableRef(ref, inputRef); - let {direction} = useLocale(); - let cssDirection = direction === 'rtl' ? 'right' : 'left'; let isStaticColor = props['PRIVATE_staticColor']; return ( - {({state, isDisabled}) => { - fillOffset = - fillOffset !== undefined - ? clamp(fillOffset, state.getThumbMinValue(0), state.getThumbMaxValue(0)) - : state.getThumbMinValue(0); - - let fillWidth = state.getThumbPercent(0) - state.getValuePercent(fillOffset); - let isRightOfOffset = fillWidth > 0; - let offset = isRightOfOffset - ? state.getValuePercent(fillOffset) - : state.getThumbPercent(0); - + {({isDisabled}) => { return ( <> -
-
+
+ +
## Examples @@ -105,11 +107,12 @@ By default, slider values are percentages between 0 and 100. Use the `minValue`, -```tsx links={{Slider: '#slider', SliderOutput: '#slideroutput', SliderTrack: '#slidertrack', SliderThumb: '#sliderthumb'}} +```tsx links={{Slider: '#slider', SliderOutput: '#slideroutput', SliderTrack: '#slidertrack', SliderFill: '#sliderfill', SliderThumb: '#sliderthumb'}}