Skip to content
20 changes: 13 additions & 7 deletions packages/@adobe/react-spectrum/test/color/ColorField.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,16 @@ describe('ColorField', function () {
});

it('should allow placeholder and show warning', function () {
using spyWarn = jest.spyOn(console, 'warn').mockImplementation(() => {});
let {getByPlaceholderText, getByRole} = renderComponent({placeholder: 'Enter a color'});
expect(getByRole('textbox')).toBe(getByPlaceholderText('Enter a color'));
expect(spyWarn).toHaveBeenCalledWith(
'Placeholders are deprecated due to accessibility issues. Please use help text instead. See the docs for details: https://react-spectrum.adobe.com/react-spectrum/ColorField.html#help-text'
);
let spyWarn = jest.spyOn(console, 'warn').mockImplementation(() => {});
try {
let {getByPlaceholderText, getByRole} = renderComponent({placeholder: 'Enter a color'});
expect(getByRole('textbox')).toBe(getByPlaceholderText('Enter a color'));
expect(spyWarn).toHaveBeenCalledWith(
'Placeholders are deprecated due to accessibility issues. Please use help text instead. See the docs for details: https://react-spectrum.adobe.com/react-spectrum/ColorField.html#help-text'
);
} finally {
spyWarn.mockRestore();
}
});

it('should show valid validation state', function () {
Expand Down Expand Up @@ -535,7 +539,9 @@ describe('ColorField', function () {
expect(input).toHaveValue('0');

let button = getByTestId('submit');
await user.click(button);
await act(async () => {
await user.click(button);
});
expect(input).toHaveValue('255');
});
}
Expand Down
42 changes: 42 additions & 0 deletions packages/react-aria-components/test/NumberField.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,48 @@ describe('NumberField', () => {
expect(numberfield).not.toHaveAttribute('data-invalid');
});

it('should clear validation errors when a controlled value is updated externally', async () => {
function ControlledNumberField() {
let [value, setValue] = useState(1);

return (
<form data-testid="form">
<NumberField
value={value}
onChange={setValue}
validationBehavior="native"
isRequired
validate={v => (v % 2 ? 'Odd values are invalid' : null)}>
<Label>Value</Label>
<Group>
<Button slot="decrement">-</Button>
<Input />
<Button slot="increment">+</Button>
</Group>
<FieldError />
</NumberField>
<Button onPress={() => setValue(10)}>Set to 10</Button>
</form>
);
}

let {getByRole, getByTestId} = render(<ControlledNumberField />);
let input = getByRole('textbox');

act(() => {
getByTestId('form').checkValidity();
});

let describedBy = input.getAttribute('aria-describedby');
expect(describedBy).toBeTruthy();
expect(document.getElementById(describedBy)).toHaveTextContent('Odd values are invalid');

await user.click(getByRole('button', {name: 'Set to 10'}));

expect(input).not.toHaveAttribute('aria-describedby');
expect(input).not.toHaveAttribute('aria-invalid');
});

it('supports pasting value in another numbering system', async () => {
let {getByRole, rerender} = render(<TestNumberField />);
let input = getByRole('textbox');
Expand Down
10 changes: 9 additions & 1 deletion packages/react-stately/src/numberfield/useNumberFieldState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
} from '@react-types/shared';
import {FormValidationState, useFormValidationState} from '../form/useFormValidationState';
import {NumberFormatter, NumberParser} from '@internationalized/number';
import {useCallback, useMemo, useState} from 'react';
import {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {useControlledState} from '../utils/useControlledState';

export interface NumberFieldProps
Expand Down Expand Up @@ -185,6 +185,14 @@ export function useNumberFieldState(props: NumberFieldStateOptions): NumberField
value: numberValue
});

let prevControlledValue = useRef(value);
useEffect(() => {
if (value !== undefined && !Object.is(value, prevControlledValue.current)) {
validation.commitValidation();
}
prevControlledValue.current = value;
}, [value]);

let clampStep = step !== undefined && !isNaN(step) ? step : 1;
if (intlOptions.style === 'percent' && (step === undefined || isNaN(step))) {
clampStep = 0.01;
Expand Down