Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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'
};

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

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
}

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