diff --git a/CHANGELOG.md b/CHANGELOG.md index fa64a960b..acabba369 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Radio - do not emit change event on already checked radio - Calendar - add correct dates DOM parts based on active view [[#1278](https://github.com/IgniteUI/igniteui-webcomponents/issues/1278)] - Date-picker, Dropdown & Select - showing the component programmatically in response to an outside click event closes the dropdown popover [#1339](https://github.com/IgniteUI/igniteui-webcomponents/issues/1339) +- Radio - Initially checked radio by attribute throws error when not being last sibling [#1356](https://github.com/IgniteUI/igniteui-webcomponents/issues/1356) ## [4.11.1] - 2024-07-03 ### Changed diff --git a/src/components/common/mixins/form-associated.ts b/src/components/common/mixins/form-associated.ts index 6273d254b..5d9ac6cc9 100644 --- a/src/components/common/mixins/form-associated.ts +++ b/src/components/common/mixins/form-associated.ts @@ -251,10 +251,10 @@ export function FormAssociatedMixin>( } } - protected handleInvalid = (event: Event) => { + protected handleInvalid(event: Event) { event.preventDefault(); this.invalid = true; - }; + } protected setFormValue( value: string | File | FormData | null, diff --git a/src/components/radio-group/radio-group.spec.ts b/src/components/radio-group/radio-group.spec.ts index f31e52746..d00228bf9 100644 --- a/src/components/radio-group/radio-group.spec.ts +++ b/src/components/radio-group/radio-group.spec.ts @@ -8,6 +8,7 @@ import { arrowUp, } from '../common/controllers/key-bindings.js'; import { defineComponents } from '../common/definitions/defineComponents.js'; +import { first, last } from '../common/util.js'; import { isFocused, simulateKeyboard } from '../common/utils.spec.js'; import IgcRadioComponent from '../radio/radio.js'; import IgcRadioGroupComponent from './radio-group.js'; @@ -194,6 +195,138 @@ describe('Radio Group Component', () => { }); }); }); + + describe('Form integration', () => { + let form: HTMLFormElement; + let formData: FormData; + + function setFormListener() { + form.addEventListener('submit', (event: SubmitEvent) => { + event.preventDefault(); + formData = new FormData(form); + }); + } + + describe('Initial checked state', () => { + it('initial checked state through group', async () => { + form = await fixture(html` +
+ + Apple + Banana + Orange + +
+ `); + radios = Array.from(form.querySelectorAll(IgcRadioComponent.tagName)); + setFormListener(); + + expect(last(radios).checked).to.be.true; + + form.requestSubmit(); + expect(formData.get('fruit')).to.equal(last(radios).value); + }); + + it('initial checked state through radio attribute', async () => { + form = await fixture(html` +
+ + Apple + Banana + Orange + +
+ `); + group = form.querySelector(IgcRadioGroupComponent.tagName)!; + radios = Array.from(form.querySelectorAll(IgcRadioComponent.tagName)); + setFormListener(); + + expect(first(radios).checked).to.be.true; + expect(group.value).to.equal(first(radios).value); + + form.requestSubmit(); + expect(formData.get('fruit')).to.equal(first(radios).value); + }); + + it('initial multiple checked state through radio attribute', async () => { + form = await fixture(html` +
+ + Apple + Banana + Orange + +
+ `); + group = form.querySelector(IgcRadioGroupComponent.tagName)!; + radios = Array.from(form.querySelectorAll(IgcRadioComponent.tagName)); + setFormListener(); + + // The last checked member of the group takes over as the default checked + expect(last(radios).checked).to.be.true; + expect(group.value).to.equal(last(radios).value); + + form.requestSubmit(); + expect(formData.get('fruit')).to.equal(last(radios).value); + }); + + it('form reset when bound through group value attribute', async () => { + form = await fixture(html` +
+ + Apple + Banana + Orange + +
+ `); + group = form.querySelector(IgcRadioGroupComponent.tagName)!; + radios = Array.from(form.querySelectorAll(IgcRadioComponent.tagName)); + setFormListener(); + + expect(first(radios).checked).to.be.true; + + form.requestSubmit(); + expect(formData.get('fruit')).to.equal(first(radios).value); + + last(radios).click(); + await elementUpdated(last(radios)); + + expect(group.value).to.equal(last(radios).value); + form.requestSubmit(); + expect(formData.get('fruit')).to.equal(last(radios).value); + + form.reset(); + expect(first(radios).checked).to.be.true; + expect(group.value).to.equal(first(radios).value); + }); + }); + + describe('Validation state', () => { + it('required validator visual state', async () => { + form = await fixture(html` +
+ + Apple + Banana + Orange + +
+ `); + group = form.querySelector(IgcRadioGroupComponent.tagName)!; + radios = Array.from(form.querySelectorAll(IgcRadioComponent.tagName)); + setFormListener(); + + expect(radios.every((radio) => radio.invalid)).to.be.false; + + form.requestSubmit(); + expect(radios.every((radio) => radio.invalid)).to.be.true; + + form.reset(); + expect(radios.every((radio) => radio.invalid)).to.be.false; + }); + }); + }); }); function createDefaultGroup() { diff --git a/src/components/radio-group/radio-group.ts b/src/components/radio-group/radio-group.ts index efe6e3d5b..8472e9cd7 100644 --- a/src/components/radio-group/radio-group.ts +++ b/src/components/radio-group/radio-group.ts @@ -92,6 +92,7 @@ export default class IgcRadioGroupComponent extends LitElement { if (allRadiosUnchecked && this._value) { this._setSelectedRadio(); + this._setDefaultValue(); } } @@ -103,6 +104,12 @@ export default class IgcRadioGroupComponent extends LitElement { } } + private _setDefaultValue() { + for (const radio of this._radios) { + Object.assign(radio, { _defaultValue: radio.checked }); + } + } + private _setSelectedRadio() { for (const radio of this._radios) { radio.checked = radio.value === this._value; diff --git a/src/components/radio/radio.ts b/src/components/radio/radio.ts index aa09399df..889cee4d9 100644 --- a/src/components/radio/radio.ts +++ b/src/components/radio/radio.ts @@ -17,7 +17,13 @@ import { registerComponent } from '../common/definitions/register.js'; import type { Constructor } from '../common/mixins/constructor.js'; import { EventEmitterMixin } from '../common/mixins/event-emitter.js'; import { FormAssociatedRequiredMixin } from '../common/mixins/form-associated-required.js'; -import { createCounter, isLTR, partNameMap, wrap } from '../common/util.js'; +import { + createCounter, + isLTR, + last, + partNameMap, + wrap, +} from '../common/util.js'; import { styles } from './themes/radio.base.css.js'; import { styles as shared } from './themes/shared/radio.common.css.js'; import { all } from './themes/themes.js'; @@ -134,7 +140,9 @@ export default class IgcRadioComponent extends FormAssociatedRequiredMixin( @property({ type: Boolean }) public set checked(value: boolean) { this._checked = Boolean(value); - this._checked ? this._updateCheckedState() : this._updateUncheckedState(); + if (this.hasUpdated) { + this._checked ? this._updateCheckedState() : this._updateUncheckedState(); + } } public get checked(): boolean { @@ -171,11 +179,16 @@ export default class IgcRadioComponent extends FormAssociatedRequiredMixin( public override connectedCallback() { super.connectedCallback(); - - this._checked = this === this._checkedRadios[0]; this.updateValidity(); } + protected override async firstUpdated() { + await this.updateComplete; + this._checked && this === last(this._checkedRadios) + ? this._updateCheckedState() + : this.updateValidity(); + } + /** Simulates a click on the radio control. */ public override click() { this.input.click();