-
Notifications
You must be signed in to change notification settings - Fork 7
Add setCustomValidity()
and setValidity()
to form elements
#443
Conversation
🦋 Changeset detectedLatest commit: 78f2edd The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
b3e178b
to
0c54f60
Compare
0c54f60
to
43e8c43
Compare
```html | ||
<glide-core-input label="Label" pattern="[a-z]{4,8}"></glide-core-input> | ||
``` | ||
|
||
```js | ||
// Returns `false`. | ||
document.querySelector('glide-core-input').checkValidity(); | ||
``` |
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.
These snippets could be removed, as folks should know how to use the native APIs, but I don't think they necessarily hurt anything. I also wanted to call it out, because we do deviate from native's behavior a bit here.
Having small snippets like this will make my job a little easier when talking about the changes with other groups.
```js | ||
const input = document.querySelector('glide-core-input'); | ||
|
||
// `setCustomValidity()` sets the validity message on the element | ||
// and places the element in an invalid state. | ||
input.setCustomValidity( | ||
'Please enter a name that is greater than 1 character.', | ||
); | ||
|
||
// The element is now marked as invalid. | ||
// Returns `false`. | ||
input.checkValidity(); | ||
|
||
// Displays the validity message to the user. | ||
// Returns `false`. | ||
input.reportValidity(); | ||
|
||
// Like native, provide an empty string to change | ||
// the validity state to valid. | ||
input.setCustomValidity(''); | ||
``` |
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.
Another case where this shouldn't be needed, but it'll make my life a little easier when discussing the changes externally. Better in the release note than Storybook, I think.
disabled: { | ||
table: { | ||
defaultValue: { summary: 'false' }, | ||
type: { summary: 'boolean' }, | ||
}, | ||
}, | ||
'hide-label': { | ||
table: { | ||
defaultValue: { summary: 'false' }, | ||
type: { summary: 'boolean' }, | ||
}, | ||
}, | ||
name: { | ||
table: { | ||
defaultValue: { summary: '""' }, | ||
type: { summary: 'string' }, | ||
}, | ||
}, |
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.
Reordered these to align with the order above so they match.
|
||
await elementUpdated(component); | ||
|
||
// Like native, the validity message shouldn't display until `reportValidity()` is called. |
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.
Folks should know this if they use the platform, but it may be confusing to some, so I called it out - is this helpful or not?
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.
I like it
this.#isShowValidationFeedback && this.validityMessage, | ||
() => | ||
html`<span class="validity-message" data-test="validity-message" | ||
>${unsafeHTML(this.validityMessage)}</span |
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.
We have use cases where links are in the error message. Using unsafeHTML
allows users to provide markup when required.
$0.setCustomValidity('<span>Here is a <a href="#">Link</a></span>');
I'm only aware of one use case at the moment where a link in the markup is needed - so the regular, non-HTML string path is the most likely used.
$0.setCustomValidity('validity message');
Native's setValidity()
and setCustomValidity()
both accept string arguments for the content. I'm trading being closer to native over using another slot and then breaking away from native APIs. I think it would be confusing. To me, the tradeoff is worth it.
We also won't do any sanitization on this content ourselves. Consumers should do it at their level rather than here.
}); | ||
|
||
it('is valid when filled in, readonly, and value exceeds `maxlength`', async () => { | ||
it('is valid when `value` is empty and `required` is updated to `false` programmatically', async () => { |
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.
... here!
if ( | ||
!this.required && | ||
this.#internals.validity.valueMissing && | ||
!this.value | ||
) { |
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 is the bug fix for programmatically removing required
and resetting the validity state to valid
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.
Radio Group has some issues with value
and checked
when programmatic changes happen. I'll be working on these right after this merges.
}); | ||
|
||
it('is valid when filled in, disabled, and value exceeds `maxlength`', async () => { | ||
it('blurs the textarea and reports validity if `blur` is called', async () => { |
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.
Similar to input - diff is a lot due to the removal of tests. These are existing tests... until...
.to.be.true; | ||
}); | ||
|
||
it('sets the validity message with `setCustomValidity()`', async () => { |
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.
here! (much shorter)
|
||
await elementUpdated(component); | ||
|
||
// Like native, the validity message shouldn't display until `reportValidity()` is called. |
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.
I like it
src/dropdown.stories.ts
Outdated
type: { | ||
summary: 'method', | ||
detail: ` | ||
// Sets a custom validity message, similar to the native "setCustomValidity" method. |
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.
I think this sets a new precedent. It's different than methods like reportValidity
and other native methods and properties in that it refers to native and explains how native works. Not saying that's bad or good—just that it's inconsistent with the rest of the story
Also, is message
actually optional as it says below?
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.
Yeah, good point. This mostly follows native, except of course for the optional markup it can receive. But as mentioned above, at least for the time being, this is an edge case (but a known one we need to support!). I'd be in favor of following our rule of "only document what native doesn't already do, or when we deviate from native". In that case, I can remove this documentation across all components to not set a new precedent.
I am working on a form example that I'll pass along to folks. It could also go up a layer in our Design System if we'd like.
Also, is message actually optional as it says below?
I'll double check this in the next commit 🫡
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.
In that case, I can remove this documentation across all components to not set a new precedent.
Sweet. My thought exactly.
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.
Sweet, updated with 78f2edd
🚀 Description
This PR is massive, but it's mostly rinse and repeat / copy paste across all of the form components.
setCustomValidity()
method to each form element to allow for users to provide custom validity messages for the components.setCustomValidity()
only sets the validity message. One must callreportValidity()
for the message to appear and the error state to be visually displayed.description
slot.customError
validity flag, just like the native elementssetValidity()
method to each form element to allow for users to have even more granular control over the validation state.reportValidity()
for the message to appear and the error state to be visually displayed.description
slot.pattern
attribute to Inputmaxlength
is exceeded.setCustomValidity()
andsetValidity()
to provide more information to the user.required
attribute returns it to a valid state.setValidity()
no longer accepts a thirdanchor
argument due to it not being utilized. The anchor is automatically set to itself.📋 Checklist
🔬 How to Test
A few notes worth mentioning:
$0
. This is the host element. As an example, if you are testingglide-core-checkbox
,$0
must be<glide-core-checkbox>
.setValidity()
andsetCustomValidity()
are copy/paste. So once you get through one, the others should go pretty quick once you get a feel of what we are testing!blur()
to be called, which callsreportValidity()
. This means the validity message may appear right away. If this happens, this is likely the cause. If you blur the element, you can try again, but from my troubleshooting in Storybook that always appears to be the cause. Seems to mostly happen with Input and Textarea when I right-click with DevTools and/or inspect.1️⃣ Checkbox
Removing required
required: true
for you!)$0.checkValidity()
should return falserequired
attribute$0.required = false;
$0.checkValidity()
should now return truesetCustomValidity()
$0.setCustomValidity('whatever you want here')
$0.checkValidity()
should return false$0.reportValidity()
should return falsereportValidity()
, the string provided should be rendered where the description slot was and the component should have the error border$0.setCustomValidity('')
$0.checkValidity()
should return true$0.reportValidity()
should return truesetValidity()
$0.setValidity({ customError: true }, 'whatever you want here');
$0.checkValidity()
should return false$0.reportValidity()
should return falsereportValidity()
, the string provided should be rendered where the description slot was and the component should have the error border$0.setValidity({})
$0.checkValidity()
should return true$0.reportValidity()
should return true2️⃣ Checkbox Group
setCustomValidity()
$0.setCustomValidity('whatever you want here')
$0.checkValidity()
should return false$0.reportValidity()
should return falsereportValidity()
, the string provided should be rendered where the description slot was and the component should have the error border$0.setCustomValidity('')
$0.checkValidity()
should return true$0.reportValidity()
should return truesetValidity()
$0.setValidity({ customError: true }, 'whatever you want here');
$0.checkValidity()
should return false$0.reportValidity()
should return falsereportValidity()
, the string provided should be rendered where the description slot was and the component should have the error border$0.setValidity({})
$0.checkValidity()
should return true$0.reportValidity()
should return true3️⃣ Dropdown
setCustomValidity()
$0.setCustomValidity('whatever you want here')
$0.checkValidity()
should return false$0.reportValidity()
should return falsereportValidity()
, the string provided should be rendered where the description slot was and the component should have the error border$0.setCustomValidity('')
$0.checkValidity()
should return true$0.reportValidity()
should return truesetValidity()
$0.setValidity({ customError: true }, 'whatever you want here');
$0.checkValidity()
should return false$0.reportValidity()
should return falsereportValidity()
, the string provided should be rendered where the description slot was and the component should have the error border$0.setValidity({})
$0.checkValidity()
should return true$0.reportValidity()
should return true4️⃣ Input
pattern (test 1)
pattern="[a-z]{4,8}"
) on glide-core-inputblur()
event will be called when you open DevTools!$0.checkValidity()
should return false$0.reportValidity()
should return false$0.checkValidity()
should return true$0.reportValidity()
should return truepattern (test 2)
pattern="[a-z]{4,8}"
) on glide-core-inputblur()
event will be called when you open DevTools!$0.checkValidity()
should return false$0.reportValidity()
should return false$0.pattern = undefined
$0.checkValidity()
should return true$0.reportValidity()
should return truesetCustomValidity()
$0.setCustomValidity('whatever you want here')
reportValidity()
.$0.checkValidity()
should return false$0.reportValidity()
should return falsereportValidity()
, the string provided should be rendered where the description slot was and the component should have the error border$0.setCustomValidity('')
$0.checkValidity()
should return true$0.reportValidity()
should return truesetValidity()
$0.setValidity({ customError: true }, 'whatever you want here');
reportValidity()
.$0.checkValidity()
should return false$0.reportValidity()
should return falsereportValidity()
, the string provided should be rendered where the description slot was and the component should have the error border$0.setValidity({})
$0.checkValidity()
should return true$0.reportValidity()
should return truecombining invalid states (1)
$0.validity
should only have one flag that istrue
-valueMissing
$0.setCustomValidity('whatever you want here')
$0.validity
should now have onlyvalueMissing
andcustomError
set totrue
$0.validity
should now have all flags set tofalse
combining invalid states (2)
$0.validity
should only have one flag that istrue
-valueMissing
$0.setCustomValidity('whatever you want here')
$0.validity
should now have onlyvalueMissing
andcustomError
set totrue
$0.setCustomValidity('')
$0.validity
should havevalueMissing
astrue
only, andcustomError
should now befalse
5️⃣ Radio Group
setCustomValidity()
$0.setCustomValidity('whatever you want here')
$0.checkValidity()
should return false$0.reportValidity()
should return falsereportValidity()
, the string provided should be rendered where the description slot was and the component should have the error border$0.setCustomValidity('')
$0.checkValidity()
should return true$0.reportValidity()
should return truesetValidity()
$0.setValidity({ customError: true }, 'whatever you want here');
$0.checkValidity()
should return false$0.reportValidity()
should return falsereportValidity()
, the string provided should be rendered where the description slot was and the component should have the error border$0.setValidity({})
$0.checkValidity()
should return true$0.reportValidity()
should return true6️⃣ Textarea
setCustomValidity()
$0.setCustomValidity('whatever you want here')
reportValidity()
.$0.checkValidity()
should return false$0.reportValidity()
should return falsereportValidity()
, the string provided should be rendered where the description slot was and the component should have the error border$0.setCustomValidity('')
$0.checkValidity()
should return true$0.reportValidity()
should return truesetValidity()
$0.setValidity({ customError: true }, 'whatever you want here');
reportValidity()
.$0.checkValidity()
should return false$0.reportValidity()
should return falsereportValidity()
, the string provided should be rendered where the description slot was and the component should have the error border$0.setValidity({})
$0.checkValidity()
should return true$0.reportValidity()
should return true📸 Images/Videos of Functionality
Checkbox
Checkbox Group
Dropdown
Input
Radio Group
Textarea