adobe / react-spectrum

A collection of libraries and tools that help you build adaptive, accessible, and robust user experiences.
https://react-spectrum.adobe.com
Apache License 2.0
12.62k stars 1.09k forks source link

Native validation doesn't dismiss errors as the user corrects them #6983

Open sbking opened 2 weeks ago

sbking commented 2 weeks ago

Provide a general summary of the issue here

React ARIA's "native" validation doesn't work like HTML5 native validation works in most browsers, which results in a significantly worse user experience.

🤔 Expected Behavior?

In most modern browsers, when an HTML5 validation error is already showing, the validation error will update in real time until the user has fixed it, and then the error will immediately go away without the user blurring the field.

😯 Current Behavior

When validationBehavior is set to "native" and a validation error is showing, the error will never go away until the user blurs the field. The error message also won't update if the native validation issue changes until the user blurs the field.

💁 Possible Solution

I think the useFormValidation component should have two modes:

🔦 Context

No response

🖥️ Steps to Reproduce

https://codesandbox.io/s/quirky-meitner-jl79fh?file=/src/App.js

Version

latest

What browsers are you seeing the problem on?

Firefox, Chrome, Safari

If other, please specify.

No response

What operating system are you using?

macOS Sonoma 14.3

🧢 Your Company/Team

No response

🕷 Tracking Issue

No response

LFDanLu commented 2 weeks ago

The behavior of validating on blur for "native" validation is intentional since we thought it might be distracting to the user to display errors as they were entering their updated input value. Does implementing realtime validation like as shown here suffice instead?

sbking commented 2 weeks ago

@LFDanLu Not really, unfortunately - that would require me to manually control the lazy/eager validation behavior that users expect from native validation for every form field, and I'm not sure if I would be able to use <FormValidationContext.Provider /> anymore, making it all much more complicated.

The behavior of validating on blur for "native" validation is intentional since we thought it might be distracting to the user to display errors as they were entering their updated input value.

Did you happen to take a look at the CodeSandbox I provided? Try it out in a few browsers, react-aria's "native" mode just doesn't work like native HTML5 validation works in every modern browser I've tested it with. I think users do expect the error to go away when they've corrected it. They just don't expect an error to pop up initially while they're not finished typing their original input.

musjj commented 2 weeks ago

Yep, I tested the native form in both Firefox and Chrome. After an error, the field switches to real-time validation and updates the error as the user types. Would love to see the same behavior with react-aria-component's Forms.

devongovett commented 2 weeks ago

The goal isn't to match what browsers do by default - for that you don't even need React Aria at all, you can just use native inputs. Our goal is to improve the UX. Showing errors while the user is typing and has not completed their input yet is very distracting and can be misleading. An error might appear and disappear multiple times, for example while entering an email address, which could be very confusing for users. That's why by default we show errors only on "complete" input, which we determine from the blur event.

If you want different behavior, there is support for completely controlling the live error message using props, or using a library like react-hook-form.

musjj commented 2 weeks ago

The goal isn't to match what browsers do by default - for that you don't even need React Aria at all, you can just use native inputs

I'd agree that strictly matching the browser's behavior just for the sake of it isn't good. But I think the standard browser behavior in this case is just really nice.

To clarify, errors are only re-validated in real-time after a submit attempt that triggers a validation error (you can check the code sandbox above). When typing for the first time, no error messages will be displayed until the user submits the form.

An error might appear and disappear multiple times

With the native form behavior, the field stops re-validating in real time after valid input is typed in. So once the error disappears, it will not show up again until the user attempts to submit (or blur). IMO this behavior is super reasonable and very user-friendly.

sbking commented 2 weeks ago

Our goal is to improve the UX. Showing errors while the user is typing and has not completed their input yet is very distracting and can be misleading. An error might appear and disappear multiple times, for example while entering an email address, which could be very confusing for users. That's why by default we show errors only on "complete" input, which we determine from the blur event.

@devongovett I think you are misunderstanding the issue. I have no problem with waiting for a signal that the user has completed their input (e.g. a blur or submit event) to validate the input initially. That is good behavior and matches how HTML5 native validation works.

What I am saying is that when an error is already showing, the validation should switch to a real-time validation mode so that the user gets instant feedback when they fix the error. Once there is no more error, the input should switch back to deferred validation until the field is blurred again. That is how all modern browsers handle "native" validation and I really think it's a much better UX. Please try out the CodeSandbox I provided. It's a much worse user experience that this could ever happen:

Screenshot 2024-09-04 at 12 22 29 PM

In no "native" browser HTML5 validation will an error message show "you are currently using 1 character" while the user has dozens of characters in the input box. That would be misleading and a bad user experience, so why does it work that way in react-aria?

Note that the behavior I'm suggesting is also very similar to how the new CSS :user-invalid pseudo-class works in most browsers: https://developer.mozilla.org/en-US/docs/Web/CSS/:user-invalid

The goal isn't to match what browsers do by default - for that you don't even need React Aria at all, you can just use native inputs.

The UI library I use uses useTextBox from react-aria internally, and I would have to vendor the whole Input component in to do what you're suggesting. I want to continue to take advantage of errorMessageProps to show error messages with custom styling, but with native validation behavior.

If you want different behavior, there is support for completely controlling the live error message using props, or using a library like react-hook-form.

It looks like what I'm describing is in fact the default behavior of react-hook-form. Swapping out <FormValidationContext.Provider /> with react-hook-form is definitely possible, but I'm trying to use react-aria as much as possible, and the only issue I have with it is that it shows stale errors after the user has fixed them. Would you be opposed to a PR that introduces an "html5" mode that tries to work more like the actual native validation behavior and :user-invalid?

devongovett commented 1 week ago

Looks like all browsers implement this differently. Here's another example.

None of these are ideal in my opinion. I think Safari's behavior is the least annoying, but still misleading. Firefox's behavior would be better if the error message was less specific (e.g. not including the current character count). We can't control that though (the default error messages come from the browser).

I think I'd want to continue revalidating on blur rather than only on re-submit, and not updating the error message live. Not sure whether it's better to hide the previous error immediately like Safari and potentially re-display it on blur, or just wait until the user is done editing to update it like we do today.

sbking commented 1 week ago

@devongovett The common denominator between those three browser behaviors you described is that stale error messages are generally hidden when they are no longer relevant, so the user doesn't have to keep blurring and re-focusing the field to tell if they fixed the problem or not. That is the most important part of my issue. I don't really mind Safari's behavior of hiding the error even if a new one popped up.

I do think Firefox's behavior is the worst of the three, because it shows messages that are no longer true. But at least the message does go away if you fix the length error entirely. But I think the default behavior in React ARIA is pretty bad UX... I really think it's incorrect to continue showing "You are currently using 1 character" when they have 100 characters in the input and there's no longer a length issue.

In Chrome, the error message updates as you type after an error. If you type just "devon" into the email field and hit submit, you'll get an error: "Please include an @ in the email address". Then typing the "@" immediately changes the error to "Please enter a part following '@'". This is annoying because I'm not done typing yet.

I think it's much worse to continue showing "Please include an @ in the email address" when... it's already there. Again, no error message would be shown while the user is typing their initial input, and most users will generally type their full email into an email address field. I just want users to have an easy way to know that they fixed the error without having to keep blurring and re-focusing the field.

Could there be an optional dismissStaleErrors prop for validationBehavior="native"?

sbking commented 1 week ago

@devongovett FWIW, here are a few resources that describe this "Reward Early, Punish Late" pattern better than I do:

https://medium.com/wdstack/inline-validation-in-forms-designing-the-experience-123fb34088ce#7967

https://www.smashingmagazine.com/2022/09/inline-validation-web-forms-ux/#4-reward-early-punish-late

https://smart-interface-design-patterns.com/articles/inline-validation-ux/#2-reward-early-punish-late-pattern

https://x.com/BHolmesDev/status/1746911677440774274

You're completely right that this is more of a "implement good UX as a default" issue, rather than a "do exactly what the browser does natively" issue. Reward early, punish late is the default behavior/philosophy, in one form or another, for browser-native HTML5 validation as well as many other UI libraries like react-hook-form. I think there is generally wide support from UX designers that it's a good pattern. It's disappointing that react-aria, despite focusing on good UX, does not facilitate it even as an option without completely ejecting and managing the form events and controlled input states manually.

devongovett commented 1 week ago

I agree that showing outdated error messages is not ideal. It's just a question of when exactly to hide/update the error. As I showed above, there isn't a clear standard across browsers for this. Each option has pros and cons.

I could see potentially implementing Safari's behavior of hiding the error message as soon as the user edits the value, but as I said before it's still not ideal if the value is actually still invalid and the error re-appears on blur. It could also cause additional layout shifts while typing, and users might forget what the error was that they are supposed to fix.

If the error messages were less specific (e.g. "Please enter an email address" instead of "Please include an @ in the email address") then we could potentially wait until the input is valid to hide the message since it wouldn't change as you type, but we can't guarantee this since the messages depend on the browser.

Updating the error message live has a number of downsides:

Another thing to consider is what to do with errors that come from server side validation. For example, say you enter an email address in a signup form and submit, and then the server says there is already an account with that email. There's not really a way to know when to hide that error except by re-submitting the form. Perhaps that's another reason to go with Safari's behavior of hiding as soon as the value changes. We currently clear server errors on blur. Would be nice if errors from client side and server side validation behaved somewhat consistently.

sbking commented 1 week ago

If the error messages were less specific (e.g. "Please enter an email address" instead of "Please include an @ in the email address") then we could potentially wait until the input is valid to hide the message since it wouldn't change as you type, but we can't guarantee this since the messages depend on the browser.

Could it use the input.validity instance properties like typeMismatch etc instead of the input.validationMessage?

Another thing to consider is what to do with errors that come from server side validation. For example, say you enter an email address in a signup form and submit, and then the server says there is already an account with that email. There's not really a way to know when to hide that error except by re-submitting the form. Perhaps that's another reason to go with Safari's behavior of hiding as soon as the value changes. We currently clear server errors on blur. Would be nice if errors from client side and server side validation behaved somewhat consistently.

IMO a good default option would be to dismiss the server error as soon as the user changes the input, as that signals to the user that the error might be fixed (to the best of our knowledge). I believe that would still allow using the errorMessage prop to do real-time server (re)validation when needed, like a username input that checks if the username is available.

But I don't really think that's the best default behavior for client-side validations, even though that's how Safari handles it. As a user I'd prefer the error message to stay until there are no more errors, to the best of the app's knowledge - either way the layout will shift.

For screen reader users, hearing a potentially irrelevant error message after typing each character rather than the character you entered could be very confusing.

I think aria-live="polite" handles this for well-behaved screen readers, see this example: https://www.w3.org/WAI/tutorials/forms/notifications/#during-typing

"The value “polite” de-emphasizes the importance of the message and does not cause screen readers to interrupt their current tasks to read aloud this message. Thus the message is only read once when the user stops typing rather than on every keystroke that the user makes."