Skip to content
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

Merged
merged 10 commits into from Nov 23, 2021

Conversation

Trendy
Copy link
Contributor

@Trendy Trendy commented Nov 22, 2021

See #584 for discussion

I still haven't implemented the changes to check the validity in sl-form, just looking for feedback.

@vercel
Copy link

vercel bot commented Nov 22, 2021

This pull request is being automatically deployed with Vercel (learn more).
To see the status of your deployment, click below or on the icon next to each commit.

🔍 Inspect: https://vercel.com/shoelace/shoelace/3Whq4oVZsgiddkntG84sY5D4Qwep
✅ Preview: https://shoelace-git-fork-trendy-radio-group-shoelace.vercel.app

Copy link
Member

@claviska claviska left a 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>

src/components/radio-group/radio-group.ts Show resolved Hide resolved
src/components/radio-group/radio-group.ts Show resolved Hide resolved
src/components/radio-group/radio-group.ts Outdated Show resolved Hide resolved
radios[0].required = true;

setTimeout(() => {
radios[0].reportValidity();
Copy link
Member

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.

Copy link
Contributor Author

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:

  1. Expose the underlying input element, modify it directly (very dirty IMHO).
  2. Use a custom getter/setter for the required attribute, this would let me directly manipulate the input element before changes have trigger on the sl-radio prop change (which should cause the correct validity message to trigger right after I set the prop on sl-radio).
  3. 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.
  4. Something tired me isn't thinking about.

Copy link
Contributor

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.

Copy link
Member

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.

docs/components/radio-group.md Show resolved Hide resolved
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() {
Copy link
Contributor Author

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[] {
Copy link
Contributor Author

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?

Copy link
Member

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[] {
Copy link
Contributor Author

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?

Copy link
Member

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[] {
Copy link
Contributor Author

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?

Copy link
Member

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));
Copy link
Contributor

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.

Copy link
Member

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.

Copy link
Contributor

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.

Copy link
Contributor

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[])];
Copy link
Member

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();
Copy link
Member

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() {
Copy link
Member

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() {
Copy link
Member

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.

@claviska
Copy link
Member

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. 😢

@claviska claviska merged commit e5cbee2 into shoelace-style:next Nov 23, 2021
@claviska
Copy link
Member

Sorry for the unintentional merge. I clicked the wrong button in VS Code. 🤦🏻‍♂️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants