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);