Skip to content

Commit 8f7f180

Browse files
authored
fix(radio): Initial checked state through DOM attribute (#1357)
1 parent 474177b commit 8f7f180

File tree

5 files changed

+160
-6
lines changed

5 files changed

+160
-6
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
7070
- Radio - do not emit change event on already checked radio
7171
- Calendar - add correct dates DOM parts based on active view [[#1278](https://github.com/IgniteUI/igniteui-webcomponents/issues/1278)]
7272
- 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)
73+
- Radio - Initially checked radio by attribute throws error when not being last sibling [#1356](https://github.com/IgniteUI/igniteui-webcomponents/issues/1356)
7374

7475
## [4.11.1] - 2024-07-03
7576
### Changed

src/components/common/mixins/form-associated.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -251,10 +251,10 @@ export function FormAssociatedMixin<T extends Constructor<LitElement>>(
251251
}
252252
}
253253

254-
protected handleInvalid = (event: Event) => {
254+
protected handleInvalid(event: Event) {
255255
event.preventDefault();
256256
this.invalid = true;
257-
};
257+
}
258258

259259
protected setFormValue(
260260
value: string | File | FormData | null,

src/components/radio-group/radio-group.spec.ts

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
arrowUp,
99
} from '../common/controllers/key-bindings.js';
1010
import { defineComponents } from '../common/definitions/defineComponents.js';
11+
import { first, last } from '../common/util.js';
1112
import { isFocused, simulateKeyboard } from '../common/utils.spec.js';
1213
import IgcRadioComponent from '../radio/radio.js';
1314
import IgcRadioGroupComponent from './radio-group.js';
@@ -194,6 +195,138 @@ describe('Radio Group Component', () => {
194195
});
195196
});
196197
});
198+
199+
describe('Form integration', () => {
200+
let form: HTMLFormElement;
201+
let formData: FormData;
202+
203+
function setFormListener() {
204+
form.addEventListener('submit', (event: SubmitEvent) => {
205+
event.preventDefault();
206+
formData = new FormData(form);
207+
});
208+
}
209+
210+
describe('Initial checked state', () => {
211+
it('initial checked state through group', async () => {
212+
form = await fixture(html`
213+
<form>
214+
<igc-radio-group name="fruit" value="orange">
215+
<igc-radio value="apple">Apple</igc-radio>
216+
<igc-radio value="banana">Banana</igc-radio>
217+
<igc-radio value="orange">Orange</igc-radio>
218+
</igc-radio-group>
219+
</form>
220+
`);
221+
radios = Array.from(form.querySelectorAll(IgcRadioComponent.tagName));
222+
setFormListener();
223+
224+
expect(last(radios).checked).to.be.true;
225+
226+
form.requestSubmit();
227+
expect(formData.get('fruit')).to.equal(last(radios).value);
228+
});
229+
230+
it('initial checked state through radio attribute', async () => {
231+
form = await fixture(html`
232+
<form>
233+
<igc-radio-group name="fruit">
234+
<igc-radio value="apple" checked>Apple</igc-radio>
235+
<igc-radio value="banana">Banana</igc-radio>
236+
<igc-radio value="orange">Orange</igc-radio>
237+
</igc-radio-group>
238+
</form>
239+
`);
240+
group = form.querySelector(IgcRadioGroupComponent.tagName)!;
241+
radios = Array.from(form.querySelectorAll(IgcRadioComponent.tagName));
242+
setFormListener();
243+
244+
expect(first(radios).checked).to.be.true;
245+
expect(group.value).to.equal(first(radios).value);
246+
247+
form.requestSubmit();
248+
expect(formData.get('fruit')).to.equal(first(radios).value);
249+
});
250+
251+
it('initial multiple checked state through radio attribute', async () => {
252+
form = await fixture(html`
253+
<form>
254+
<igc-radio-group name="fruit">
255+
<igc-radio value="apple" checked>Apple</igc-radio>
256+
<igc-radio value="banana" checked>Banana</igc-radio>
257+
<igc-radio value="orange" checked>Orange</igc-radio>
258+
</igc-radio-group>
259+
</form>
260+
`);
261+
group = form.querySelector(IgcRadioGroupComponent.tagName)!;
262+
radios = Array.from(form.querySelectorAll(IgcRadioComponent.tagName));
263+
setFormListener();
264+
265+
// The last checked member of the group takes over as the default checked
266+
expect(last(radios).checked).to.be.true;
267+
expect(group.value).to.equal(last(radios).value);
268+
269+
form.requestSubmit();
270+
expect(formData.get('fruit')).to.equal(last(radios).value);
271+
});
272+
273+
it('form reset when bound through group value attribute', async () => {
274+
form = await fixture(html`
275+
<form>
276+
<igc-radio-group name="fruit" value="apple">
277+
<igc-radio value="apple">Apple</igc-radio>
278+
<igc-radio value="banana">Banana</igc-radio>
279+
<igc-radio value="orange">Orange</igc-radio>
280+
</igc-radio-group>
281+
</form>
282+
`);
283+
group = form.querySelector(IgcRadioGroupComponent.tagName)!;
284+
radios = Array.from(form.querySelectorAll(IgcRadioComponent.tagName));
285+
setFormListener();
286+
287+
expect(first(radios).checked).to.be.true;
288+
289+
form.requestSubmit();
290+
expect(formData.get('fruit')).to.equal(first(radios).value);
291+
292+
last(radios).click();
293+
await elementUpdated(last(radios));
294+
295+
expect(group.value).to.equal(last(radios).value);
296+
form.requestSubmit();
297+
expect(formData.get('fruit')).to.equal(last(radios).value);
298+
299+
form.reset();
300+
expect(first(radios).checked).to.be.true;
301+
expect(group.value).to.equal(first(radios).value);
302+
});
303+
});
304+
305+
describe('Validation state', () => {
306+
it('required validator visual state', async () => {
307+
form = await fixture(html`
308+
<form>
309+
<igc-radio-group name="fruit">
310+
<igc-radio value="apple" required>Apple</igc-radio>
311+
<igc-radio value="banana">Banana</igc-radio>
312+
<igc-radio value="orange">Orange</igc-radio>
313+
</igc-radio-group>
314+
</form>
315+
`);
316+
group = form.querySelector(IgcRadioGroupComponent.tagName)!;
317+
radios = Array.from(form.querySelectorAll(IgcRadioComponent.tagName));
318+
setFormListener();
319+
320+
expect(radios.every((radio) => radio.invalid)).to.be.false;
321+
322+
form.requestSubmit();
323+
expect(radios.every((radio) => radio.invalid)).to.be.true;
324+
325+
form.reset();
326+
expect(radios.every((radio) => radio.invalid)).to.be.false;
327+
});
328+
});
329+
});
197330
});
198331

199332
function createDefaultGroup() {

src/components/radio-group/radio-group.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ export default class IgcRadioGroupComponent extends LitElement {
9292

9393
if (allRadiosUnchecked && this._value) {
9494
this._setSelectedRadio();
95+
this._setDefaultValue();
9596
}
9697
}
9798

@@ -103,6 +104,12 @@ export default class IgcRadioGroupComponent extends LitElement {
103104
}
104105
}
105106

107+
private _setDefaultValue() {
108+
for (const radio of this._radios) {
109+
Object.assign(radio, { _defaultValue: radio.checked });
110+
}
111+
}
112+
106113
private _setSelectedRadio() {
107114
for (const radio of this._radios) {
108115
radio.checked = radio.value === this._value;

src/components/radio/radio.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,13 @@ import { registerComponent } from '../common/definitions/register.js';
1717
import type { Constructor } from '../common/mixins/constructor.js';
1818
import { EventEmitterMixin } from '../common/mixins/event-emitter.js';
1919
import { FormAssociatedRequiredMixin } from '../common/mixins/form-associated-required.js';
20-
import { createCounter, isLTR, partNameMap, wrap } from '../common/util.js';
20+
import {
21+
createCounter,
22+
isLTR,
23+
last,
24+
partNameMap,
25+
wrap,
26+
} from '../common/util.js';
2127
import { styles } from './themes/radio.base.css.js';
2228
import { styles as shared } from './themes/shared/radio.common.css.js';
2329
import { all } from './themes/themes.js';
@@ -134,7 +140,9 @@ export default class IgcRadioComponent extends FormAssociatedRequiredMixin(
134140
@property({ type: Boolean })
135141
public set checked(value: boolean) {
136142
this._checked = Boolean(value);
137-
this._checked ? this._updateCheckedState() : this._updateUncheckedState();
143+
if (this.hasUpdated) {
144+
this._checked ? this._updateCheckedState() : this._updateUncheckedState();
145+
}
138146
}
139147

140148
public get checked(): boolean {
@@ -171,11 +179,16 @@ export default class IgcRadioComponent extends FormAssociatedRequiredMixin(
171179

172180
public override connectedCallback() {
173181
super.connectedCallback();
174-
175-
this._checked = this === this._checkedRadios[0];
176182
this.updateValidity();
177183
}
178184

185+
protected override async firstUpdated() {
186+
await this.updateComplete;
187+
this._checked && this === last(this._checkedRadios)
188+
? this._updateCheckedState()
189+
: this.updateValidity();
190+
}
191+
179192
/** Simulates a click on the radio control. */
180193
public override click() {
181194
this.input.click();

0 commit comments

Comments
 (0)