From 37a1680a85efbab5adcf474d1abc905efc75e6fa Mon Sep 17 00:00:00 2001 From: Cory LaViska Date: Tue, 17 Dec 2024 11:10:04 -0500 Subject: [PATCH 01/22] move states --- docs/_layouts/component.njk | 50 ++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/docs/_layouts/component.njk b/docs/_layouts/component.njk index 7d1e57faf..1b7a95cc5 100644 --- a/docs/_layouts/component.njk +++ b/docs/_layouts/component.njk @@ -143,31 +143,6 @@ {% endif %} - {# States #} - {% if component.states.length %} -

States

-
- - - - - - - - - - {% for state in component.states %} - - - - - - {% endfor %} - -
NameDescriptionCSS selector
{{ state.name }}{{ state.description | inlineMarkdown | safe }}[data-state-{{ state.name }}]
-
- {% endif %} - {# Events #} {% if component.events.length %}

Events

@@ -225,6 +200,31 @@ {% endif %} + {# Custom States #} + {% if component.cssStates.length %} +

Custom States

+
+ + + + + + + + + + {% for state in component.cssStates %} + + + + + + {% endfor %} + +
NameDescriptionCSS selector
{{ state.name }}{{ state.description | inlineMarkdown | safe }}[data-state-{{ state.name }}]
+
+ {% endif %} + {# CSS Parts #} {% if component.cssParts.length %}

CSS parts

From 8fbd0b54e42889cea18bfd533291584418bedb78 Mon Sep 17 00:00:00 2001 From: Cory LaViska Date: Tue, 17 Dec 2024 11:10:10 -0500 Subject: [PATCH 02/22] update changelog --- docs/docs/resources/changelog.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/docs/resources/changelog.md b/docs/docs/resources/changelog.md index 725e481be..2e51ef074 100644 --- a/docs/docs/resources/changelog.md +++ b/docs/docs/resources/changelog.md @@ -12,6 +12,14 @@ Components with the Experimental bad During the alpha period, things might break! We take breaking changes very seriously, but sometimes they're necessary to make the final product that much better. We appreciate your patience! ::: +## Next + +- Added `checked` and `disabled` custom states to `` and `` +- Removed stateful CSS parts in favor of custom states + - ``: `control--checked`, `control--indeterminate` + - ``: `control--checked` + - ``: `item--disabled`, `item--expanded`, `item--indeterminate`, `item--selected` + ## 3.0.0-alpha.5 - Added the Finnish translation From 5d687f1aa02b2f0c44e05015375822b6d9cc132c Mon Sep 17 00:00:00 2001 From: Cory LaViska Date: Tue, 17 Dec 2024 11:12:40 -0500 Subject: [PATCH 03/22] add custom states to checkbox/radio --- src/components/checkbox/checkbox.ts | 25 ++++++++++++++++++------- src/components/radio/radio.ts | 8 ++++++-- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/components/checkbox/checkbox.ts b/src/components/checkbox/checkbox.ts index 0780a8d7f..5abef71ed 100644 --- a/src/components/checkbox/checkbox.ts +++ b/src/components/checkbox/checkbox.ts @@ -36,8 +36,6 @@ import styles from './checkbox.css'; * * @csspart base - The component's label . * @csspart control - The square container that wraps the checkbox's checked state. - * @csspart control--checked - Matches the control part when the checkbox is checked. - * @csspart control--indeterminate - Matches the control part when the checkbox is indeterminate. * @csspart checked-icon - The checked icon, a `` element. * @csspart indeterminate-icon - The indeterminate icon, a `` element. * @csspart label - The container that wraps the checkbox's label. @@ -53,6 +51,11 @@ import styles from './checkbox.css'; * @cssproperty --box-shadow - The shadow effects around the edges of the checkbox. * @cssproperty --checked-icon-color - The color of the checkbox's icon. * @cssproperty --toggle-size - The size of the checkbox. + * + * @cssstate checked - Applied when the checkbox is checked. + * @cssstate disabled - Applied when the checkbox is disabled. + * @cssstate indeterminate - Applied when the checkbox is in an indeterminate state. + * */ @customElement('wa-checkbox') export default class WaCheckbox extends WebAwesomeFormAssociatedElement { @@ -157,20 +160,28 @@ export default class WaCheckbox extends WebAwesomeFormAssociatedElement { } handleValueOrCheckedChange() { - this.toggleCustomState('checked', this.checked); - // These @watch() commands seem to override the base element checks for changes, so we need to setValue for the form and and updateValidity() this.setValue(this.checked ? this.value : null, this._value); this.updateValidity(); } - @watch(['checked', 'indeterminate'], { waitUntilFirstUpdate: true }) + @watch(['checked', 'indeterminate']) handleStateChange() { - this.input.checked = this.checked; // force a sync update - this.input.indeterminate = this.indeterminate; // force a sync update + if (this.hasUpdated) { + this.input.checked = this.checked; // force a sync update + this.input.indeterminate = this.indeterminate; // force a sync update + } + + this.toggleCustomState('checked', this.checked); + this.toggleCustomState('indeterminate', this.indeterminate); this.updateValidity(); } + @watch('disabled') + handleDisabledChange() { + this.toggleCustomState('disabled', this.disabled); + } + protected willUpdate(changedProperties: PropertyValues): void { super.willUpdate(changedProperties); diff --git a/src/components/radio/radio.ts b/src/components/radio/radio.ts index 119e145a6..de346abc0 100644 --- a/src/components/radio/radio.ts +++ b/src/components/radio/radio.ts @@ -24,7 +24,6 @@ import styles from './radio.css'; * * @csspart base - The component's base wrapper. * @csspart control - The circular container that wraps the radio's checked state. - * @csspart control--checked - The radio control when the radio is checked. * @csspart checked-icon - The checked icon. * @csspart label - The container that wraps the radio's label. * @@ -38,6 +37,9 @@ import styles from './radio.css'; * @cssproperty --checked-icon-color - The color of the radio's checked icon. * @cssproperty --checked-icon-scale - The size of the checked icon relative to the radio. * @cssproperty --toggle-size - The size of the radio. + * + * @cssstate checked - Applied when the control is checked. + * @cssstate disabled - Applied when the control is disabled. */ @customElement('wa-radio') export default class WaRadio extends WebAwesomeFormAssociatedElement { @@ -92,6 +94,7 @@ export default class WaRadio extends WebAwesomeFormAssociatedElement { @watch('checked') handleCheckedChange() { + this.toggleCustomState('checked', this.checked); this.setAttribute('aria-checked', this.checked ? 'true' : 'false'); this.tabIndex = this.checked ? 0 : -1; } @@ -105,6 +108,7 @@ export default class WaRadio extends WebAwesomeFormAssociatedElement { @watch('disabled', { waitUntilFirstUpdate: true }) handleDisabledChange() { + this.toggleCustomState('disabled', this.disabled); this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false'); } @@ -124,7 +128,7 @@ export default class WaRadio extends WebAwesomeFormAssociatedElement { 'radio--disabled': this.disabled, })} > - + ${this.checked ? html` Date: Tue, 17 Dec 2024 11:13:20 -0500 Subject: [PATCH 04/22] remove data- attribute fallback for states --- src/internal/webawesome-element.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/internal/webawesome-element.ts b/src/internal/webawesome-element.ts index 17e2364ed..ec7311894 100644 --- a/src/internal/webawesome-element.ts +++ b/src/internal/webawesome-element.ts @@ -497,8 +497,6 @@ export class WebAwesomeFormAssociatedElement (this.internals.states as Set).add(state); } catch (_) { // Without this, test suite errors. - } finally { - this.setAttribute(`data-wa-${state}`, ''); } } @@ -507,8 +505,6 @@ export class WebAwesomeFormAssociatedElement (this.internals.states as Set).delete(state); } catch (_) { // Without this, test suite errors. - } finally { - this.removeAttribute(`data-wa-${state}`); } } From 88e2af266f3efaa831a4ba40e9e71eb694fd028c Mon Sep 17 00:00:00 2001 From: Cory LaViska Date: Tue, 17 Dec 2024 11:18:43 -0500 Subject: [PATCH 05/22] update docs and tests --- docs/docs/form-controls.md | 12 ++-- src/components/checkbox/checkbox.test.ts | 12 ++-- .../color-picker/color-picker.test.ts | 32 +++++------ src/components/input/input.test.ts | 56 +++++++++---------- .../radio-group/radio-group.test.ts | 44 +++++++-------- src/components/select/select.test.ts | 44 +++++++-------- src/components/switch/switch.test.ts | 12 ++-- src/components/textarea/textarea.test.ts | 56 +++++++++---------- 8 files changed, 134 insertions(+), 134 deletions(-) diff --git a/docs/docs/form-controls.md b/docs/docs/form-controls.md index 9db42078e..1a62c0dc5 100644 --- a/docs/docs/form-controls.md +++ b/docs/docs/form-controls.md @@ -165,12 +165,12 @@ Custom validation can be applied to any form control that supports the `setCusto Due to the many ways form controls are used, Web Awesome doesn't provide out of the box validation styles for form controls as part of its default theme. Instead, the following attributes will be applied to reflect a control's validity as users interact with it. You can use them to create custom styles for any of the validation states you're interested in. -- `data-wa-required` - the form control is required -- `data-wa-optional` - the form control is optional -- `data-wa-invalid` - the form control is invalid -- `data-wa-valid` - the form control is valid -- `data-wa-user-invalid` - the form control is invalid and the user has interacted with it -- `data-wa-user-valid` - the form control is valid and the user has interacted with it +- `required` - the form control is required +- `optional` - the form control is optional +- `invalid` - the form control is invalid +- `valid` - the form control is valid +- `user-invalid` - the form control is invalid and the user has interacted with it +- `user-valid` - the form control is valid and the user has interacted with it These attributes map to the browser's built-in pseudo classes for validation: [`:required`](https://developer.mozilla.org/en-US/docs/Web/CSS/:required), [`:optional`](https://developer.mozilla.org/en-US/docs/Web/CSS/:optional), [`:invalid`](https://developer.mozilla.org/en-US/docs/Web/CSS/:invalid), [`:valid`](https://developer.mozilla.org/en-US/docs/Web/CSS/:valid), [`:user-invalid`](https://developer.mozilla.org/en-US/docs/Web/CSS/:user-invalid), and [`:user-valid`](https://developer.mozilla.org/en-US/docs/Web/CSS/:user-valid). diff --git a/src/components/checkbox/checkbox.test.ts b/src/components/checkbox/checkbox.test.ts index 9dcb01e1a..cfdfbb6ac 100644 --- a/src/components/checkbox/checkbox.test.ts +++ b/src/components/checkbox/checkbox.test.ts @@ -244,12 +244,12 @@ describe('', () => { `); const checkbox = el.querySelector('wa-checkbox')!; - expect(checkbox.hasAttribute('data-wa-required')).to.be.true; - expect(checkbox.hasAttribute('data-wa-optional')).to.be.false; - expect(checkbox.hasAttribute('data-wa-invalid')).to.be.true; - expect(checkbox.hasAttribute('data-wa-valid')).to.be.false; - expect(checkbox.hasAttribute('data-wa-user-invalid')).to.be.false; - expect(checkbox.hasAttribute('data-wa-user-valid')).to.be.false; + expect(checkbox.hasCustomState('required')).to.be.true; + expect(checkbox.hasCustomState('optional')).to.be.false; + expect(checkbox.hasCustomState('invalid')).to.be.true; + expect(checkbox.hasCustomState('valid')).to.be.false; + expect(checkbox.hasCustomState('user-invalid')).to.be.false; + expect(checkbox.hasCustomState('user-valid')).to.be.false; }); }); diff --git a/src/components/color-picker/color-picker.test.ts b/src/components/color-picker/color-picker.test.ts index 36f52224e..8a1efe5f1 100644 --- a/src/components/color-picker/color-picker.test.ts +++ b/src/components/color-picker/color-picker.test.ts @@ -501,12 +501,12 @@ describe('', () => { const grid = el.shadowRoot!.querySelector('[part~="grid"]')!; expect(el.checkValidity()).to.be.true; - expect(el.hasAttribute('data-wa-required')).to.be.true; - expect(el.hasAttribute('data-wa-optional')).to.be.false; - expect(el.hasAttribute('data-wa-invalid')).to.be.false; - expect(el.hasAttribute('data-wa-valid')).to.be.true; - expect(el.hasAttribute('data-wa-user-invalid')).to.be.false; - expect(el.hasAttribute('data-wa-user-valid')).to.be.false; + expect(el.hasCustomState('required')).to.be.true; + expect(el.hasCustomState('optional')).to.be.false; + expect(el.hasCustomState('invalid')).to.be.false; + expect(el.hasCustomState('valid')).to.be.true; + expect(el.hasCustomState('user-invalid')).to.be.false; + expect(el.hasCustomState('user-valid')).to.be.false; await clickOnElement(trigger); await aTimeout(500); @@ -514,8 +514,8 @@ describe('', () => { await el.updateComplete; expect(el.checkValidity()).to.be.true; - expect(el.hasAttribute('data-wa-user-invalid')).to.be.false; - expect(el.hasAttribute('data-wa-user-valid')).to.be.true; + expect(el.hasCustomState('user-invalid')).to.be.false; + expect(el.hasCustomState('user-valid')).to.be.true; }); it('should receive the correct validation attributes ("states") when invalid', async () => { @@ -523,12 +523,12 @@ describe('', () => { const trigger = el.shadowRoot!.querySelector('[part~="trigger"]')!; const grid = el.shadowRoot!.querySelector('[part~="grid"]')!; - expect(el.hasAttribute('data-wa-required')).to.be.true; - expect(el.hasAttribute('data-wa-optional')).to.be.false; - expect(el.hasAttribute('data-wa-invalid')).to.be.true; - expect(el.hasAttribute('data-wa-valid')).to.be.false; - expect(el.hasAttribute('data-wa-user-invalid')).to.be.false; - expect(el.hasAttribute('data-wa-user-valid')).to.be.false; + expect(el.hasCustomState('required')).to.be.true; + expect(el.hasCustomState('optional')).to.be.false; + expect(el.hasCustomState('invalid')).to.be.true; + expect(el.hasCustomState('valid')).to.be.false; + expect(el.hasCustomState('user-invalid')).to.be.false; + expect(el.hasCustomState('user-valid')).to.be.false; await clickOnElement(trigger); await aTimeout(500); @@ -536,8 +536,8 @@ describe('', () => { await el.updateComplete; expect(el.checkValidity()).to.be.true; - expect(el.hasAttribute('data-wa-user-invalid')).to.be.false; - expect(el.hasAttribute('data-wa-user-valid')).to.be.true; + expect(el.hasCustomState('user-invalid')).to.be.false; + expect(el.hasCustomState('user-valid')).to.be.true; }); }); }); diff --git a/src/components/input/input.test.ts b/src/components/input/input.test.ts index 219587092..5ed7531d9 100644 --- a/src/components/input/input.test.ts +++ b/src/components/input/input.test.ts @@ -122,12 +122,12 @@ describe('', () => { const el = await fixture(html` `); expect(el.checkValidity()).to.be.true; - expect(el.hasAttribute('data-wa-required')).to.be.true; - expect(el.hasAttribute('data-wa-optional')).to.be.false; - expect(el.hasAttribute('data-wa-invalid')).to.be.false; - expect(el.hasAttribute('data-wa-valid')).to.be.true; - expect(el.hasAttribute('data-wa-user-invalid')).to.be.false; - expect(el.hasAttribute('data-wa-user-valid')).to.be.false; + expect(el.hasCustomState('required')).to.be.true; + expect(el.hasCustomState('optional')).to.be.false; + expect(el.hasCustomState('invalid')).to.be.false; + expect(el.hasCustomState('valid')).to.be.true; + expect(el.hasCustomState('user-invalid')).to.be.false; + expect(el.hasCustomState('user-valid')).to.be.false; el.focus(); await el.updateComplete; @@ -137,19 +137,19 @@ describe('', () => { await el.updateComplete; expect(el.checkValidity()).to.be.true; - expect(el.hasAttribute('data-wa-user-invalid')).to.be.false; - expect(el.hasAttribute('data-wa-user-valid')).to.be.true; + expect(el.hasCustomState('user-invalid')).to.be.false; + expect(el.hasCustomState('user-valid')).to.be.true; }); it('should receive the correct validation attributes ("states") when invalid', async () => { const el = await fixture(html` `); - expect(el.hasAttribute('data-wa-required')).to.be.true; - expect(el.hasAttribute('data-wa-optional')).to.be.false; - expect(el.hasAttribute('data-wa-invalid')).to.be.true; - expect(el.hasAttribute('data-wa-valid')).to.be.false; - expect(el.hasAttribute('data-wa-user-invalid')).to.be.false; - expect(el.hasAttribute('data-wa-user-valid')).to.be.false; + expect(el.hasCustomState('required')).to.be.true; + expect(el.hasCustomState('optional')).to.be.false; + expect(el.hasCustomState('invalid')).to.be.true; + expect(el.hasCustomState('valid')).to.be.false; + expect(el.hasCustomState('user-invalid')).to.be.false; + expect(el.hasCustomState('user-valid')).to.be.false; el.focus(); await el.updateComplete; @@ -159,20 +159,20 @@ describe('', () => { el.blur(); await el.updateComplete; - expect(el.hasAttribute('data-wa-user-invalid')).to.be.true; - expect(el.hasAttribute('data-wa-user-valid')).to.be.false; + expect(el.hasCustomState('user-invalid')).to.be.true; + expect(el.hasCustomState('user-valid')).to.be.false; }); it('should receive validation attributes ("states") even when novalidate is used on the parent form', async () => { const el = await fixture(html`
`); const input = el.querySelector('wa-input')!; - expect(input.hasAttribute('data-wa-required')).to.be.true; - expect(input.hasAttribute('data-wa-optional')).to.be.false; - expect(input.hasAttribute('data-wa-invalid')).to.be.true; - expect(input.hasAttribute('data-wa-valid')).to.be.false; - expect(input.hasAttribute('data-wa-user-invalid')).to.be.false; - expect(input.hasAttribute('data-wa-user-valid')).to.be.false; + expect(input.hasCustomState('required')).to.be.true; + expect(input.hasCustomState('optional')).to.be.false; + expect(input.hasCustomState('invalid')).to.be.true; + expect(input.hasCustomState('valid')).to.be.false; + expect(input.hasCustomState('user-invalid')).to.be.false; + expect(input.hasCustomState('user-valid')).to.be.false; }); }); @@ -229,10 +229,10 @@ describe('', () => { await input.updateComplete; expect(input.checkValidity()).to.be.false; - expect(input.hasAttribute('data-wa-invalid')).to.be.true; - expect(input.hasAttribute('data-wa-valid')).to.be.false; - expect(input.hasAttribute('data-wa-user-invalid')).to.be.false; - expect(input.hasAttribute('data-wa-user-valid')).to.be.false; + expect(input.hasCustomState('invalid')).to.be.true; + expect(input.hasCustomState('valid')).to.be.false; + expect(input.hasCustomState('user-invalid')).to.be.false; + expect(input.hasCustomState('user-valid')).to.be.false; input.focus(); await sendKeys({ type: 'test' }); @@ -240,8 +240,8 @@ describe('', () => { input.blur(); await input.updateComplete; - expect(input.hasAttribute('data-wa-user-invalid')).to.be.true; - expect(input.hasAttribute('data-wa-user-valid')).to.be.false; + expect(input.hasCustomState('user-invalid')).to.be.true; + expect(input.hasCustomState('user-valid')).to.be.false; }); it('should be present in form data when using the form attribute and located outside of a
', async () => { diff --git a/src/components/radio-group/radio-group.test.ts b/src/components/radio-group/radio-group.test.ts index a12fc3086..37b65e866 100644 --- a/src/components/radio-group/radio-group.test.ts +++ b/src/components/radio-group/radio-group.test.ts @@ -100,19 +100,19 @@ describe('', () => { const secondRadio = radioGroup.querySelectorAll('wa-radio')[1]; expect(radioGroup.checkValidity()).to.be.true; - expect(radioGroup.hasAttribute('data-wa-required')).to.be.true; - expect(radioGroup.hasAttribute('data-wa-optional')).to.be.false; - expect(radioGroup.hasAttribute('data-wa-invalid')).to.be.false; - expect(radioGroup.hasAttribute('data-wa-valid')).to.be.true; - expect(radioGroup.hasAttribute('data-wa-user-invalid')).to.be.false; - expect(radioGroup.hasAttribute('data-wa-user-valid')).to.be.false; + expect(radioGroup.hasCustomState('required')).to.be.true; + expect(radioGroup.hasCustomState('optional')).to.be.false; + expect(radioGroup.hasCustomState('invalid')).to.be.false; + expect(radioGroup.hasCustomState('valid')).to.be.true; + expect(radioGroup.hasCustomState('user-invalid')).to.be.false; + expect(radioGroup.hasCustomState('user-valid')).to.be.false; await clickOnElement(secondRadio); await secondRadio.updateComplete; expect(radioGroup.checkValidity()).to.be.true; - expect(radioGroup.hasAttribute('data-wa-user-invalid')).to.be.false; - expect(radioGroup.hasAttribute('data-wa-user-valid')).to.be.true; + expect(radioGroup.hasCustomState('user-invalid')).to.be.false; + expect(radioGroup.hasCustomState('user-valid')).to.be.true; }); it('should receive the correct validation attributes ("states") when invalid', async () => { @@ -124,19 +124,19 @@ describe('', () => { `); const secondRadio = radioGroup.querySelectorAll('wa-radio')[1]; - expect(radioGroup.hasAttribute('data-wa-required')).to.be.true; - expect(radioGroup.hasAttribute('data-wa-optional')).to.be.false; - expect(radioGroup.hasAttribute('data-wa-invalid')).to.be.true; - expect(radioGroup.hasAttribute('data-wa-valid')).to.be.false; - expect(radioGroup.hasAttribute('data-wa-user-invalid')).to.be.false; - expect(radioGroup.hasAttribute('data-wa-user-valid')).to.be.false; + expect(radioGroup.hasCustomState('required')).to.be.true; + expect(radioGroup.hasCustomState('optional')).to.be.false; + expect(radioGroup.hasCustomState('invalid')).to.be.true; + expect(radioGroup.hasCustomState('valid')).to.be.false; + expect(radioGroup.hasCustomState('user-invalid')).to.be.false; + expect(radioGroup.hasCustomState('user-valid')).to.be.false; await clickOnElement(secondRadio); radioGroup.value = ''; await radioGroup.updateComplete; - expect(radioGroup.hasAttribute('data-wa-user-invalid')).to.be.true; - expect(radioGroup.hasAttribute('data-wa-user-valid')).to.be.false; + expect(radioGroup.hasCustomState('user-invalid')).to.be.true; + expect(radioGroup.hasCustomState('user-valid')).to.be.false; }); it('should receive validation attributes ("states") even when novalidate is used on the parent form', async () => { @@ -150,12 +150,12 @@ describe('', () => { `); const radioGroup = el.querySelector('wa-radio-group')!; - expect(radioGroup.hasAttribute('data-wa-required')).to.be.true; - expect(radioGroup.hasAttribute('data-wa-optional')).to.be.false; - expect(radioGroup.hasAttribute('data-wa-invalid')).to.be.true; - expect(radioGroup.hasAttribute('data-wa-valid')).to.be.false; - expect(radioGroup.hasAttribute('data-wa-user-invalid')).to.be.false; - expect(radioGroup.hasAttribute('data-wa-user-valid')).to.be.false; + expect(radioGroup.hasCustomState('required')).to.be.true; + expect(radioGroup.hasCustomState('optional')).to.be.false; + expect(radioGroup.hasCustomState('invalid')).to.be.true; + expect(radioGroup.hasCustomState('valid')).to.be.false; + expect(radioGroup.hasCustomState('user-invalid')).to.be.false; + expect(radioGroup.hasCustomState('user-valid')).to.be.false; }); it('should show a constraint validation error when setCustomValidity() is called', async () => { diff --git a/src/components/select/select.test.ts b/src/components/select/select.test.ts index 4c45e1a4f..f0cb8bf3a 100644 --- a/src/components/select/select.test.ts +++ b/src/components/select/select.test.ts @@ -330,12 +330,12 @@ describe('', () => { const secondOption = el.querySelectorAll('wa-option')[1]; expect(el.checkValidity()).to.be.true; - expect(el.hasAttribute('data-wa-required')).to.be.true; - expect(el.hasAttribute('data-wa-optional')).to.be.false; - expect(el.hasAttribute('data-wa-invalid')).to.be.false; - expect(el.hasAttribute('data-wa-valid')).to.be.true; - expect(el.hasAttribute('data-wa-user-invalid')).to.be.false; - expect(el.hasAttribute('data-wa-user-valid')).to.be.false; + expect(el.hasCustomState('required')).to.be.true; + expect(el.hasCustomState('optional')).to.be.false; + expect(el.hasCustomState('invalid')).to.be.false; + expect(el.hasCustomState('valid')).to.be.true; + expect(el.hasCustomState('user-invalid')).to.be.false; + expect(el.hasCustomState('user-valid')).to.be.false; await el.show(); await clickOnElement(secondOption); @@ -344,8 +344,8 @@ describe('', () => { await el.updateComplete; expect(el.checkValidity()).to.be.true; - expect(el.hasAttribute('data-wa-user-invalid')).to.be.false; - expect(el.hasAttribute('data-wa-user-valid')).to.be.true; + expect(el.hasCustomState('user-invalid')).to.be.false; + expect(el.hasCustomState('user-valid')).to.be.true; }); it('should receive the correct validation attributes ("states") when invalid', async () => { @@ -358,12 +358,12 @@ describe('', () => { `); const secondOption = el.querySelectorAll('wa-option')[1]; - expect(el.hasAttribute('data-wa-required')).to.be.true; - expect(el.hasAttribute('data-wa-optional')).to.be.false; - expect(el.hasAttribute('data-wa-invalid')).to.be.true; - expect(el.hasAttribute('data-wa-valid')).to.be.false; - expect(el.hasAttribute('data-wa-user-invalid')).to.be.false; - expect(el.hasAttribute('data-wa-user-valid')).to.be.false; + expect(el.hasCustomState('required')).to.be.true; + expect(el.hasCustomState('optional')).to.be.false; + expect(el.hasCustomState('invalid')).to.be.true; + expect(el.hasCustomState('valid')).to.be.false; + expect(el.hasCustomState('user-invalid')).to.be.false; + expect(el.hasCustomState('user-valid')).to.be.false; await el.show(); await clickOnElement(secondOption); @@ -372,8 +372,8 @@ describe('', () => { el.blur(); await el.updateComplete; - expect(el.hasAttribute('data-wa-user-invalid')).to.be.true; - expect(el.hasAttribute('data-wa-user-valid')).to.be.false; + expect(el.hasCustomState('user-invalid')).to.be.true; + expect(el.hasCustomState('user-valid')).to.be.false; }); it('should receive validation attributes ("states") even when novalidate is used on the parent form', async () => { @@ -388,12 +388,12 @@ describe('', () => { `); const select = el.querySelector('wa-select')!; - expect(select.hasAttribute('data-wa-required')).to.be.true; - expect(select.hasAttribute('data-wa-optional')).to.be.false; - expect(select.hasAttribute('data-wa-invalid')).to.be.true; - expect(select.hasAttribute('data-wa-valid')).to.be.false; - expect(select.hasAttribute('data-wa-user-invalid')).to.be.false; - expect(select.hasAttribute('data-wa-user-valid')).to.be.false; + expect(select.hasCustomState('required')).to.be.true; + expect(select.hasCustomState('optional')).to.be.false; + expect(select.hasCustomState('invalid')).to.be.true; + expect(select.hasCustomState('valid')).to.be.false; + expect(select.hasCustomState('user-invalid')).to.be.false; + expect(select.hasCustomState('user-valid')).to.be.false; }); }); diff --git a/src/components/switch/switch.test.ts b/src/components/switch/switch.test.ts index ab923f904..7b7fa964b 100644 --- a/src/components/switch/switch.test.ts +++ b/src/components/switch/switch.test.ts @@ -230,12 +230,12 @@ describe('', () => { const el = await fixture(html`
`); const waSwitch = el.querySelector('wa-switch')!; - expect(waSwitch.hasAttribute('data-wa-required')).to.be.true; - expect(waSwitch.hasAttribute('data-wa-optional')).to.be.false; - expect(waSwitch.hasAttribute('data-wa-invalid')).to.be.true; - expect(waSwitch.hasAttribute('data-wa-valid')).to.be.false; - expect(waSwitch.hasAttribute('data-wa-user-invalid')).to.be.false; - expect(waSwitch.hasAttribute('data-wa-user-valid')).to.be.false; + expect(waSwitch.hasCustomState('required')).to.be.true; + expect(waSwitch.hasCustomState('optional')).to.be.false; + expect(waSwitch.hasCustomState('invalid')).to.be.true; + expect(waSwitch.hasCustomState('valid')).to.be.false; + expect(waSwitch.hasCustomState('user-invalid')).to.be.false; + expect(waSwitch.hasCustomState('user-valid')).to.be.false; }); }); diff --git a/src/components/textarea/textarea.test.ts b/src/components/textarea/textarea.test.ts index 4b794c0f5..46a52d794 100644 --- a/src/components/textarea/textarea.test.ts +++ b/src/components/textarea/textarea.test.ts @@ -144,12 +144,12 @@ describe('', () => { const el = await fixture(html` `); expect(el.checkValidity()).to.be.true; - expect(el.hasAttribute('data-wa-required')).to.be.true; - expect(el.hasAttribute('data-wa-optional')).to.be.false; - expect(el.hasAttribute('data-wa-invalid')).to.be.false; - expect(el.hasAttribute('data-wa-valid')).to.be.true; - expect(el.hasAttribute('data-wa-user-invalid')).to.be.false; - expect(el.hasAttribute('data-wa-user-valid')).to.be.false; + expect(el.hasCustomState('required')).to.be.true; + expect(el.hasCustomState('optional')).to.be.false; + expect(el.hasCustomState('invalid')).to.be.false; + expect(el.hasCustomState('valid')).to.be.true; + expect(el.hasCustomState('user-invalid')).to.be.false; + expect(el.hasCustomState('user-valid')).to.be.false; el.focus(); await sendKeys({ press: 'b' }); @@ -158,19 +158,19 @@ describe('', () => { await el.updateComplete; expect(el.checkValidity()).to.be.true; - expect(el.hasAttribute('data-wa-user-invalid')).to.be.false; - expect(el.hasAttribute('data-wa-user-valid')).to.be.true; + expect(el.hasCustomState('user-invalid')).to.be.false; + expect(el.hasCustomState('user-valid')).to.be.true; }); it('should receive the correct validation attributes ("states") when invalid', async () => { const el = await fixture(html` `); - expect(el.hasAttribute('data-wa-required')).to.be.true; - expect(el.hasAttribute('data-wa-optional')).to.be.false; - expect(el.hasAttribute('data-wa-invalid')).to.be.true; - expect(el.hasAttribute('data-wa-valid')).to.be.false; - expect(el.hasAttribute('data-wa-user-invalid')).to.be.false; - expect(el.hasAttribute('data-wa-user-valid')).to.be.false; + expect(el.hasCustomState('required')).to.be.true; + expect(el.hasCustomState('optional')).to.be.false; + expect(el.hasCustomState('invalid')).to.be.true; + expect(el.hasCustomState('valid')).to.be.false; + expect(el.hasCustomState('user-invalid')).to.be.false; + expect(el.hasCustomState('user-valid')).to.be.false; el.focus(); await sendKeys({ press: 'a' }); @@ -179,8 +179,8 @@ describe('', () => { el.blur(); await el.updateComplete; - expect(el.hasAttribute('data-wa-user-invalid')).to.be.true; - expect(el.hasAttribute('data-wa-user-valid')).to.be.false; + expect(el.hasCustomState('user-invalid')).to.be.true; + expect(el.hasCustomState('user-valid')).to.be.false; }); it('should receive validation attributes ("states") even when novalidate is used on the parent form', async () => { @@ -189,12 +189,12 @@ describe('', () => { `); const textarea = el.querySelector('wa-textarea')!; - expect(textarea.hasAttribute('data-wa-required')).to.be.true; - expect(textarea.hasAttribute('data-wa-optional')).to.be.false; - expect(textarea.hasAttribute('data-wa-invalid')).to.be.true; - expect(textarea.hasAttribute('data-wa-valid')).to.be.false; - expect(textarea.hasAttribute('data-wa-user-invalid')).to.be.false; - expect(textarea.hasAttribute('data-wa-user-valid')).to.be.false; + expect(textarea.hasCustomState('required')).to.be.true; + expect(textarea.hasCustomState('optional')).to.be.false; + expect(textarea.hasCustomState('invalid')).to.be.true; + expect(textarea.hasCustomState('valid')).to.be.false; + expect(textarea.hasCustomState('user-invalid')).to.be.false; + expect(textarea.hasCustomState('user-valid')).to.be.false; }); }); @@ -222,10 +222,10 @@ describe('', () => { await textarea.updateComplete; expect(textarea.checkValidity()).to.be.false; - expect(textarea.hasAttribute('data-wa-invalid')).to.be.true; - expect(textarea.hasAttribute('data-wa-valid')).to.be.false; - expect(textarea.hasAttribute('data-wa-user-invalid')).to.be.false; - expect(textarea.hasAttribute('data-wa-user-valid')).to.be.false; + expect(textarea.hasCustomState('invalid')).to.be.true; + expect(textarea.hasCustomState('valid')).to.be.false; + expect(textarea.hasCustomState('user-invalid')).to.be.false; + expect(textarea.hasCustomState('user-valid')).to.be.false; textarea.focus(); await sendKeys({ type: 'test' }); @@ -233,8 +233,8 @@ describe('', () => { textarea.blur(); await textarea.updateComplete; - expect(textarea.hasAttribute('data-wa-user-invalid')).to.be.true; - expect(textarea.hasAttribute('data-wa-user-valid')).to.be.false; + expect(textarea.hasCustomState('user-invalid')).to.be.true; + expect(textarea.hasCustomState('user-valid')).to.be.false; }); it('should be present in form data when using the form attribute and located outside of a
', async () => { From 4a046684c81d2c2b718d5a5c9279e5b0414a331f Mon Sep 17 00:00:00 2001 From: Cory LaViska Date: Tue, 17 Dec 2024 11:22:49 -0500 Subject: [PATCH 06/22] add comments and fix hasCustomState --- src/internal/webawesome-element.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/internal/webawesome-element.ts b/src/internal/webawesome-element.ts index ec7311894..58871caf8 100644 --- a/src/internal/webawesome-element.ts +++ b/src/internal/webawesome-element.ts @@ -491,7 +491,7 @@ export class WebAwesomeFormAssociatedElement this.setValidity(flags, finalMessage, formControl); } - // Custom states + /** Adds a custom state to the element. */ addCustomState(state: string) { try { (this.internals.states as Set).add(state); @@ -500,6 +500,7 @@ export class WebAwesomeFormAssociatedElement } } + /** Removes a custom state from the element. */ deleteCustomState(state: string) { try { (this.internals.states as Set).delete(state); @@ -508,6 +509,7 @@ export class WebAwesomeFormAssociatedElement } } + /** Toggles a custom state on the element. */ toggleCustomState(state: string, force: boolean) { if (force) { this.addCustomState(state); @@ -522,6 +524,7 @@ export class WebAwesomeFormAssociatedElement this.toggleCustomState(state, !this.hasCustomState(state)); } + /** Determines if the element has the specified custom state. */ hasCustomState(state: string) { let bool = false; @@ -529,10 +532,6 @@ export class WebAwesomeFormAssociatedElement bool = (this.internals.states as Set).has(state); } catch (_) { // Without this, test suite errors. - } finally { - if (!bool) { - bool = this.hasAttribute(`data-wa-${state}`); - } } return bool; From 7c40243da30f5a23fda34c61d580ba73015bf73d Mon Sep 17 00:00:00 2001 From: Cory LaViska Date: Tue, 17 Dec 2024 11:26:33 -0500 Subject: [PATCH 07/22] add custom state types to wa form control --- src/internal/webawesome-element.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/internal/webawesome-element.ts b/src/internal/webawesome-element.ts index 58871caf8..16bc4aa17 100644 --- a/src/internal/webawesome-element.ts +++ b/src/internal/webawesome-element.ts @@ -142,6 +142,12 @@ export interface WebAwesomeFormControl extends WebAwesomeElement { reportValidity: () => boolean; setCustomValidity: (message: string) => void; + // Custom state methods + hasCustomState: (state: string) => boolean; + addCustomState: (state: string) => void; + deleteCustomState(state: string): void; + toggleCustomState(state: string, force: boolean): void; + // Form properties hasInteracted: boolean; valueHasChanged?: boolean; From 0eebf4429243300363afe42389e736f9c7bc58ad Mon Sep 17 00:00:00 2001 From: Cory LaViska Date: Tue, 17 Dec 2024 11:29:47 -0500 Subject: [PATCH 08/22] move custom states --- docs/_layouts/component.njk | 50 ++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/docs/_layouts/component.njk b/docs/_layouts/component.njk index 1b7a95cc5..83cfa0ecd 100644 --- a/docs/_layouts/component.njk +++ b/docs/_layouts/component.njk @@ -200,31 +200,6 @@ {% endif %} - {# Custom States #} - {% if component.cssStates.length %} -

Custom States

-
- - - - - - - - - - {% for state in component.cssStates %} - - - - - - {% endfor %} - -
NameDescriptionCSS selector
{{ state.name }}{{ state.description | inlineMarkdown | safe }}[data-state-{{ state.name }}]
-
- {% endif %} - {# CSS Parts #} {% if component.cssParts.length %}

CSS parts

@@ -249,6 +224,31 @@ {% endif %} + {# Custom States #} + {% if component.cssStates.length %} +

Custom States

+
+ + + + + + + + + + {% for state in component.cssStates %} + + + + + + {% endfor %} + +
NameDescriptionCSS selector
{{ state.name }}{{ state.description | inlineMarkdown | safe }}:state({{ state.name }})
+
+ {% endif %} + {# Dependencies #} {% if component.dependencies.length %}

Dependencies

From fe05300bdcddf2578592868601f25b9cd60be265 Mon Sep 17 00:00:00 2001 From: Cory LaViska Date: Tue, 17 Dec 2024 11:29:59 -0500 Subject: [PATCH 09/22] use :state() --- docs/assets/examples/page-demo/demo.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/assets/examples/page-demo/demo.js b/docs/assets/examples/page-demo/demo.js index 3e9801f44..79704fae7 100644 --- a/docs/assets/examples/page-demo/demo.js +++ b/docs/assets/examples/page-demo/demo.js @@ -10,7 +10,7 @@ let includes = `${stylesheets} `; function render() { - let slots = Array.from(fieldset.querySelectorAll('wa-checkbox[name=slot]:is([data-wa-checked])')); + let slots = Array.from(fieldset.querySelectorAll('wa-checkbox[name=slot]:state(checked)')); let slotsHTML = slots .map(slot => { let name = slot.getAttribute('value'); From 543fa3c85c239752cef9df7dc46d90c3564ab5da Mon Sep 17 00:00:00 2001 From: Cory LaViska Date: Tue, 17 Dec 2024 11:30:07 -0500 Subject: [PATCH 10/22] update tests to check states --- src/components/checkbox/checkbox.test.ts | 12 ++++++------ src/components/range/range.test.ts | 20 ++++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/components/checkbox/checkbox.test.ts b/src/components/checkbox/checkbox.test.ts index cfdfbb6ac..4ef27ae5d 100644 --- a/src/components/checkbox/checkbox.test.ts +++ b/src/components/checkbox/checkbox.test.ts @@ -199,17 +199,17 @@ describe('', () => { expect(checkbox.checkValidity()).to.be.false; expect(checkbox.checkValidity()).to.be.false; - expect(checkbox.hasAttribute('data-wa-invalid')).to.be.true; - expect(checkbox.hasAttribute('data-wa-valid')).to.be.false; - expect(checkbox.hasAttribute('data-wa-user-invalid')).to.be.true; - expect(checkbox.hasAttribute('data-wa-user-valid')).to.be.false; + expect(checkbox.hasCustomState('invalid')).to.be.true; + expect(checkbox.hasCustomState('valid')).to.be.false; + expect(checkbox.hasCustomState('user-invalid')).to.be.true; + expect(checkbox.hasCustomState('user-valid')).to.be.false; await clickOnElement(checkbox); await checkbox.updateComplete; await aTimeout(0); - expect(checkbox.hasAttribute('data-wa-user-invalid')).to.be.true; - expect(checkbox.hasAttribute('data-wa-user-valid')).to.be.false; + expect(checkbox.hasCustomState('user-invalid')).to.be.true; + expect(checkbox.hasCustomState('user-valid')).to.be.false; }); it('should be invalid when required and unchecked', async () => { diff --git a/src/components/range/range.test.ts b/src/components/range/range.test.ts index 70f011788..a948c0f17 100644 --- a/src/components/range/range.test.ts +++ b/src/components/range/range.test.ts @@ -164,18 +164,18 @@ describe('', () => { await range.updateComplete; expect(range.checkValidity()).to.be.false; - expect(range.hasAttribute('data-wa-invalid')).to.be.true; - expect(range.hasAttribute('data-wa-valid')).to.be.false; - expect(range.hasAttribute('data-wa-user-invalid')).to.be.false; - expect(range.hasAttribute('data-wa-user-valid')).to.be.false; + expect(range.hasCustomState('invalid')).to.be.true; + expect(range.hasCustomState('valid')).to.be.false; + expect(range.hasCustomState('user-invalid')).to.be.false; + expect(range.hasCustomState('user-valid')).to.be.false; await clickOnElement(range); await range.updateComplete; range.blur(); await range.updateComplete; - expect(range.hasAttribute('data-wa-user-invalid')).to.be.true; - expect(range.hasAttribute('data-wa-user-valid')).to.be.false; + expect(range.hasCustomState('user-invalid')).to.be.true; + expect(range.hasCustomState('user-valid')).to.be.false; }); it('should receive validation attributes ("states") even when novalidate is used on the parent form', async () => { @@ -185,10 +185,10 @@ describe('', () => { range.setCustomValidity('Invalid value'); await range.updateComplete; - expect(range.hasAttribute('data-wa-invalid')).to.be.true; - expect(range.hasAttribute('data-wa-valid')).to.be.false; - expect(range.hasAttribute('data-wa-user-invalid')).to.be.false; - expect(range.hasAttribute('data-wa-user-valid')).to.be.false; + expect(range.hasCustomState('invalid')).to.be.true; + expect(range.hasCustomState('valid')).to.be.false; + expect(range.hasCustomState('user-invalid')).to.be.false; + expect(range.hasCustomState('user-valid')).to.be.false; }); it('should be present in form data when using the form attribute and located outside of a
', async () => { From 1855d1b809075d42ed9d10a03b799054352d760d Mon Sep 17 00:00:00 2001 From: Cory LaViska Date: Tue, 17 Dec 2024 11:30:19 -0500 Subject: [PATCH 11/22] update tests to use :state() --- src/internal/test/form-control-base-tests.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/internal/test/form-control-base-tests.ts b/src/internal/test/form-control-base-tests.ts index ef793261f..9ff8f7893 100644 --- a/src/internal/test/form-control-base-tests.ts +++ b/src/internal/test/form-control-base-tests.ts @@ -184,7 +184,7 @@ function runAllValidityTests( control.customError = 'MyError'; await control.updateComplete; expect(control.validity.valid).to.equal(false); - expect(control.hasAttribute('data-wa-invalid')).to.equal(true); + expect(control.hasCustomState('invalid')).to.equal(true); expect(control.validationMessage).to.equal('MyError'); }); @@ -193,7 +193,7 @@ function runAllValidityTests( // expect(control.validity.valid).to.equal(true) control.setAttribute('custom-error', 'MyError'); await control.updateComplete; - expect(control.hasAttribute('data-wa-invalid')).to.equal(true); + expect(control.hasCustomState('invalid')).to.equal(true); expect(control.validationMessage).to.equal('MyError'); }); @@ -207,7 +207,7 @@ function runAllValidityTests( expect(control.disabled).to.equal(true); // expect(control.hasAttribute("disabled")).to.equal(false) expect(control.matches(':disabled')).to.equal(true); - expect(control.hasAttribute('data-wa-disabled')).to.equal(true); + expect(control.hasCustomState('disabled')).to.equal(true); fieldset.disabled = false; @@ -215,7 +215,7 @@ function runAllValidityTests( expect(control.disabled).to.equal(false); expect(control.hasAttribute('disabled')).to.equal(false); expect(control.matches(':disabled')).to.equal(false); - expect(control.hasAttribute('data-wa-disabled')).to.equal(false); + expect(control.hasCustomState('disabled')).to.equal(false); }); // it("This is the one edge case with ':disabled'. If you disable a fieldset, and then disable the element directly, it will not reflect the disabled attribute.", async () => { @@ -246,7 +246,7 @@ function runAllValidityTests( expect(control.disabled).to.equal(true); expect(control.hasAttribute('disabled')).to.equal(true); expect(control.matches(':disabled')).to.equal(true); - expect(control.hasAttribute('data-wa-disabled')).to.equal(true); + expect(control.hasCustomState('disabled')).to.equal(true); control.disabled = false; await control.updateComplete; @@ -254,7 +254,7 @@ function runAllValidityTests( expect(control.disabled).to.equal(false); expect(control.hasAttribute('disabled')).to.equal(false); expect(control.matches(':disabled')).to.equal(false); - expect(control.hasAttribute('data-wa-disabled')).to.equal(false); + expect(control.hasCustomState('disabled')).to.equal(false); }); } }); From ad36ba55693ac44557b0670a6f1cbd5d5524df7b Mon Sep 17 00:00:00 2001 From: Cory LaViska Date: Tue, 17 Dec 2024 11:34:18 -0500 Subject: [PATCH 12/22] rename part --- docs/docs/resources/changelog.md | 2 ++ src/components/tab-group/tab-group.css | 8 ++++---- src/components/tab-group/tab-group.test.ts | 2 +- src/components/tab-group/tab-group.ts | 12 ++++++------ 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/docs/docs/resources/changelog.md b/docs/docs/resources/changelog.md index 2e51ef074..5afc1362d 100644 --- a/docs/docs/resources/changelog.md +++ b/docs/docs/resources/changelog.md @@ -15,10 +15,12 @@ During the alpha period, things might break! We take breaking changes very serio ## Next - Added `checked` and `disabled` custom states to `` and `` +scroll-button-end - Removed stateful CSS parts in favor of custom states - ``: `control--checked`, `control--indeterminate` - ``: `control--checked` - ``: `item--disabled`, `item--expanded`, `item--indeterminate`, `item--selected` +- Renamed the `scroll-button--start` and `scroll-button--end` parts to `scroll-button-start` and `scroll-button-end` in `` ## 3.0.0-alpha.5 diff --git a/src/components/tab-group/tab-group.css b/src/components/tab-group/tab-group.css index 9ead6fa33..9b40f682a 100644 --- a/src/components/tab-group/tab-group.css +++ b/src/components/tab-group/tab-group.css @@ -40,20 +40,20 @@ width: var(--wa-space-xl); } -.tab-group__scroll-button--start { +.tab-group__scroll-button-start { left: 0; } -.tab-group__scroll-button--end { +.tab-group__scroll-button-end { right: 0; } -.tab-group--rtl .tab-group__scroll-button--start { +.tab-group--rtl .tab-group__scroll-button-start { left: auto; right: 0; } -.tab-group--rtl .tab-group__scroll-button--end { +.tab-group--rtl .tab-group__scroll-button-end { left: 0; right: auto; } diff --git a/src/components/tab-group/tab-group.test.ts b/src/components/tab-group/tab-group.test.ts index 9045afd24..b89ee30a4 100644 --- a/src/components/tab-group/tab-group.test.ts +++ b/src/components/tab-group/tab-group.test.ts @@ -303,7 +303,7 @@ describe('', () => { expect(isElementVisibleFromOverflow(tabGroup, firstTab!)).to.be.true; expect(isElementVisibleFromOverflow(tabGroup, lastTab!)).to.be.false; - const scrollToRightButton = tabGroup.shadowRoot?.querySelector('wa-icon-button[part*="scroll-button--end"]'); + const scrollToRightButton = tabGroup.shadowRoot?.querySelector('wa-icon-button[part*="scroll-button-end"]'); expect(scrollToRightButton).not.to.be.null; await clickOnElement(scrollToRightButton!); diff --git a/src/components/tab-group/tab-group.ts b/src/components/tab-group/tab-group.ts index f258d5c7f..172989fae 100644 --- a/src/components/tab-group/tab-group.ts +++ b/src/components/tab-group/tab-group.ts @@ -36,8 +36,8 @@ import styles from './tab-group.css'; * @csspart tabs - The container that wraps the tabs. * @csspart body - The tab group's body where tab panels are slotted in. * @csspart scroll-button - The previous/next scroll buttons that show when tabs are scrollable, an ``. - * @csspart scroll-button--start - The starting scroll button. - * @csspart scroll-button--end - The ending scroll button. + * @csspart scroll-button-start - The starting scroll button. + * @csspart scroll-button-end - The ending scroll button. * @csspart scroll-button__base - The scroll button's exported `base` part. * * @cssproperty --indicator-color - The color of the active tab indicator. @@ -392,9 +392,9 @@ export default class WaTabGroup extends WebAwesomeElement { ${this.hasScrollControls ? html` Date: Tue, 17 Dec 2024 11:43:51 -0500 Subject: [PATCH 13/22] hoist internals and custom state methods to WebAwesomeElement --- src/internal/webawesome-element.ts | 123 ++++++++++++++--------------- 1 file changed, 60 insertions(+), 63 deletions(-) diff --git a/src/internal/webawesome-element.ts b/src/internal/webawesome-element.ts index 16bc4aa17..ea6b8779e 100644 --- a/src/internal/webawesome-element.ts +++ b/src/internal/webawesome-element.ts @@ -6,6 +6,18 @@ import componentStyles from '../styles/shadow/component.css'; import { CustomErrorValidator } from './validators/custom-error-validator.js'; export default class WebAwesomeElement extends LitElement { + constructor() { + super(); + + try { + this.internals = this.attachInternals(); + } catch (_e) { + /* Need to tell people if they need a polyfill. */ + /* eslint-disable-next-line */ + console.error('Element internals are not supported in your browser. Consider using a polyfill'); + } + } + // Make localization attributes reactive @property() dir: string; @property() lang: string; @@ -40,6 +52,8 @@ export default class WebAwesomeElement extends LitElement { // Store the constructor value of all `static properties = {}` initialReflectedProperties: Map = new Map(); + internals: ElementInternals; + attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) { if (!this.#hasRecordedInitialProperties) { (this.constructor as typeof WebAwesomeElement).elementProperties.forEach( @@ -98,6 +112,52 @@ export default class WebAwesomeElement extends LitElement { throw e; } } + + /** Adds a custom state to the element. */ + addCustomState(state: string) { + try { + (this.internals.states as Set).add(state); + } catch (_) { + // Without this, test suite errors. + } + } + + /** Removes a custom state from the element. */ + deleteCustomState(state: string) { + try { + (this.internals.states as Set).delete(state); + } catch (_) { + // Without this, test suite errors. + } + } + + /** Toggles a custom state on the element. */ + toggleCustomState(state: string, force: boolean) { + if (force) { + this.addCustomState(state); + return; + } + + if (!force) { + this.deleteCustomState(state); + return; + } + + this.toggleCustomState(state, !this.hasCustomState(state)); + } + + /** Determines if the element has the specified custom state. */ + hasCustomState(state: string) { + let bool = false; + + try { + bool = (this.internals.states as Set).has(state); + } catch (_) { + // Without this, test suite errors. + } + + return bool; + } } export interface Validator { @@ -142,12 +202,6 @@ export interface WebAwesomeFormControl extends WebAwesomeElement { reportValidity: () => boolean; setCustomValidity: (message: string) => void; - // Custom state methods - hasCustomState: (state: string) => boolean; - addCustomState: (state: string) => void; - deleteCustomState(state: string): void; - toggleCustomState(state: string, force: boolean): void; - // Form properties hasInteracted: boolean; valueHasChanged?: boolean; @@ -197,9 +251,6 @@ export class WebAwesomeFormAssociatedElement required: boolean = false; - // Form validation methods - internals: ElementInternals; - assumeInteractionOn: string[] = ['wa-input']; // Additional @@ -219,14 +270,6 @@ export class WebAwesomeFormAssociatedElement constructor() { super(); - try { - this.internals = this.attachInternals(); - } catch (_e) { - /* Need to tell people if they need a polyfill. */ - /* eslint-disable-next-line */ - console.error('Element internals are not supported in your browser. Consider using a polyfill'); - } - if (!isServer) { // eslint-disable-next-line this.addEventListener('invalid', this.emitInvalid); @@ -496,50 +539,4 @@ export class WebAwesomeFormAssociatedElement this.setValidity(flags, finalMessage, formControl); } - - /** Adds a custom state to the element. */ - addCustomState(state: string) { - try { - (this.internals.states as Set).add(state); - } catch (_) { - // Without this, test suite errors. - } - } - - /** Removes a custom state from the element. */ - deleteCustomState(state: string) { - try { - (this.internals.states as Set).delete(state); - } catch (_) { - // Without this, test suite errors. - } - } - - /** Toggles a custom state on the element. */ - toggleCustomState(state: string, force: boolean) { - if (force) { - this.addCustomState(state); - return; - } - - if (!force) { - this.deleteCustomState(state); - return; - } - - this.toggleCustomState(state, !this.hasCustomState(state)); - } - - /** Determines if the element has the specified custom state. */ - hasCustomState(state: string) { - let bool = false; - - try { - bool = (this.internals.states as Set).has(state); - } catch (_) { - // Without this, test suite errors. - } - - return bool; - } } From 651c38afbfde24e578c7a2a9f564ea453d0d08e6 Mon Sep 17 00:00:00 2001 From: Cory LaViska Date: Tue, 17 Dec 2024 11:45:45 -0500 Subject: [PATCH 14/22] use states instead of parts --- src/components/tree-item/tree-item.ts | 36 +++++++++++++-------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/components/tree-item/tree-item.ts b/src/components/tree-item/tree-item.ts index 1bc9546b1..fb5549ae2 100644 --- a/src/components/tree-item/tree-item.ts +++ b/src/components/tree-item/tree-item.ts @@ -44,10 +44,6 @@ import styles from './tree-item.css'; * * @csspart base - The component's base wrapper. * @csspart item - The tree item's container. This element wraps everything except slotted tree item children. - * @csspart item--disabled - Applied when the tree item is disabled. - * @csspart item--expanded - Applied when the tree item is expanded. - * @csspart item--indeterminate - Applied when the selection is indeterminate. - * @csspart item--selected - Applied when the tree item is selected. * @csspart indentation - The tree item's indentation container. * @csspart expand-button - The container that wraps the tree item's expand button and spinner. * @csspart spinner - The spinner that shows when a lazy tree item is in the loading state. @@ -57,8 +53,6 @@ import styles from './tree-item.css'; * @csspart checkbox - The checkbox that shows when using multiselect. * @csspart checkbox__base - The checkbox's exported `base` part. * @csspart checkbox__control - The checkbox's exported `control` part. - * @csspart checkbox__control--checked - The checkbox's exported `control--checked` part. - * @csspart checkbox__control--indeterminate - The checkbox's exported `control--indeterminate` part. * @csspart checkbox__checked-icon - The checkbox's exported `checked-icon` part. * @csspart checkbox__indeterminate-icon - The checkbox's exported `indeterminate-icon` part. * @csspart checkbox__label - The checkbox's exported `label` part. @@ -68,6 +62,11 @@ import styles from './tree-item.css'; * @cssproperty --expand-button-color - The color of the expand button. * @cssproperty [--show-duration=200ms] - The animation duration when expanding tree items. * @cssproperty [--hide-duration=200ms] - The animation duration when collapsing tree items. + * + * @cssstate disabled - Applied when the tree item is disabled. + * @cssstate expanded - Applied when the tree item is expanded. + * @cssstate indeterminate - Applied when the selection is indeterminate. + * @cssstate selected - Applied when the tree item is selected. */ @customElement('wa-tree-item') export default class WaTreeItem extends WebAwesomeElement { @@ -189,11 +188,23 @@ export default class WaTreeItem extends WebAwesomeElement { @watch('disabled') handleDisabledChange() { + this.toggleCustomState('disabled', this.disabled); this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false'); } + @watch('expanded') + handleExpandedState() { + this.toggleCustomState('expanded', this.expanded); + } + + @watch('indeterminate') + handleIndeterminateStateChange() { + this.toggleCustomState('indeterminate', this.indeterminate); + } + @watch('selected') handleSelectedChange() { + this.toggleCustomState('selected', this.selected); this.setAttribute('aria-selected', this.selected ? 'true' : 'false'); } @@ -251,16 +262,7 @@ export default class WaTreeItem extends WebAwesomeElement { 'tree-item--rtl': this.localize.dir() === 'rtl', })}" > -
+