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
#587 Add a required attribute to sl-radio-group/sl-radio #595
Conversation
This pull request is being automatically deployed with Vercel (learn more). 🔍 Inspect: https://vercel.com/shoelace/shoelace/3Whq4oVZsgiddkntG84sY5D4Qwep |
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 great! I like the idea of considering "radio group" the form control instead of each individual radio. With this PR, we're getting closer to that.
A couple additional observations:
-
The
setCustomValidity()
method should also be hoisted to Radio Group. -
It would be very convenient if Radio Group worked like Select so you can get/set the selected item using the
value
prop. For example:
<sl-radio-group label="Select an option">
<sl-radio value="1" checked>Option 1</sl-radio>
<sl-radio value="2">Option 2</sl-radio>
<sl-radio value="3">Option 3</sl-radio>
</sl-radio-group>
<script>
const radioGroup = document.querySelector('sl-radio-group');
console.log(radioGroup.value); // outputs "1"
// The third radio will now be checked
radioGroup.value = 3;
</script>
radios[0].required = true; | ||
|
||
setTimeout(() => { | ||
radios[0].reportValidity(); |
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.
The result of this call to reportValidity()
should be the return value in case there's an inconsistency.
I'm assuming the setTimeout
was added to wait for the next render so the radio will be ticked, right? All other reportValidity()
methods in the library are synchronous, so making this one async feels a bit weird. This might be a good use case for calling update()
.
Does this work in lieu of the timeout?
radios[0].required = true;
const changedProps = new Map<string, any>();
changedProps.set('required', false);
radios[0].update(changedProps);
If not, we may have to live with making reportValidity
an async 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.
The setTimeout
was added because I ran into an issue with timing.
I don't know when the sl-radio
elements are added to the default slot. So I can't reliably force a required flag on the first radio element during initial render.
I just want the browser-level validation message to display on the first sl-radio
element in the group, and the only way to trigger it is to set the underlying InputElement
to required
, then run the reportValidation
. That requires that I manipulate the attribute on sl-radio
, which means I need to wait for the event queue to flush before triggering the validation.
There are a couple of things I have thought about to get around this, but haven't decided:
- Expose the underlying input element, modify it directly (very dirty IMHO).
- Use a custom getter/setter for the
required
attribute, this would let me directly manipulate the input element before changes have trigger on thesl-radio
prop change (which should cause the correct validity message to trigger right after I set the prop onsl-radio
). - I could never expose ANY of the underlying input element (itself or attributes) and just provide a
triggerRequiredWarning
method of some sort. I didn't like this at first, but if you think about a use case where the first input might be off-screen (edge case IMHO)...rl-radio-group
could handle that in the future and trigger the validation on a visible input. - Something tired me isn't thinking about.
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.
@Trendy What about making the sl-radio responsible for checking its parent's required state? If a new/first sl-radio is added, it will itself acquire the correct state. If the sl-radio-group receives a (new) required state it can set it on the first sl-radio if present and otherwise assume the first child will pick it up itself.
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.
Expose the underlying input element, modify it directly (very dirty IMHO).
In this case, it's cleaner than forcing an update I guess.
radio.shadowRoot.querySelector('input[type="radio"]').required = true;
It would keep things synchronous, at least. I'm not opposed to it since the components are required to be used together.
Co-authored-by: Cory LaViska <cory@abeautifulsite.net>
|
||
@query('slot:not([name])') defaultSlot: HTMLSlotElement; | ||
|
||
/** The radio group label. Required for proper accessibility. Alternatively, you can use the label slot. */ | ||
@property() label = ''; | ||
|
||
/** The current value of the radio group. */ | ||
@property() | ||
get 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 might be odd, but I need a way to get the value after render, but not have it be blank because the default isn't in sync with the defaulted 'checked' radio.
If you know a better way to ensure this has a good default when the user does a get
on the value.... I'm open to it.
@@ -39,6 +87,63 @@ export default class SlRadioGroup extends LitElement { | |||
}); | |||
} | |||
|
|||
getAllRadios(): SlRadio[] { |
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.
Should this be documented and exposed to the users?
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.
Nope. They can use radioGroup.querySelector('sl-radio')
.
return [...this.querySelectorAll('sl-radio')]; | ||
} | ||
|
||
checkRadioByIndex(index: number): SlRadio[] { |
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.
Should this be documented and exposed to the users?
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.
Nah. They control the slot so it doesn’t seem valuable.
return radios; | ||
} | ||
|
||
deselectAll(): SlRadio[] { |
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.
Okay, first copy/pasta was reasonable, but do these methods make sense to expose and document?
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.
Only public API methods should have JSDoc comments. The rest are pseudo-private methods that don’t get published. Is this intended to be public?
if (index < 0) index = radios.length - 1; | ||
if (index > radios.length - 1) index = 0; | ||
|
||
this.getAllRadios().map(radio => (radio.checked = false)); |
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 forEach
would be better than map
here.
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.
Is this a performance assumption? I tend to use map
over forEach
since the result of getAllRadios()
is an array.
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.
Nope. The result is not used, so mapping
does not add any usefulness. forEach
and map
both iterate over the array elements. The latter answers a new array with the result of each iteration as new value for the array. Ie [ 1, 2, 3 ].map(function(x) { return x * x; })
will result in [ 1, 4, 9 ]
. forEach
always returns undefined
.
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 do think performance is slightly worse with map
since a new array structure is created. For forEach
this is not necessary. Not sure if this is noticeable in real live.
@@ -39,6 +42,26 @@ export default class SlRadioGroup extends LitElement { | |||
}); | |||
} | |||
|
|||
reportValidity() { | |||
const radios = [...(this.defaultSlot.assignedElements({ flatten: true }) as SlRadio[])]; |
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.
For some reason, this results in chunk.QG7EXAZB.js:150 Uncaught TypeError: Cannot read properties of null (reading 'assignedElements')
when you click the "Validate group" button.
Any reason you're not using this.getAllRadios()
here?
radios[0].required = true; | ||
|
||
setTimeout(() => { | ||
radios[0].reportValidity(); |
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.
Expose the underlying input element, modify it directly (very dirty IMHO).
In this case, it's cleaner than forcing an update I guess.
radio.shadowRoot.querySelector('input[type="radio"]').required = true;
It would keep things synchronous, at least. I'm not opposed to it since the components are required to be used together.
/** Indicates that a selection is required. */ | ||
@property({ type: Boolean, reflect: true }) required = false; | ||
|
||
connectedCallback() { |
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.
Right now, important base class logic that's supposed to happen is being overwritten by this. You need to call super.connectedCallback()
at the top of this method. (This is a big gotcha when coming from Stencil to Lit.)
this.addEventListener('sl-change', this.syncRadioButtons); | ||
} | ||
|
||
disconnectedCallback() { |
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.
Add super.disconnectedCallback()
at the top of this method block.
I played with this locally and made some improvements. Published them to this branch: https://github.com/shoelace-style/shoelace/tree/Trendy-radio-group After trying out the updates in VoiceOver, it reads pretty badly. Not sure how to improve on it. We may need to revisit the approach of using Radio Group as a "single control" and make it work more like HTML — but that won't be easy since the inputs are in shadow roots. This one isn't an easy fix, unfortunately. 😢 |
Sorry for the unintentional merge. I clicked the wrong button in VS Code. 🤦🏻♂️ |
See #584 for discussion
I still haven't implemented the changes to check the validity in sl-form, just looking for feedback.