diff --git a/docs/resources/changelog.md b/docs/resources/changelog.md index 4c6b8e0e..cb1c954c 100644 --- a/docs/resources/changelog.md +++ b/docs/resources/changelog.md @@ -12,6 +12,7 @@ _During the beta period, these restrictions may be relaxed in the event of a mis - Added `sl-request-close` event to `sl-dialog` and `sl-drawer` - Added `dialog.denyClose` and `drawer.denyClose` animations - Fixed a bug in `sl-color-picker` where setting `value` immediately wouldn't trigger an update +- Fixed a bug that resulted in form controls having incorrect validity when `disabled` was initially set [#473](https://github.com/shoelace-style/shoelace/issues/473) ## 2.0.0-beta.44 diff --git a/src/components/checkbox/checkbox.ts b/src/components/checkbox/checkbox.ts index 565f5ade..8a3f03a9 100644 --- a/src/components/checkbox/checkbox.ts +++ b/src/components/checkbox/checkbox.ts @@ -64,6 +64,14 @@ export default class SlCheckbox extends LitElement { this.invalid = !this.input.checkValidity(); } + updated(changedProps: Map) { + // Disabled form controls are always valid, so we need to recheck validity when the state changes + if (changedProps.get('disabled')) { + this.input.disabled = this.disabled; + this.invalid = !this.input.checkValidity(); + } + } + /** Simulates a click on the checkbox. */ click() { this.input.click(); diff --git a/src/components/input/input.test.ts b/src/components/input/input.test.ts new file mode 100644 index 00000000..40835bf5 --- /dev/null +++ b/src/components/input/input.test.ts @@ -0,0 +1,35 @@ +import { expect, fixture, html, waitUntil } from '@open-wc/testing'; +import sinon from 'sinon'; + +import '../../../dist/shoelace.js'; +import type SlInput from './input'; + +describe('', () => { + it('should be disabled with the disabled attribute', async () => { + const el = await fixture(html` `); + const input = el.shadowRoot?.querySelector('[part="input"]') as HTMLInputElement; + + expect(input.disabled).to.be.true; + }); + + it('should be valid by default', async () => { + const el = (await fixture(html` `)) as SlInput; + + expect(el.invalid).to.be.false; + }); + + it('should be invalid when required and empty', async () => { + const el = (await fixture(html` `)) as SlInput; + + expect(el.invalid).to.be.true; + }); + + it('should be invalid when required and after removing disabled ', async () => { + const el = (await fixture(html` `)) as SlInput; + + el.disabled = false; + await el.updateComplete; + + expect(el.invalid).to.be.true; + }); +}); diff --git a/src/components/input/input.ts b/src/components/input/input.ts index 42333a40..60820451 100644 --- a/src/components/input/input.ts +++ b/src/components/input/input.ts @@ -156,6 +156,14 @@ export default class SlInput extends LitElement { this.invalid = !this.input.checkValidity(); } + updated(changedProps: Map) { + // Disabled form controls are always valid, so we need to recheck validity when the state changes + if (changedProps.get('disabled')) { + this.input.disabled = this.disabled; + this.invalid = !this.input.checkValidity(); + } + } + disconnectedCallback() { super.disconnectedCallback(); this.shadowRoot!.removeEventListener('slotchange', this.handleSlotChange); diff --git a/src/components/radio/radio.ts b/src/components/radio/radio.ts index 438e39bb..71d6c39e 100644 --- a/src/components/radio/radio.ts +++ b/src/components/radio/radio.ts @@ -56,6 +56,14 @@ export default class SlRadio extends LitElement { /** Emitted when the control gains focus. */ @event('sl-focus') slFocus: EventEmitter; + updated(changedProps: Map) { + // Disabled form controls are always valid, so we need to recheck validity when the state changes + if (changedProps.get('disabled')) { + this.input.disabled = this.disabled; + this.invalid = !this.input.checkValidity(); + } + } + /** Simulates a click on the radio. */ click() { this.input.click(); diff --git a/src/components/range/range.ts b/src/components/range/range.ts index a4da1015..0752479a 100644 --- a/src/components/range/range.ts +++ b/src/components/range/range.ts @@ -100,6 +100,14 @@ export default class SlRange extends LitElement { }); } + updated(changedProps: Map) { + // Disabled form controls are always valid, so we need to recheck validity when the state changes + if (changedProps.get('disabled')) { + this.input.disabled = this.disabled; + this.invalid = !this.input.checkValidity(); + } + } + disconnectedCallback() { super.disconnectedCallback(); this.resizeObserver.unobserve(this.input); diff --git a/src/components/select/select.ts b/src/components/select/select.ts index ec4f661d..d9293ca6 100644 --- a/src/components/select/select.ts +++ b/src/components/select/select.ts @@ -137,6 +137,14 @@ export default class SlSelect extends LitElement { this.invalid = !this.input.checkValidity(); } + updated(changedProps: Map) { + // Disabled form controls are always valid, so we need to recheck validity when the state changes + if (changedProps.get('disabled')) { + this.input.disabled = this.disabled; + this.invalid = !this.input.checkValidity(); + } + } + disconnectedCallback() { super.disconnectedCallback(); this.resizeObserver.unobserve(this); @@ -187,13 +195,6 @@ export default class SlSelect extends LitElement { this.syncItemsFromValue(); } - @watch('disabled') - handleDisabledChange() { - if (this.disabled && this.isOpen) { - this.dropdown.hide(); - } - } - handleFocus() { if (!this.hasFocus) { this.hasFocus = true; @@ -281,6 +282,13 @@ export default class SlSelect extends LitElement { this.box.focus(); } + @watch('disabled') + async handleDisabledChange() { + if (this.disabled && this.isOpen) { + this.dropdown.hide(); + } + } + @watch('multiple') handleMultipleChange() { // Cast to array | string based on `this.multiple` diff --git a/src/components/switch/switch.ts b/src/components/switch/switch.ts index 754daf9a..ae7b83fe 100644 --- a/src/components/switch/switch.ts +++ b/src/components/switch/switch.ts @@ -64,6 +64,14 @@ export default class SlSwitch extends LitElement { this.invalid = !this.input.checkValidity(); } + updated(changedProps: Map) { + // Disabled form controls are always valid, so we need to recheck validity when the state changes + if (changedProps.get('disabled')) { + this.input.disabled = this.disabled; + this.invalid = !this.input.checkValidity(); + } + } + /** Simulates a click on the switch. */ click() { this.input.click(); diff --git a/src/components/textarea/textarea.test.ts b/src/components/textarea/textarea.test.ts new file mode 100644 index 00000000..18ce3378 --- /dev/null +++ b/src/components/textarea/textarea.test.ts @@ -0,0 +1,35 @@ +import { expect, fixture, html, waitUntil } from '@open-wc/testing'; +import sinon from 'sinon'; + +import '../../../dist/shoelace.js'; +import type SlTextarea from './textarea'; + +describe('', () => { + it('should be disabled with the disabled attribute', async () => { + const el = await fixture(html` `); + const textarea = el.shadowRoot?.querySelector('[part="textarea"]') as HTMLInputElement; + + expect(textarea.disabled).to.be.true; + }); + + it('should be valid by default', async () => { + const el = (await fixture(html` `)) as SlTextarea; + + expect(el.invalid).to.be.false; + }); + + it('should be invalid when required and empty', async () => { + const el = (await fixture(html` `)) as SlTextarea; + + expect(el.invalid).to.be.true; + }); + + it('should be invalid when required and after removing disabled ', async () => { + const el = (await fixture(html` `)) as SlTextarea; + + el.disabled = false; + await el.updateComplete; + + expect(el.invalid).to.be.true; + }); +}); diff --git a/src/components/textarea/textarea.ts b/src/components/textarea/textarea.ts index 4911d304..559265f4 100644 --- a/src/components/textarea/textarea.ts +++ b/src/components/textarea/textarea.ts @@ -144,6 +144,14 @@ export default class SlTextarea extends LitElement { this.invalid = !this.input.checkValidity(); } + updated(changedProps: Map) { + // Disabled form controls are always valid, so we need to recheck validity when the state changes + if (changedProps.get('disabled')) { + this.input.disabled = this.disabled; + this.invalid = !this.input.checkValidity(); + } + } + disconnectedCallback() { super.disconnectedCallback(); this.resizeObserver.unobserve(this.input);