Skip to content

Commit

Permalink
fix(radio): Initial checked state through DOM attribute (#1357)
Browse files Browse the repository at this point in the history
  • Loading branch information
rkaraivanov authored Aug 27, 2024
1 parent 474177b commit 8f7f180
Show file tree
Hide file tree
Showing 5 changed files with 160 additions and 6 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/components/common/mixins/form-associated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,10 +251,10 @@ export function FormAssociatedMixin<T extends Constructor<LitElement>>(
}
}

protected handleInvalid = (event: Event) => {
protected handleInvalid(event: Event) {
event.preventDefault();
this.invalid = true;
};
}

protected setFormValue(
value: string | File | FormData | null,
Expand Down
133 changes: 133 additions & 0 deletions src/components/radio-group/radio-group.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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`
<form>
<igc-radio-group name="fruit" value="orange">
<igc-radio value="apple">Apple</igc-radio>
<igc-radio value="banana">Banana</igc-radio>
<igc-radio value="orange">Orange</igc-radio>
</igc-radio-group>
</form>
`);
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`
<form>
<igc-radio-group name="fruit">
<igc-radio value="apple" checked>Apple</igc-radio>
<igc-radio value="banana">Banana</igc-radio>
<igc-radio value="orange">Orange</igc-radio>
</igc-radio-group>
</form>
`);
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`
<form>
<igc-radio-group name="fruit">
<igc-radio value="apple" checked>Apple</igc-radio>
<igc-radio value="banana" checked>Banana</igc-radio>
<igc-radio value="orange" checked>Orange</igc-radio>
</igc-radio-group>
</form>
`);
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`
<form>
<igc-radio-group name="fruit" value="apple">
<igc-radio value="apple">Apple</igc-radio>
<igc-radio value="banana">Banana</igc-radio>
<igc-radio value="orange">Orange</igc-radio>
</igc-radio-group>
</form>
`);
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`
<form>
<igc-radio-group name="fruit">
<igc-radio value="apple" required>Apple</igc-radio>
<igc-radio value="banana">Banana</igc-radio>
<igc-radio value="orange">Orange</igc-radio>
</igc-radio-group>
</form>
`);
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() {
Expand Down
7 changes: 7 additions & 0 deletions src/components/radio-group/radio-group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export default class IgcRadioGroupComponent extends LitElement {

if (allRadiosUnchecked && this._value) {
this._setSelectedRadio();
this._setDefaultValue();
}
}

Expand All @@ -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;
Expand Down
21 changes: 17 additions & 4 deletions src/components/radio/radio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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();
Expand Down

0 comments on commit 8f7f180

Please sign in to comment.