diff --git a/docs/docs/components/color-picker.md b/docs/docs/components/color-picker.md index 3b0f05184..0fcb26d19 100644 --- a/docs/docs/components/color-picker.md +++ b/docs/docs/components/color-picker.md @@ -5,7 +5,7 @@ layout: component --- ```html {.example} - + ``` :::info diff --git a/src/components/button/button.test.ts b/src/components/button/button.test.ts index 5e2eed396..55d9030c3 100644 --- a/src/components/button/button.test.ts +++ b/src/components/button/button.test.ts @@ -8,33 +8,29 @@ import type WaButton from './button.js'; const variants = ['brand', 'success', 'neutral', 'warning', 'danger']; describe('', () => { - it('form control base tests', async () => { - await Promise.allSettled([ - runFormControlBaseTests({ - tagName: 'wa-button', - variantName: 'type="button"', + runFormControlBaseTests({ + tagName: 'wa-button', + variantName: 'type="button"', - init: (control: WaButton) => { - control.type = 'button'; - } - }), - runFormControlBaseTests({ - tagName: 'wa-button', - variantName: 'type="submit"', + init: (control: WaButton) => { + control.type = 'button'; + } + }); + runFormControlBaseTests({ + tagName: 'wa-button', + variantName: 'type="submit"', - init: (control: WaButton) => { - control.type = 'submit'; - } - }), - runFormControlBaseTests({ - tagName: 'wa-button', - variantName: 'href="xyz"', + init: (control: WaButton) => { + control.type = 'submit'; + } + }); + runFormControlBaseTests({ + tagName: 'wa-button', + variantName: 'href="xyz"', - init: (control: WaButton) => { - control.href = 'some-url'; - } - }) - ]); + init: (control: WaButton) => { + control.href = 'some-url'; + } }); for (const fixture of fixtures) { @@ -48,6 +44,64 @@ describe('', () => { }); }); + describe('when an attribute is removed', () => { + it("should return to 'neutral' when attribute removed with no initial attribute", async () => { + const el = await fixture(html`Button label`); + + expect(el.variant).to.equal('neutral'); + expect(el.getAttribute('variant')).to.equal('neutral'); + + el.removeAttribute('variant'); + await el.updateComplete; + + expect(el.variant).to.equal('neutral'); + expect(el.getAttribute('variant')).to.equal('neutral'); + }); + + it("should return to 'neutral' when attribute removed with an initial attribute", async () => { + const el = await fixture(html`Button label`); + + expect(el.variant).to.equal('primary'); + expect(el.getAttribute('variant')).to.equal('primary'); + + el.removeAttribute('variant'); + await el.updateComplete; + + expect(el.variant).to.equal('neutral'); + expect(el.getAttribute('variant')).to.equal('neutral'); + }); + }); + + describe('when a property is set to null', () => { + it("should return to 'default' when property set to null with no initial attribute", async () => { + const el = await fixture(html`Button label`); + + expect(el.variant).to.equal('neutral'); + expect(el.getAttribute('variant')).to.equal('neutral'); + + // @ts-expect-error Its a test. Stop. + el.variant = null; + await el.updateComplete; + + expect(el.variant).to.equal('neutral'); + expect(el.getAttribute('variant')).to.equal('neutral'); + }); + + it("should return to 'default' when property set to null with an initial attribute", async () => { + const el = await fixture(html`Button label`); + + expect(el.variant).to.equal('primary'); + expect(el.getAttribute('variant')).to.equal('primary'); + + // @ts-expect-error Its a test. Stop. + el.variant = null; + await el.updateComplete; + + expect(el.variant).to.equal('neutral'); + expect(el.getAttribute('variant')).to.equal('neutral'); + }); + }); + describe('when provided no parameters', () => { it('passes accessibility test', async () => { const el = await fixture(html` Button Label `); diff --git a/src/components/color-picker/color-picker.ts b/src/components/color-picker/color-picker.ts index 45c0440e9..f30f0c430 100644 --- a/src/components/color-picker/color-picker.ts +++ b/src/components/color-picker/color-picker.ts @@ -2,8 +2,8 @@ import '../button-group/button-group.js'; import '../button/button.js'; import '../dropdown/dropdown.js'; import '../icon/icon.js'; -// import '../input/input.js'; -// import '../visually-hidden/visually-hidden.js'; +import '../input/input.js'; +import '../visually-hidden/visually-hidden.js'; import { clamp } from '../../internal/math.js'; import { classMap } from 'lit/directives/class-map.js'; import { customElement, eventOptions, property, query, state } from 'lit/decorators.js'; @@ -830,12 +830,21 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement { } } + private reportValidityAfterShow = () => { + // Remove the event so we dont emit "wa-invalid" twice + this.removeEventListener('invalid', this.emitInvalid); + + this.reportValidity(); + + this.addEventListener('invalid', this.emitInvalid); + }; + /** Checks for validity and shows the browser's validation message if the control is invalid. */ reportValidity() { // This won't get called when a form is submitted. This is only for manual calls. if (!this.validity.valid && !this.dropdown.open) { // Show the dropdown so the browser can focus on it - this.addEventListener('wa-after-show', () => this.reportValidity(), { once: true }); + this.addEventListener('wa-after-show', this.reportValidityAfterShow, { once: true }); this.dropdown.show(); if (!this.disabled) { diff --git a/src/internal/test/form-control-base-tests.ts b/src/internal/test/form-control-base-tests.ts index 161016002..a25dd5b50 100644 --- a/src/internal/test/form-control-base-tests.ts +++ b/src/internal/test/form-control-base-tests.ts @@ -1,7 +1,7 @@ import { aTimeout, expect } from '@open-wc/testing'; +import { clickOnElement } from '../test.js'; import { fixtures } from './fixture.js'; import { html, type TemplateResult } from 'lit'; -import { resetMouse } from '@web/test-runner-commands'; import { html as staticHTML, unsafeStatic } from 'lit/static-html.js'; import type { clientFixture, hydratedFixture } from './fixture.js'; import type { WebAwesomeFormControl } from '../webawesome-element.js'; @@ -43,10 +43,6 @@ export function runFormControlBaseTests stop accidental navigation from breaking the page. -} - // // Applicable for all Web Awesome form controls. This function checks the behavior of: // - `.validity` @@ -65,26 +61,6 @@ function runAllValidityTests( // This needs to be outside the describe block other wise everything breaks because "describe" blocks cannot be async. // https://github.com/mochajs/mocha/issues/2116 describe(`Form validity base test for ${displayName}`, () => { - beforeEach(async () => { - document.addEventListener('submit', preventSubmit); - try { - await aTimeout(10); - await resetMouse(); - } catch (_e) { - // leave me alone eslint. - } - }); - // This is silly,but it fixes an issue with `reportValidity()` causing WebKit to crash. - afterEach(async () => { - document.removeEventListener('submit', preventSubmit); - try { - await aTimeout(10); - await resetMouse(); - } catch (_e) { - // leave me alone eslint. - } - }); - for (const fixture of fixtures) { describe(`with ${fixture.type} rendering`, () => { const createControl = renderControl(fixture); @@ -142,17 +118,21 @@ function runAllValidityTests( it('should make sure that calling `.reportValidity()` will return `true` when valid', async () => { const control = await createControl(); expect(control.reportValidity()).to.equal(true); + + // This is silly,but it fixes an issue with `reportValidity()` causing WebKit to crash. + await clickOnElement(document.body); + await aTimeout(100); }); it('should not emit an `wa-invalid` event when `.checkValidity()` is called while valid', async () => { const control = await createControl(); - const emittedEvents = checkEventEmissions(control, 'wa-invalid', () => control.checkValidity()); + const emittedEvents = await checkEventEmissions(control, 'wa-invalid', () => control.checkValidity()); expect(emittedEvents.length).to.equal(0); }); it('should not emit an `wa-invalid` event when `.reportValidity()` is called while valid', async () => { const control = await createControl(); - const emittedEvents = checkEventEmissions(control, 'wa-invalid', () => control.reportValidity()); + const emittedEvents = await checkEventEmissions(control, 'wa-invalid', () => control.reportValidity()); expect(emittedEvents.length).to.equal(0); }); @@ -164,7 +144,7 @@ function runAllValidityTests( control.setCustomValidity('error'); control.disabled = true; await control.updateComplete; - const emittedEvents = checkEventEmissions(control, 'wa-invalid', () => control.checkValidity()); + const emittedEvents = await checkEventEmissions(control, 'wa-invalid', () => control.checkValidity()); expect(emittedEvents.length).to.equal(0); }); @@ -173,7 +153,7 @@ function runAllValidityTests( control.setCustomValidity('error'); control.disabled = true; await control.updateComplete; - const emittedEvents = checkEventEmissions(control, 'wa-invalid', () => control.reportValidity()); + const emittedEvents = await checkEventEmissions(control, 'wa-invalid', () => control.reportValidity()); expect(emittedEvents.length).to.equal(0); }); @@ -296,12 +276,18 @@ function runSpecialTests_slButtonOfTypeButton(createControl: CreateControlFn) { const control = await createControl(); control.setCustomValidity('error'); expect(control.checkValidity()).to.equal(false); + // This is silly,but it fixes an issue with `reportValidity()` causing WebKit to crash. + await clickOnElement(document.body); + await aTimeout(100); }); it('should make sure that calling `.reportValidity()` will still return `true` when custom error has been set', async () => { const control = await createControl(); control.setCustomValidity('error'); expect(control.reportValidity()).to.equal(false); + // This is silly,but it fixes an issue with `reportValidity()` causing WebKit to crash. + await clickOnElement(document.body); + await aTimeout(100); }); it('should emit an `wa-invalid` event when `.checkValidity()` is called in custom error case, and not disabled', async () => { @@ -309,7 +295,7 @@ function runSpecialTests_slButtonOfTypeButton(createControl: CreateControlFn) { control.setCustomValidity('error'); control.disabled = false; await control.updateComplete; - const emittedEvents = checkEventEmissions(control, 'wa-invalid', () => control.checkValidity()); + const emittedEvents = await checkEventEmissions(control, 'wa-invalid', () => control.checkValidity()); expect(emittedEvents.length).to.equal(1); }); @@ -318,8 +304,7 @@ function runSpecialTests_slButtonOfTypeButton(createControl: CreateControlFn) { control.setCustomValidity('error'); control.disabled = false; await control.updateComplete; - const emittedEvents = checkEventEmissions(control, 'wa-invalid', () => control.reportValidity()); - + const emittedEvents = await checkEventEmissions(control, 'wa-invalid', () => control.reportValidity()); expect(emittedEvents.length).to.equal(1); }); } @@ -338,13 +323,16 @@ function runSpecialTests_slButtonWithHref(createControl: CreateControlFn) { const control = await createControl(); control.setCustomValidity('error'); expect(control.reportValidity()).to.equal(false); + // This is silly,but it fixes an issue with `reportValidity()` causing WebKit to crash. + await clickOnElement(document.body); + await aTimeout(100); }); it('should emit an `wa-invalid` event when `.checkValidity()` is called in custom error case', async () => { const control = await createControl(); control.setCustomValidity('error'); await control.updateComplete; - const emittedEvents = checkEventEmissions(control, 'wa-invalid', () => control.checkValidity()); + const emittedEvents = await checkEventEmissions(control, 'wa-invalid', () => control.checkValidity()); expect(emittedEvents.length).to.equal(1); }); @@ -352,7 +340,7 @@ function runSpecialTests_slButtonWithHref(createControl: CreateControlFn) { const control = await createControl(); control.setCustomValidity('error'); await control.updateComplete; - const emittedEvents = checkEventEmissions(control, 'wa-invalid', () => control.reportValidity()); + const emittedEvents = await checkEventEmissions(control, 'wa-invalid', () => control.reportValidity()); expect(emittedEvents.length).to.equal(1); }); } @@ -377,6 +365,9 @@ function runSpecialTests_standard(createControl: CreateControlFn) { const control = await createControl(); control.setCustomValidity('error'); expect(control.reportValidity()).to.equal(false); + // This is silly,but it fixes an issue with `reportValidity()` causing WebKit to crash. + await clickOnElement(document.body); + await aTimeout(100); }); it('should emit an `wa-invalid` event when `.checkValidity()` is called in custom error case and not disabled', async () => { @@ -384,7 +375,7 @@ function runSpecialTests_standard(createControl: CreateControlFn) { control.setCustomValidity('error'); control.disabled = false; await control.updateComplete; - const emittedEvents = checkEventEmissions(control, 'wa-invalid', () => control.checkValidity()); + const emittedEvents = await checkEventEmissions(control, 'wa-invalid', () => control.checkValidity()); expect(emittedEvents.length).to.equal(1); }); @@ -393,7 +384,7 @@ function runSpecialTests_standard(createControl: CreateControlFn) { control.setCustomValidity('error'); control.disabled = false; await control.updateComplete; - const emittedEvents = checkEventEmissions(control, 'wa-invalid', () => control.reportValidity()); + const emittedEvents = await checkEventEmissions(control, 'wa-invalid', () => control.reportValidity()); expect(emittedEvents.length).to.equal(1); }); @@ -416,21 +407,26 @@ function createFormControl void): Event[] { +function checkEventEmissions(control: WebAwesomeFormControl, eventType: string, action: () => void): Promise { const emittedEvents: Event[] = []; const eventHandler = (event: Event) => { emittedEvents.push(event); }; - try { - control.addEventListener(eventType, eventHandler); - action(); - } finally { - control.removeEventListener(eventType, eventHandler); - } + return new Promise(resolve => { + (async () => { + try { + control.addEventListener(eventType, eventHandler); + action(); + await aTimeout(300); + } finally { + control.removeEventListener(eventType, eventHandler); + } - return emittedEvents; + resolve(emittedEvents); + })(); + }); } // Component `wa-button` behaves quite different to the other components. To keep things simple we use simple conditions diff --git a/src/internal/webawesome-element.ts b/src/internal/webawesome-element.ts index a557b4c3b..6188a7444 100644 --- a/src/internal/webawesome-element.ts +++ b/src/internal/webawesome-element.ts @@ -10,6 +10,42 @@ export default class WebAwesomeElement extends LitElement { @property({ type: Boolean, reflect: true, attribute: 'did-ssr' }) didSSR = isServer || Boolean(this.shadowRoot); + #hasRecordedInitialProperties = false; + + // Store the constructor value of all `static properties = {}` + initialReflectedProperties: Map = new Map(); + + attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) { + if (!this.#hasRecordedInitialProperties) { + (this.constructor as typeof WebAwesomeElement).elementProperties.forEach( + (obj, prop: keyof typeof this & string) => { + // eslint-disable-next-line + if (obj.reflect && this[prop] != null) { + this.initialReflectedProperties.set(prop, this[prop]); + } + } + ); + + this.#hasRecordedInitialProperties = true; + } + + super.attributeChangedCallback(name, oldValue, newValue); + } + + protected willUpdate(changedProperties: Parameters[0]): void { + super.willUpdate(changedProperties); + + // Run the morph fixing *after* willUpdate. + this.initialReflectedProperties.forEach((value, prop: string & keyof typeof this) => { + // If a prop changes to `null`, we assume this happens via an attribute changing to `null`. + // eslint-disable-next-line + if (changedProperties.has(prop) && this[prop] == null) { + // Silly type gymnastics to appease the compiler. + (this as Record)[prop] = value; + } + }); + } + protected firstUpdated(changedProperties: Parameters[0]): void { super.firstUpdated(changedProperties);