From 7d22e18bfb65eacfb03917d91305a921cb741357 Mon Sep 17 00:00:00 2001 From: Cory LaViska Date: Tue, 21 Jun 2022 09:21:33 -0400 Subject: [PATCH] reflect fieldset and add required --- .../radio-group/radio-group.styles.ts | 5 ++ .../radio-group/radio-group.test.ts | 61 +++++++++++++++++++ src/components/radio-group/radio-group.ts | 10 ++- src/components/radio/radio.ts | 33 +++++++++- 4 files changed, 104 insertions(+), 5 deletions(-) create mode 100644 src/components/radio-group/radio-group.test.ts diff --git a/src/components/radio-group/radio-group.styles.ts b/src/components/radio-group/radio-group.styles.ts index ffad5702..f7e3e9fb 100644 --- a/src/components/radio-group/radio-group.styles.ts +++ b/src/components/radio-group/radio-group.styles.ts @@ -44,4 +44,9 @@ export default css` overflow: hidden; white-space: nowrap; } + + .radio-group--required .radio-group__label::after { + content: var(--sl-input-required-content); + margin-inline-start: -2px; + } `; diff --git a/src/components/radio-group/radio-group.test.ts b/src/components/radio-group/radio-group.test.ts new file mode 100644 index 00000000..95da9d85 --- /dev/null +++ b/src/components/radio-group/radio-group.test.ts @@ -0,0 +1,61 @@ +import { expect, fixture, html } from '@open-wc/testing'; +import type SlRadio from '../radio/radio'; +import type SlRadioGroup from './radio-group'; + +describe('', () => { + describe('validation tests', () => { + it(`should be valid when required and one radio is checked`, async () => { + const el = await fixture(html` + + Option 1 + Option 2 + Option 3 + + `); + const radio = el.querySelector('sl-radio')!; + + expect(radio.reportValidity()).to.be.true; + }); + + it(`should be invalid when required and no radios are checked`, async () => { + const el = await fixture(html` + + Option 1 + Option 2 + Option 3 + + `); + const radio = el.querySelector('sl-radio')!; + + expect(radio.reportValidity()).to.be.false; + }); + + it(`should be valid when required and a different radio is checked`, async () => { + const el = await fixture(html` + + Option 1 + Option 2 + Option 3 + + `); + const radio = el.querySelectorAll('sl-radio')![2]; + + expect(radio.reportValidity()).to.be.true; + }); + + it(`should be invalid when custom validity is set`, async () => { + const el = await fixture(html` + + Option 1 + Option 2 + Option 3 + + `); + const radio = el.querySelector('sl-radio')!; + + radio.setCustomValidity('Error'); + + expect(radio.reportValidity()).to.be.false; + }); + }); +}); diff --git a/src/components/radio-group/radio-group.ts b/src/components/radio-group/radio-group.ts index dada60c1..3b496ece 100644 --- a/src/components/radio-group/radio-group.ts +++ b/src/components/radio-group/radio-group.ts @@ -33,7 +33,10 @@ export default class SlRadioGroup extends LitElement { @property() label = ''; /** Shows the fieldset and legend that surrounds the radio group. */ - @property({ type: Boolean, attribute: 'fieldset' }) fieldset = false; + @property({ type: Boolean, attribute: 'fieldset', reflect: true }) fieldset = false; + + /** Ensures a child radio is checked before allowing the containing form to submit. */ + @property({ type: Boolean, reflect: true }) required = false; connectedCallback() { super.connectedCallback(); @@ -89,7 +92,7 @@ export default class SlRadioGroup extends LitElement { const radios = this.getAllRadios(); const checkedRadio = radios.find(radio => radio.checked); - this.hasButtonGroup = !!radios.find(radio => radio.tagName.toLowerCase() === 'sl-radio-button'); + this.hasButtonGroup = radios.some(radio => radio.tagName.toLowerCase() === 'sl-radio-button'); radios.forEach(radio => { radio.setAttribute('role', 'radio'); @@ -113,7 +116,8 @@ export default class SlRadioGroup extends LitElement { part="base" class=${classMap({ 'radio-group': true, - 'radio-group--has-fieldset': this.fieldset + 'radio-group--has-fieldset': this.fieldset, + 'radio-group--required': this.required })} > diff --git a/src/components/radio/radio.ts b/src/components/radio/radio.ts index 6be81a2a..f856b106 100644 --- a/src/components/radio/radio.ts +++ b/src/components/radio/radio.ts @@ -74,8 +74,37 @@ export default class SlRadio extends LitElement { } /** Checks for validity and shows the browser's validation message if the control is invalid. */ - reportValidity() { - return this.input.reportValidity(); + reportValidity(): boolean { + const group = this.closest('sl-radio-group'); + const allRadios = group?.getAllRadios().filter(radio => !radio.disabled); + const isRequired = group?.required; + const isChecked = allRadios?.some(radio => radio.checked); + const internalRadio = (radio: SlRadio): HTMLInputElement => + radio.shadowRoot!.querySelector('input[type="radio"]')!; + + // If no radio group or radios are found, skip validation + if (!group || !allRadios) { + return true; + } + + // If the radio group is required but no radios are checked, mark the first internal radio required and report it + if (isRequired && !isChecked) { + const radio = internalRadio(allRadios[0]); + radio.required = true; + return radio.reportValidity(); + } + + // Reset the required state of all internal radios so we can accurately report custom validation messages + allRadios.forEach(radio => (internalRadio(radio).required = false)); + + // Report custom validation errors + for (const radio of allRadios) { + if (!internalRadio(radio).reportValidity()) { + return false; + } + } + + return true; } /** Sets a custom validation message. If `message` is not empty, the field will be considered invalid. */