Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,12 @@ Step3WithMin2Max21.story = {
name: 'step = 3 with min = 2, max = 21'
};

export const Step3WithMin2Max21ValueSnappingDisabled: NumberFieldStory = () => render({step: 3, minValue: 2, maxValue: 21, isValueSnappingDisabled: true});

Step3WithMin2Max21ValueSnappingDisabled.story = {
name: 'step = 3 with min = 2, max = 21, isValueSnappingDisabled = true'
};
Comment on lines +210 to +214
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export const InteractOutsideBehaviorNone: NumberFieldStory = () => render({step: 3, minValue: 2, maxValue: 21, commitBehavior: 'validate'});
InteractOutsideBehaviorNone.story = {
name: 'commitBehavior = validate'
};
export const CommitBehaviorValidate: NumberFieldStory = () => render({step: 3, minValue: 2, maxValue: 21, commitBehavior: 'validate'});
CommitBehaviorValidate.story = {
name: 'commitBehavior = validate'
};


export const AutoFocus: NumberFieldStory = () => render({autoFocus: true});

AutoFocus.story = {
Expand Down
69 changes: 69 additions & 0 deletions packages/@react-spectrum/numberfield/test/NumberField.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,30 @@ describe('NumberField', function () {
expect(container).not.toHaveAttribute('aria-invalid');
});

it.each`
Name
${'NumberField'}
`('$Name will allow typing of a number less than the min when value snapping is disabled', async () => {
let {
container,
textField
} = renderNumberField({onChange: onChangeSpy, minValue: 10, isValueSnappingDisabled: true});

expect(container).not.toHaveAttribute('aria-invalid');

act(() => {textField.focus();});
await user.clear(textField);
await user.keyboard('5');
expect(onChangeSpy).toHaveBeenCalledTimes(0);
expect(textField).toHaveAttribute('value', '5');
act(() => {textField.blur();});
expect(onChangeSpy).toHaveBeenCalledTimes(1);
expect(onChangeSpy).toHaveBeenCalledWith(5);
expect(textField).toHaveAttribute('value', '5');

expect(container).not.toHaveAttribute('aria-invalid');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe in the v3 spectrum component this should be false, but if we write the same test in the react aria component, does it have the invalid attribute? seems like it should

});

it.each`
Name
${'NumberField'}
Expand Down Expand Up @@ -383,6 +407,37 @@ describe('NumberField', function () {
expect(textField).toHaveAttribute('value', '1');
});

it.each`
Name
${'NumberField'}
`('$Name will allow typing of a number greater than the max when value snapping is disabled', async () => {
let {
container,
textField
} = renderNumberField({onChange: onChangeSpy, maxValue: 1, defaultValue: 0, isValueSnappingDisabled: true});

expect(container).not.toHaveAttribute('aria-invalid');

act(() => {textField.focus();});
await user.keyboard('2');
expect(onChangeSpy).not.toHaveBeenCalled();
act(() => {textField.blur();});
expect(onChangeSpy).toHaveBeenCalled();
expect(onChangeSpy).toHaveBeenCalledWith(2);
expect(textField).toHaveAttribute('value', '2');

expect(container).not.toHaveAttribute('aria-invalid');

onChangeSpy.mockReset();
act(() => {textField.focus();});
await user.keyboard('2');
expect(onChangeSpy).not.toHaveBeenCalled();
act(() => {textField.blur();});
expect(onChangeSpy).toHaveBeenCalled();
expect(onChangeSpy).toHaveBeenCalledWith(22);
expect(textField).toHaveAttribute('value', '22');
});

it.each`
Name
${'NumberField'}
Expand Down Expand Up @@ -772,6 +827,20 @@ describe('NumberField', function () {
expect(textField).toHaveAttribute('value', result);
});

it.each`
Name | value
${'NumberField down positive'} | ${'6'}
${'NumberField up positive'} | ${'8'}
${'NumberField down negative'} | ${'-8'}
${'NumberField up negative'} | ${'-6'}
`('$Name does not round to step on commit when value snapping is disabled', async ({value}) => {
let {textField} = renderNumberField({onChange: onChangeSpy, step: 5, isValueSnappingDisabled: true});
act(() => {textField.focus();});
await user.keyboard(value);
act(() => {textField.blur();});
expect(textField).toHaveAttribute('value', value);
});

it.each`
Name | value | result
${'NumberField down positive'} | ${'6'} | ${'5'}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ export function useNumberFieldState(
onChange,
locale,
isDisabled,
isReadOnly
isReadOnly,
isValueSnappingDisabled
} = props;

if (value === null) {
Expand Down Expand Up @@ -168,7 +169,9 @@ export function useNumberFieldState(

// Clamp to min and max, round to the nearest step, and round to specified number of digits
let clampedValue: number;
if (step === undefined || isNaN(step)) {
if (isValueSnappingDisabled) {
clampedValue = newParsedValue;
} else if (step === undefined || isNaN(step)) {
clampedValue = clamp(newParsedValue, minValue, maxValue);
} else {
clampedValue = snapValueToStep(newParsedValue, minValue, maxValue, step);
Expand Down
6 changes: 5 additions & 1 deletion packages/@react-types/numberfield/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@ export interface NumberFieldProps extends InputBase, Validation<number>, Focusab
* Formatting options for the value displayed in the number field.
* This also affects what characters are allowed to be typed by the user.
*/
formatOptions?: Intl.NumberFormatOptions
formatOptions?: Intl.NumberFormatOptions,
/**
* Disables value snapping when user finishes editing the value (e.g. on blur).
*/
isValueSnappingDisabled?: boolean
Copy link
Member

@snowystinger snowystinger Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

such a long prop name, how would we feel instead about, question to the team
isSnapDisabled

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that shorter name is fine

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perhaps interactOutside behavior related? like RangeCalendar? and DatePicker?
See #8899 (review) as well

}

export interface AriaNumberFieldProps extends NumberFieldProps, DOMProps, AriaLabelingProps, TextInputDOMEvents {
Expand Down
11 changes: 11 additions & 0 deletions packages/react-aria-components/test/NumberField.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -250,4 +250,15 @@ describe('NumberField', () => {
await user.keyboard('{Enter}');
expect(input).toHaveValue('200');
});

it('should not change the edited input value when value snapping is disabled', async () => {
let {getByRole} = render(<TestNumberField defaultValue={20} minValue={10} step={10} maxValue={50} isValueSnappingDisabled />);
let input = getByRole('textbox');
await user.tab();
await user.clear(input);
await user.keyboard('1024');
await user.tab();
expect(input).toHaveValue('1,024');
expect(announce).toHaveBeenLastCalledWith('1,024', 'assertive');
});
});