-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Input.error
prop, useValidateOnSubmit
hook
#1534
Conversation
ee9c7df
to
2e9049f
Compare
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## main #1534 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 61 63 +2
Lines 972 1001 +29
Branches 373 382 +9
=========================================
+ Hits 972 1001 +29 ☔ View full report in Codecov by Sentry. |
2e9049f
to
141b196
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good in general.
src/hooks/use-validation-error.ts
Outdated
if (ref.current) { | ||
ref.current.setCustomValidity(error ?? ''); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (ref.current) { | |
ref.current.setCustomValidity(error ?? ''); | |
} | |
ref.current?.setCustomValidity(error ?? ''); |
if (error) { | ||
feedback = 'error'; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will overwrite an explicitly set feedback when an error is set, which might not be super intuitive:
// This will render as `feedback="error"`.
<Input error="Review this and that" feedback="warning" />
That said, considering we intend to get rid of the feedback
prop, maybe we can just accept this trade-off in exchange for simplicity.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That said, considering we intend to get rid of the feedback prop, maybe we can just accept this trade-off in exchange for simplicity.
That's my plan.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Works for me then
src/hooks/use-validate-on-submit.ts
Outdated
const onSubmit = (event: Event) => { | ||
if (event.type !== 'submit') { | ||
throw new Error('Event type is not "submit"'); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would typing event
as SubmitEvent
help avoiding this check? If I'm not wrong, with that we would ensure the returned event handler is not allowed anywhere other than onSubmit
, as TypeScript will complain.
This is safer nonetheless.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good idea. I will do. I might keep the check in case there are any JS consumers in tests that misuse this somehow.
src/hooks/use-validate-on-submit.ts
Outdated
if ('validity' in el && !el.validity.valid) { | ||
if (!foundFirst) { | ||
el.focus(); | ||
foundFirst = true; | ||
} | ||
|
||
// If the user has focused an empty, required input field and | ||
// triggered a submission by pressing Enter, that will not trigger | ||
// a "change" event. Trigger this event to allow the input's "change" | ||
// handler to update its custom error. | ||
if (el.validity.valueMissing) { | ||
el.dispatchEvent(new Event('change')); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I'm getting this right, this if
statement does two things:
- Dispatch
change
event for every invalid element. - Focus the first invalid element.
I think all nested ifs make it a bit harder to follow due to all the conditions you have to mentally drag.
Perhaps a simple solution would be to invert the validity check condition and discard that early:
if ('validity' in el && !el.validity.valid) { | |
if (!foundFirst) { | |
el.focus(); | |
foundFirst = true; | |
} | |
// If the user has focused an empty, required input field and | |
// triggered a submission by pressing Enter, that will not trigger | |
// a "change" event. Trigger this event to allow the input's "change" | |
// handler to update its custom error. | |
if (el.validity.valueMissing) { | |
el.dispatchEvent(new Event('change')); | |
} | |
} | |
} | |
if (!('validity' in el) || el.validity.valid) { | |
continue; | |
} | |
if (!foundFirst) { | |
el.focus(); | |
foundFirst = true; | |
} | |
// If the user has focused an empty, required input field and | |
// triggered a submission by pressing Enter, that will not trigger | |
// a "change" event. Trigger this event to allow the input's "change" | |
// handler to update its custom error. | |
if (el.validity.valueMissing) { | |
el.dispatchEvent(new Event('change')); | |
} | |
} |
Then this translates into 3 individual conditions which are easier to mentally parse.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, this is better. Will do.
const onSelectURL = sinon.stub(); | ||
const form = mount(<CustomForm onSelectURL={onSelectURL} />); | ||
changeURL(form, 'not valid'); | ||
|
||
const input = form.find('input').getDOMNode(); | ||
const focusStub = sinon.stub(input, 'focus'); | ||
submitForm(form); | ||
|
||
assert.calledOnce(focusStub); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps you could attach the component to the DOM when mounting it (mount(..., { attachTo: container })
), and then check if document.activeElement
is equal to form.find('input').getDOMNode()
instead of stubbing the focus method. But I have to admit I have faced flaky tests when trying to do that, so I'm fine with this if the other approach is problematic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But I have to admit I have faced flaky tests when trying to do that, so I'm fine with this if the other approach is problematic.
Indeed that's the rationale here - a call to HTMLElement.focus
is much easier to check for as there is no asynchrony involved.
This sets a custom validation error for the input component which is communicated to the browser via `HTMLInputElement.setCustomValidity`. This, along with any native validation constraints such as `required`, will prevent form submission until they are resolved.
By default forms handle validation automatically when submitted, displaying an error message and focusing the first control with an error. This hook is useful if the caller wants to customize how validation error messages are presented on submission.
141b196
to
9c4d65e
Compare
This PR adds a couple of APIs designed to make handling custom validation errors and custom presentation of validation errors easier:
Input.error
prop specifies an error message that is synced to the underlying<input>
DOM element usingHTMLInputElement.setCustomValidity
. This in turn will prevent submission of any containing form which has validation checks enabled (the default).useValidateOnSubmit
hook can be used together with a form'snoValidate
property. This hook implements the parts of native validation that are still useful when validation errors are presented in a custom way: checking the validation status usingHTMLFormElement.checkValidity
and focusing the first control with an errorSee the tests for
useValidateOnSubmit
for a worked example of a simple form using both of these.I plan to use these new changes downstream to resolve hypothesis/product-backlog#1535. This PR only applies the change for
Input
components, but the same change should be applied toSelect
andTextarea
in future.