From c4e68a22951a9c3cf0efb4914d02a70d3e40c827 Mon Sep 17 00:00:00 2001 From: Ivaylo Barakov Date: Thu, 27 Jan 2022 16:57:56 +0200 Subject: [PATCH] feat(radio): support required attribute (#212) * feat(radio): support required attribute Co-authored-by: Galina Edinakova Co-authored-by: Diyan Dimitrov <43128948+DiyanDimitrov@users.noreply.github.com> --- CHANGELOG.md | 1 + .../radio-group/radio-group.spec.ts | 15 +++++++++- src/components/radio-group/radio-group.ts | 30 ++++++++++++++----- src/components/radio/radio.spec.ts | 15 ++++++++++ src/components/radio/radio.ts | 20 +++++++++++-- stories/form.stories.ts | 5 +++- stories/radio.stories.ts | 10 ++++++- 7 files changed, 83 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93c220b2b..fcd062a12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Dark Themes - Slider component - Range Slider component +- Support `required` property in Radio component. ### Changed diff --git a/src/components/radio-group/radio-group.spec.ts b/src/components/radio-group/radio-group.spec.ts index f0d4142d1..8baebde4f 100644 --- a/src/components/radio-group/radio-group.spec.ts +++ b/src/components/radio-group/radio-group.spec.ts @@ -112,6 +112,17 @@ describe('Radio Group Component', () => { expect(radio3).to.be.calledWith('igcFocus'); expect(radio3).to.be.calledWith('igcChange'); }); + + it('should set required attribute correctly', async () => { + expect(Array.from(radios).every((r) => r.required)).to.be.false; + expect(radios[0].required).to.be.true; + + radios[1].checked = true; + await elementUpdated(group); + + expect(radios[0].required).to.be.false; + expect(radios[1].required).to.be.true; + }); }); const values = ['orange', 'apple', 'mango']; @@ -119,7 +130,9 @@ describe('Radio Group Component', () => { template = html` ${values.map( (value) => - html`${value}` + html`${value}` )} ` ) => { diff --git a/src/components/radio-group/radio-group.ts b/src/components/radio-group/radio-group.ts index 311c429a2..cd97b3c2f 100644 --- a/src/components/radio-group/radio-group.ts +++ b/src/components/radio-group/radio-group.ts @@ -8,12 +8,11 @@ export default class IgcRadioGroupComponent extends LitElement { public static override styles = styles; - @queryAssignedElements({ flatten: true, selector: 'igc-radio' }) - private _slottedRadios!: Array; - - private get radios() { - return this._slottedRadios.filter((radio) => !radio.disabled); - } + @queryAssignedElements({ + flatten: true, + selector: 'igc-radio:not([disabled])', + }) + private radios!: Array; private get isLTR(): boolean { const styles = window.getComputedStyle(this); @@ -23,11 +22,28 @@ export default class IgcRadioGroupComponent extends LitElement { constructor() { super(); this.addEventListener('keydown', this.handleKeydown); + this.addEventListener('igcChange', this.updateRequiredState); } @property({ reflect: true }) public alignment: 'vertical' | 'horizontal' = 'vertical'; + private updateRequiredState() { + const hasRequired = this.radios.some((r) => r.required); + + if (hasRequired) { + this.radios.forEach((r) => (r.required = false)); + + const hasChecked = this.radios.some((r) => r.checked); + + if (hasChecked) { + this.radios.filter((r) => r.checked)[0].required = true; + } else { + this.radios[0].required = true; + } + } + } + private handleKeydown = (event: KeyboardEvent) => { const { key } = event; @@ -61,7 +77,7 @@ export default class IgcRadioGroupComponent extends LitElement { }; protected override render() { - return html``; + return html``; } } diff --git a/src/components/radio/radio.spec.ts b/src/components/radio/radio.spec.ts index 33c76117b..01b21ded9 100644 --- a/src/components/radio/radio.spec.ts +++ b/src/components/radio/radio.spec.ts @@ -46,6 +46,8 @@ describe('Radio Component', () => { expect(radio.invalid).to.equal(false); expect(radio.disabled).to.equal(false); expect(input.disabled).to.equal(false); + expect(radio.required).to.equal(false); + expect(input.required).to.equal(false); expect(radio.labelPosition).to.equal('after'); }); @@ -158,6 +160,19 @@ describe('Radio Component', () => { ); }); + it('sets the required property successfully', async () => { + radio.required = true; + expect(radio.required).to.be.true; + await elementUpdated(radio); + expect(input).dom.to.equal(``, DIFF_OPTIONS); + + radio.required = false; + expect(radio.required).to.be.false; + await elementUpdated(radio); + + expect(input).dom.to.equal(``, DIFF_OPTIONS); + }); + it('should emit focus/blur events when methods are called', () => { const eventSpy = sinon.spy(radio, 'emitEvent'); radio.focus(); diff --git a/src/components/radio/radio.ts b/src/components/radio/radio.ts index 48ebe4746..9399a24a5 100644 --- a/src/components/radio/radio.ts +++ b/src/components/radio/radio.ts @@ -54,6 +54,10 @@ export default class IgcRadioComponent extends EventEmitterMixin< @property() public value!: string; + /** Makes the control a required field. */ + @property({ type: Boolean, reflect: true }) + public required = false; + /** The checked state of the control. */ @property({ type: Boolean }) @blazorTwoWayBind('igcChange', 'detail') @@ -133,16 +137,25 @@ export default class IgcRadioComponent extends EventEmitterMixin< this.input.focus(); this._tabIndex = 0; this.emitEvent('igcChange', { detail: this.checked }); + } else { + if (this.required) { + this.required = false; + this.getAllInGroup()[0].required = true; + } } } protected getSiblings() { + return this.getAllInGroup().filter( + (radio) => radio.name === this.name && radio !== this + ); + } + + protected getAllInGroup() { const group = this.closest('igc-radio-group'); if (!group) return []; - return Array.from( - group.querySelectorAll('igc-radio') - ).filter((radio) => radio.name === this.name && radio !== this); + return Array.from(group.querySelectorAll('igc-radio')); } protected override render() { @@ -157,6 +170,7 @@ export default class IgcRadioComponent extends EventEmitterMixin< type="radio" name="${ifDefined(this.name)}" value="${ifDefined(this.value)}" + .required="${this.required}" .disabled="${this.disabled}" .checked="${live(this.checked)}" tabindex=${this._tabIndex} diff --git a/stories/form.stories.ts b/stories/form.stories.ts index 35523f5e0..e20076b63 100644 --- a/stories/form.stories.ts +++ b/stories/form.stories.ts @@ -43,7 +43,10 @@ The cat was playing
in the garden.Gender: ${radios.map( - (v) => html`${v} ` + (v) => + html`${v}` )} diff --git a/stories/radio.stories.ts b/stories/radio.stories.ts index 6a80b09b9..60dd81876 100644 --- a/stories/radio.stories.ts +++ b/stories/radio.stories.ts @@ -17,6 +17,12 @@ const metadata = { description: 'The value attribute of the control.', control: 'text', }, + required: { + type: 'boolean', + description: 'Makes the control a required field.', + control: 'boolean', + defaultValue: false, + }, checked: { type: 'boolean', description: 'The checked state of the control.', @@ -55,6 +61,7 @@ export default metadata; interface ArgTypes { name: string; value: string; + required: boolean; checked: boolean; disabled: boolean; invalid: boolean; @@ -64,13 +71,14 @@ interface ArgTypes { // endregion const Template: Story = ( - { labelPosition, checked, disabled }: ArgTypes, + { labelPosition, checked, disabled, required }: ArgTypes, { globals: { direction } }: Context ) => html` Label