mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-12 04:09:12 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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> `);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user