Skip to content

fix: #8659 Number Field Input Validation Update - #10362

Open
jsmitrah wants to merge 22 commits into
adobe:mainfrom
jsmitrah:fix/8659/numberfield-controlled-input-validation-update
Open

fix: #8659 Number Field Input Validation Update#10362
jsmitrah wants to merge 22 commits into
adobe:mainfrom
jsmitrah:fix/8659/numberfield-controlled-input-validation-update

Conversation

@jsmitrah

@jsmitrah jsmitrah commented Jul 24, 2026

Copy link
Copy Markdown

Closes #8659

✅ Pull Request Checklist:

  • Included link to corresponding React Spectrum GitHub Issue.
  • Added/updated unit tests and storybook for this change (for new code or code which already has tests).
  • Filled out test instructions.
  • Updated documentation (if it already exists for this component).
  • Looked at the Accessibility Practices for this feature - Aria Practices

📝 Test Instructions:

  • Submit the form with an empty input and verify the validation error is displayed.
  • Click "Set to XXX" and verify the validation message is cleared automatically.
  • Confirm the form can now be submitted successfully without requiring manual input changes.

🧢 Your Project:


let button = getByTestId('submit');
await user.click(button);
await act(async () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

what's this extra act for? user event uses fireEvent which already wraps everything inside an act

Also, why are there changes in this file? using should be the correct way to use a spy.

If there's something that isn't being awaited, likely it's something leaking from an earlier test into this one.

Whatever it is though, it doesn't give me a lot of confidence in this approach, especially without any explanation.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This was from an earlier attempt to fix CI failures caused by validation updates not being fully awaited, specifically triggering “state update outside of act” warnings.

Since userEvent already wraps in act, I initially removed the extra wrapper, but CI still surfaced warnings, so I added an explicit act() wrapper around the update to ensure stability.

I also reverted the unrelated changes here and aligned it with the existing patterns (using for spies).

value: numberValue
});

let prevControlledValue = useRef(value);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

how does this fix work? what is the root cause of the problem? what other approaches were considered?

This comment #8659 (comment) made it sound like this was a bigger issue than just NumberField, did you consider the root of all of them for a more holistic approach?

@jsmitrah jsmitrah Aug 13, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thanks for reviewing! After digging deeper into #8659, I realized this is a broader issue across input types with two related root causes:

1. Stale validation on controlled changes
When a value updates externally (e.g. via a controlled prop or button), no native input/change event fires, so validation isn’t re-committed. Existing errors can remain until a blur or another commit-triggering event occurs.

2. Missing validation for programmatic constraint violations
For constraints like maxLength and minLength, the native DOM does not update its validity state when values are set programmatically (e.g. validity.tooLong remains false). This means invalid values may not surface an error or block submission.

Approaches considered:
I initially explored detecting external updates using DOM-based checks (e.g. focus detection), but this proved unreliable for composite components like DatePicker and TimeField, where internal focus changes were incorrectly treated as external updates.

Updated approach:
Based on that, I reverted the earlier useNumberFieldState changes and moved the solution into the shared validation layer:

  • Added an optional isFocusWithin prop so composite components can provide accurate focus state
  • Re-commit validation when values change externally to prevent stale validation state
  • Ensure constraint validation reflects programmatic updates so form behavior stays consistent

I’ve pushed these updates along with the corresponding tests.


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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This comment (i'm not sure why it's hidden as outdated) made it sound possibly related to focus, but i don't see anything about focus here
#8659 (comment)

what did you discover when you looked into this?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

It’s not directly about focus itself.
From what I found, the issue is that validation isn’t re-committed when values change programmatically. Focus only matters in distinguishing external updates (when the field is unfocused), which is now handled in the shared validation logic as mentioned above.

@snowystinger snowystinger added the waiting Waiting on Issue Author label Aug 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

waiting Waiting on Issue Author

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Controlled input validation is not updated after an external change

3 participants