fix morphing (#172)

* fix morphing

* fix morphing

* prettier

* fix morphing

* fix morphing

* fix morphing

* prettier

* add morphing tests

* prettier

* fix tests for reportValidity

* fix tests for reportValidity

* fix tests for reportValidity

* try CI now

* prettier
This commit is contained in:
Konnor Rogers
2024-09-17 15:33:29 -04:00
committed by GitHub
parent 25944d8d7d
commit 9339f87ead
5 changed files with 167 additions and 72 deletions

View File

@@ -5,7 +5,7 @@ layout: component
---
```html {.example}
<wa-color-picker label="Select a color"></wa-color-picker>
<wa-color-picker label="Select a color" required></wa-color-picker>
```
:::info

View File

@@ -8,33 +8,29 @@ import type WaButton from './button.js';
const variants = ['brand', 'success', 'neutral', 'warning', 'danger'];
describe('<wa-button>', () => {
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('<wa-button>', () => {
});
});
describe('when an attribute is removed', () => {
it("should return to 'neutral' when attribute removed with no initial attribute", async () => {
const el = await fixture<WaButton>(html`<wa-button>Button label</wa-button>`);
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<WaButton>(html`<wa-button variant="primary">Button label</wa-button>`);
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<WaButton>(html`<wa-button>Button label</wa-button>`);
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<WaButton>(html`<wa-button variant="primary">Button label</wa-button>`);
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<WaButton>(html` <wa-button>Button Label</wa-button> `);

View File

@@ -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) {

View File

@@ -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<T extends WebAwesomeFormControl = WebAwe
runAllValidityTests(tagName, displayName, renderControl);
}
function preventSubmit(e: SubmitEvent) {
e.preventDefault(); // => 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<T extends WebAwesomeFormControl = WebAwesomeFormContr
// Runs an action while listening for emitted events of a given type. Returns an array of all events of the given type
// that have been been emitted while the action was running.
function checkEventEmissions(control: WebAwesomeFormControl, eventType: string, action: () => void): Event[] {
function checkEventEmissions(control: WebAwesomeFormControl, eventType: string, action: () => void): Promise<Event[]> {
const emittedEvents: Event[] = [];
const eventHandler = (event: Event) => {
emittedEvents.push(event);
};
try {
control.addEventListener(eventType, eventHandler);
action();
} finally {
control.removeEventListener(eventType, eventHandler);
}
return new Promise<Event[]>(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

View File

@@ -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<string, unknown> = 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<LitElement['willUpdate']>[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<string, unknown>)[prop] = value;
}
});
}
protected firstUpdated(changedProperties: Parameters<LitElement['firstUpdated']>[0]): void {
super.firstUpdated(changedProperties);