refactor styles and simplify custom states (#1016)

This commit is contained in:
Cory LaViska
2025-06-04 08:09:14 -04:00
committed by GitHub
parent 5980b5f843
commit f8dca5d1a8
69 changed files with 291 additions and 310 deletions

View File

@@ -21,7 +21,7 @@ import styles from './{{ tagWithoutPrefix tag }}.css';
*/
@customElement("{{ tag }}")
export default class {{ properCase tag }} extends WebAwesomeElement {
static shadowStyle = styles;
static css = styles;
/** An example attribute. */
@property() attr = 'example';

View File

@@ -28,7 +28,7 @@ import styles from './animated-image.css';
*/
@customElement('wa-animated-image')
export default class WaAnimatedImage extends WebAwesomeElement {
static shadowStyle = styles;
static css = styles;
@query('.animated') animatedImage: HTMLImageElement;

View File

@@ -23,7 +23,7 @@ import { animations } from './animations.js';
*/
@customElement('wa-animation')
export default class WaAnimation extends WebAwesomeElement {
static shadowStyle = styles;
static css = styles;
private animation?: Animation;
private hasStarted = false;

View File

@@ -29,7 +29,7 @@ import styles from './avatar.css';
*/
@customElement('wa-avatar')
export default class WaAvatar extends WebAwesomeElement {
static shadowStyle = styles;
static css = styles;
@state() private hasError = false;

View File

@@ -21,7 +21,7 @@ import styles from './badge.css';
*/
@customElement('wa-badge')
export default class WaBadge extends WebAwesomeElement {
static shadowStyle = [variantStyles, appearanceStyles, styles];
static css = [variantStyles, appearanceStyles, styles];
/** The badge's theme variant. Defaults to `brand` if not within another element with a variant. */
@property({ reflect: true }) variant: 'brand' | 'neutral' | 'success' | 'warning' | 'danger' = 'brand';

View File

@@ -24,7 +24,7 @@ import styles from './breadcrumb-item.css';
*/
@customElement('wa-breadcrumb-item')
export default class WaBreadcrumbItem extends WebAwesomeElement {
static shadowStyle = styles;
static css = styles;
@query('slot:not([name])') defaultSlot: HTMLSlotElement;

View File

@@ -21,7 +21,7 @@ import styles from './breadcrumb.css';
*/
@customElement('wa-breadcrumb')
export default class WaBreadcrumb extends WebAwesomeElement {
static shadowStyle = styles;
static css = styles;
private readonly localize = new LocalizeController(this);
private separatorDir = this.localize.dir();

View File

@@ -20,7 +20,7 @@ import styles from './button-group.css';
*/
@customElement('wa-button-group')
export default class WaButtonGroup extends WebAwesomeElement {
static shadowStyle = [sizeStyles, variantStyles, styles];
static css = [sizeStyles, variantStyles, styles];
@query('slot') defaultSlot: HTMLSlotElement;

View File

@@ -53,7 +53,7 @@ import styles from './button.css';
*/
@customElement('wa-button')
export default class WaButton extends WebAwesomeFormAssociatedElement {
static shadowStyle = [styles, variantStyles, sizeStyles, appearanceStyles];
static css = [styles, variantStyles, sizeStyles, appearanceStyles];
static get validators() {
return [...super.validators, MirrorValidator()];

View File

@@ -24,7 +24,7 @@ import styles from './callout.css';
*/
@customElement('wa-callout')
export default class WaCallout extends WebAwesomeElement {
static shadowStyle = [variantStyles, appearanceStyles, sizeStyles, styles];
static css = [variantStyles, appearanceStyles, sizeStyles, styles];
/** The callout's theme variant. Defaults to `brand` if not within another element with a variant. */
@property({ reflect: true }) variant: 'brand' | 'neutral' | 'success' | 'warning' | 'danger' | 'brand' = 'brand';

View File

@@ -30,7 +30,7 @@ import styles from './card.css';
*/
@customElement('wa-card')
export default class WaCard extends WebAwesomeElement {
static shadowStyle = [sizeStyles, appearanceStyles, styles];
static css = [sizeStyles, appearanceStyles, styles];
private readonly hasSlotController = new HasSlotController(this, 'footer', 'header', 'media');

View File

@@ -16,7 +16,7 @@ import styles from './carousel-item.css';
*/
@customElement('wa-carousel-item')
export default class WaCarouselItem extends WebAwesomeElement {
static shadowStyle = styles;
static css = styles;
connectedCallback() {
super.connectedCallback();

View File

@@ -52,7 +52,7 @@ import styles from './carousel.css';
*/
@customElement('wa-carousel')
export default class WaCarousel extends WebAwesomeElement {
static shadowStyle = styles;
static css = styles;
/** When set, allows the user to navigate the carousel in the same direction indefinitely. */
@property({ type: Boolean, reflect: true }) loop = false;

View File

@@ -199,17 +199,17 @@ describe('<wa-checkbox>', () => {
expect(checkbox.checkValidity()).to.be.false;
expect(checkbox.checkValidity()).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;
expect(checkbox.customStates.has('invalid')).to.be.true;
expect(checkbox.customStates.has('valid')).to.be.false;
expect(checkbox.customStates.has('user-invalid')).to.be.true;
expect(checkbox.customStates.has('user-valid')).to.be.false;
await clickOnElement(checkbox);
await checkbox.updateComplete;
await aTimeout(0);
expect(checkbox.hasCustomState('user-invalid')).to.be.true;
expect(checkbox.hasCustomState('user-valid')).to.be.false;
expect(checkbox.customStates.has('user-invalid')).to.be.true;
expect(checkbox.customStates.has('user-valid')).to.be.false;
});
it('should be invalid when required and unchecked', async () => {
@@ -244,12 +244,12 @@ describe('<wa-checkbox>', () => {
`);
const checkbox = el.querySelector<WaCheckbox>('wa-checkbox')!;
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;
expect(checkbox.customStates.has('required')).to.be.true;
expect(checkbox.customStates.has('optional')).to.be.false;
expect(checkbox.customStates.has('invalid')).to.be.true;
expect(checkbox.customStates.has('valid')).to.be.false;
expect(checkbox.customStates.has('user-invalid')).to.be.false;
expect(checkbox.customStates.has('user-valid')).to.be.false;
});
});

View File

@@ -55,7 +55,7 @@ import styles from './checkbox.css';
*/
@customElement('wa-checkbox')
export default class WaCheckbox extends WebAwesomeFormAssociatedElement {
static shadowStyle = [formControlStyles, sizeStyles, styles];
static css = [formControlStyles, sizeStyles, styles];
static shadowRootOptions = { ...WebAwesomeFormAssociatedElement.shadowRootOptions, delegatesFocus: true };
@@ -156,14 +156,14 @@ export default class WaCheckbox extends WebAwesomeFormAssociatedElement {
this.input.indeterminate = this.indeterminate; // force a sync update
}
this.toggleCustomState('checked', this.checked);
this.toggleCustomState('indeterminate', this.indeterminate);
this.customStates.set('checked', this.checked);
this.customStates.set('indeterminate', this.indeterminate);
this.updateValidity();
}
@watch('disabled')
handleDisabledChange() {
this.toggleCustomState('disabled', this.disabled);
this.customStates.set('disabled', this.disabled);
}
protected willUpdate(changedProperties: PropertyValues<this>): void {

View File

@@ -501,12 +501,12 @@ describe('<wa-color-picker>', () => {
const grid = el.shadowRoot!.querySelector('[part~="grid"]')!;
expect(el.checkValidity()).to.be.true;
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;
expect(el.customStates.has('required')).to.be.true;
expect(el.customStates.has('optional')).to.be.false;
expect(el.customStates.has('invalid')).to.be.false;
expect(el.customStates.has('valid')).to.be.true;
expect(el.customStates.has('user-invalid')).to.be.false;
expect(el.customStates.has('user-valid')).to.be.false;
await clickOnElement(trigger);
await aTimeout(500);
@@ -514,8 +514,8 @@ describe('<wa-color-picker>', () => {
await el.updateComplete;
expect(el.checkValidity()).to.be.true;
expect(el.hasCustomState('user-invalid')).to.be.false;
expect(el.hasCustomState('user-valid')).to.be.true;
expect(el.customStates.has('user-invalid')).to.be.false;
expect(el.customStates.has('user-valid')).to.be.true;
});
it('should receive the correct validation attributes ("states") when invalid', async () => {
@@ -523,12 +523,12 @@ describe('<wa-color-picker>', () => {
const trigger = el.shadowRoot!.querySelector('[part~="trigger"]')!;
const grid = el.shadowRoot!.querySelector('[part~="grid"]')!;
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;
expect(el.customStates.has('required')).to.be.true;
expect(el.customStates.has('optional')).to.be.false;
expect(el.customStates.has('invalid')).to.be.true;
expect(el.customStates.has('valid')).to.be.false;
expect(el.customStates.has('user-invalid')).to.be.false;
expect(el.customStates.has('user-valid')).to.be.false;
await clickOnElement(trigger);
await aTimeout(500);
@@ -536,8 +536,8 @@ describe('<wa-color-picker>', () => {
await el.updateComplete;
expect(el.checkValidity()).to.be.true;
expect(el.hasCustomState('user-invalid')).to.be.false;
expect(el.hasCustomState('user-valid')).to.be.true;
expect(el.customStates.has('user-invalid')).to.be.false;
expect(el.customStates.has('user-valid')).to.be.true;
});
});
});

View File

@@ -102,7 +102,7 @@ declare const EyeDropper: EyeDropperConstructor;
*/
@customElement('wa-color-picker')
export default class WaColorPicker extends WebAwesomeFormAssociatedElement {
static shadowStyle = [visuallyHidden, sizeStyles, formControlStyles, styles];
static css = [visuallyHidden, sizeStyles, formControlStyles, styles];
static shadowRootOptions = { ...WebAwesomeFormAssociatedElement.shadowRootOptions, delegatesFocus: true };

View File

@@ -38,7 +38,7 @@ import styles from './comparison.css';
*/
@customElement('wa-comparison')
export default class WaComparison extends WebAwesomeElement {
static shadowStyle = styles;
static css = styles;
private readonly localize = new LocalizeController(this);
@@ -55,12 +55,12 @@ export default class WaComparison extends WebAwesomeElement {
drag(this, {
onMove: x => {
this.toggleCustomState('dragging', true);
this.customStates.set('dragging', true);
this.position = parseFloat(clamp((x / width) * 100, 0, 100).toFixed(2));
if (isRtl) this.position = 100 - this.position;
},
onStop: () => {
this.toggleCustomState('dragging', false);
this.customStates.set('dragging', false);
},
initialEvent: event,
});

View File

@@ -44,7 +44,7 @@ import styles from './copy-button.css';
*/
@customElement('wa-copy-button')
export default class WaCopyButton extends WebAwesomeElement {
static shadowStyle = [visuallyHidden, styles];
static css = [visuallyHidden, styles];
private readonly localize = new LocalizeController(this);

View File

@@ -46,7 +46,7 @@ import styles from './details.css';
*/
@customElement('wa-details')
export default class WaDetails extends WebAwesomeElement {
static shadowStyle = [appearanceStyles, styles];
static css = [appearanceStyles, styles];
private detailsObserver: MutationObserver;
private readonly localize = new LocalizeController(this);

View File

@@ -54,7 +54,7 @@ import styles from './dialog.css';
*/
@customElement('wa-dialog')
export default class WaDialog extends WebAwesomeElement {
static shadowStyle = styles;
static css = styles;
private readonly localize = new LocalizeController(this);
private readonly hasSlotController = new HasSlotController(this, 'footer', 'header-actions', 'label');

View File

@@ -15,7 +15,7 @@ import styles from './divider.css';
*/
@customElement('wa-divider')
export default class WaDivider extends WebAwesomeElement {
static shadowStyle = styles;
static css = styles;
/** Sets the divider's orientation. */
@property({ reflect: true }) orientation: 'horizontal' | 'vertical' = 'horizontal';

View File

@@ -59,7 +59,7 @@ import styles from './drawer.css';
*/
@customElement('wa-drawer')
export default class WaDrawer extends WebAwesomeElement {
static shadowStyle = styles;
static css = styles;
private readonly localize = new LocalizeController(this);
private readonly hasSlotController = new HasSlotController(this, 'footer', 'header-actions', 'label');

View File

@@ -44,7 +44,7 @@ import styles from './dropdown.css';
*/
@customElement('wa-dropdown')
export default class WaDropdown extends WebAwesomeElement {
static shadowStyle = [sizeStyles, styles];
static css = [sizeStyles, styles];
@query('.dropdown') popup: WaPopup;
@query('#trigger') trigger: HTMLSlotElement;

View File

@@ -26,7 +26,7 @@ import styles from './icon-button.css';
*/
@customElement('wa-icon-button')
export default class WaIconButton extends WebAwesomeFormAssociatedElement {
static shadowStyle = styles;
static css = styles;
@query('.icon-button') button: HTMLButtonElement | HTMLLinkElement;

View File

@@ -41,7 +41,7 @@ interface IconSource {
*/
@customElement('wa-icon')
export default class WaIcon extends WebAwesomeElement {
static shadowStyle = styles;
static css = styles;
@state() private svg: SVGElement | HTMLTemplateResult | null = null;

View File

@@ -18,7 +18,7 @@ import { requestInclude } from './request.js';
*/
@customElement('wa-include')
export default class WaInclude extends WebAwesomeElement {
static shadowStyle = styles;
static css = styles;
/**
* The location of the HTML file to include. Be sure you trust the content you are including as it will be executed as

View File

@@ -108,12 +108,12 @@ describe('<wa-input>', () => {
const el = await fixture<WaInput>(html` <wa-input required value="a"></wa-input> `);
expect(el.checkValidity()).to.be.true;
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;
expect(el.customStates.has('required')).to.be.true;
expect(el.customStates.has('optional')).to.be.false;
expect(el.customStates.has('invalid')).to.be.false;
expect(el.customStates.has('valid')).to.be.true;
expect(el.customStates.has('user-invalid')).to.be.false;
expect(el.customStates.has('user-valid')).to.be.false;
el.focus();
await el.updateComplete;
@@ -123,19 +123,19 @@ describe('<wa-input>', () => {
await el.updateComplete;
expect(el.checkValidity()).to.be.true;
expect(el.hasCustomState('user-invalid')).to.be.false;
expect(el.hasCustomState('user-valid')).to.be.true;
expect(el.customStates.has('user-invalid')).to.be.false;
expect(el.customStates.has('user-valid')).to.be.true;
});
it('should receive the correct validation attributes ("states") when invalid', async () => {
const el = await fixture<WaInput>(html` <wa-input required></wa-input> `);
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;
expect(el.customStates.has('required')).to.be.true;
expect(el.customStates.has('optional')).to.be.false;
expect(el.customStates.has('invalid')).to.be.true;
expect(el.customStates.has('valid')).to.be.false;
expect(el.customStates.has('user-invalid')).to.be.false;
expect(el.customStates.has('user-valid')).to.be.false;
el.focus();
await el.updateComplete;
@@ -145,20 +145,20 @@ describe('<wa-input>', () => {
el.blur();
await el.updateComplete;
expect(el.hasCustomState('user-invalid')).to.be.true;
expect(el.hasCustomState('user-valid')).to.be.false;
expect(el.customStates.has('user-invalid')).to.be.true;
expect(el.customStates.has('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<HTMLFormElement>(html` <form novalidate><wa-input required></wa-input></form> `);
const input = el.querySelector<WaInput>('wa-input')!;
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;
expect(input.customStates.has('required')).to.be.true;
expect(input.customStates.has('optional')).to.be.false;
expect(input.customStates.has('invalid')).to.be.true;
expect(input.customStates.has('valid')).to.be.false;
expect(input.customStates.has('user-invalid')).to.be.false;
expect(input.customStates.has('user-valid')).to.be.false;
});
});
@@ -215,10 +215,10 @@ describe('<wa-input>', () => {
await input.updateComplete;
expect(input.checkValidity()).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;
expect(input.customStates.has('invalid')).to.be.true;
expect(input.customStates.has('valid')).to.be.false;
expect(input.customStates.has('user-invalid')).to.be.false;
expect(input.customStates.has('user-valid')).to.be.false;
input.focus();
await sendKeys({ type: 'test' });
@@ -226,8 +226,8 @@ describe('<wa-input>', () => {
input.blur();
await input.updateComplete;
expect(input.hasCustomState('user-invalid')).to.be.true;
expect(input.hasCustomState('user-valid')).to.be.false;
expect(input.customStates.has('user-invalid')).to.be.true;
expect(input.customStates.has('user-valid')).to.be.false;
});
it('should be present in form data when using the form attribute and located outside of a <form>', async () => {

View File

@@ -57,7 +57,7 @@ import styles from './input.css';
*/
@customElement('wa-input')
export default class WaInput extends WebAwesomeFormAssociatedElement {
static shadowStyle = [sizeStyles, appearanceStyles, formControlStyles, styles];
static css = [sizeStyles, appearanceStyles, formControlStyles, styles];
static shadowRootOptions = { ...WebAwesomeFormAssociatedElement.shadowRootOptions, delegatesFocus: true };
@@ -300,7 +300,7 @@ export default class WaInput extends WebAwesomeFormAssociatedElement {
super.updated(changedProperties);
if (changedProperties.has('value')) {
this.toggleCustomState('blank', !this.value);
this.customStates.set('blank', !this.value);
}
}

View File

@@ -43,7 +43,7 @@ import { SubmenuController } from './submenu-controller.js';
*/
@customElement('wa-menu-item')
export default class WaMenuItem extends WebAwesomeElement {
static shadowStyle = styles;
static css = styles;
private readonly localize = new LocalizeController(this);
@@ -133,7 +133,7 @@ export default class WaMenuItem extends WebAwesomeElement {
this.dispatchEvent(new Event('slotchange', { bubbles: true, composed: false, cancelable: false }));
}
this.toggleCustomState('has-submenu', this.isSubmenu());
this.customStates.set('has-submenu', this.isSubmenu());
}
private handleHostClick = (event: MouseEvent) => {
@@ -201,7 +201,7 @@ export default class WaMenuItem extends WebAwesomeElement {
render() {
const isRtl = this.hasUpdated ? this.localize.dir() === 'rtl' : this.dir === 'rtl';
const isSubmenuExpanded = this.submenuController.isExpanded();
this.toggleCustomState('submenu-expanded', isSubmenuExpanded);
this.customStates.set('submenu-expanded', isSubmenuExpanded);
this.internals.ariaHasPopup = this.isSubmenu() + '';
this.internals.ariaExpanded = isSubmenuExpanded + '';

View File

@@ -13,7 +13,7 @@ import styles from './menu-label.css';
*/
@customElement('wa-menu-label')
export default class WaMenuLabel extends WebAwesomeElement {
static shadowStyle = styles;
static css = styles;
render() {
return html`<slot></slot>`;

View File

@@ -25,7 +25,7 @@ export interface MenuSelectEventDetail {
*/
@customElement('wa-menu')
export default class WaMenu extends WebAwesomeElement {
static shadowStyle = [sizeStyles, styles];
static css = [sizeStyles, styles];
/** The component's size. */
@property({ reflect: true }) size: 'small' | 'medium' | 'large' = 'medium';

View File

@@ -17,7 +17,7 @@ import styles from './mutation-observer.css';
*/
@customElement('wa-mutation-observer')
export default class WaMutationObserver extends WebAwesomeElement {
static shadowStyle = styles;
static css = styles;
private mutationObserver: MutationObserver;

View File

@@ -35,7 +35,7 @@ import styles from './option.css';
*/
@customElement('wa-option')
export default class WaOption extends WebAwesomeElement {
static shadowStyle = styles;
static css = styles;
// @ts-expect-error - Controller is currently unused
private readonly localize = new LocalizeController(this);
@@ -130,9 +130,9 @@ export default class WaOption extends WebAwesomeElement {
// We need this because Safari doesn't honor :hover styles while dragging
// Test case: https://codepen.io/leaverou/pen/VYZOOjy
if (event.type === 'mouseenter') {
this.toggleCustomState('hover', true);
this.customStates.set('hover', true);
} else if (event.type === 'mouseleave') {
this.toggleCustomState('hover', false);
this.customStates.set('hover', false);
}
};
@@ -145,7 +145,7 @@ export default class WaOption extends WebAwesomeElement {
if (changedProperties.has('selected')) {
this.setAttribute('aria-selected', this.selected ? 'true' : 'false');
this.toggleCustomState('selected', this.selected);
this.customStates.set('selected', this.selected);
}
if (changedProperties.has('value')) {
@@ -165,7 +165,7 @@ export default class WaOption extends WebAwesomeElement {
}
if (changedProperties.has('current')) {
this.toggleCustomState('current', this.current);
this.customStates.set('current', this.current);
}
}

View File

@@ -128,7 +128,7 @@ function toLength(px: number | string): string {
*/
@customElement('wa-page')
export default class WaPage extends WebAwesomeElement {
static shadowStyle = [visuallyHidden, styles];
static css = [visuallyHidden, styles];
private headerResizeObserver = this.slotResizeObserver('header');
private subheaderResizeObserver = this.slotResizeObserver('subheader');

View File

@@ -1,3 +1,4 @@
import type { PropertyValues } from 'lit';
import { html } from 'lit';
import { customElement, property, query, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
@@ -40,10 +41,12 @@ const openPopovers = new Set<WaPopover>();
* @cssproperty [--max-width=25rem] - The maximum width of the popover's body content.
* @cssproperty [--show-duration=100ms] - The speed of the show animation.
* @cssproperty [--hide-duration=100ms] - The speed of the hide animation.
*
* @cssstate open - Applied when the popover is open.
*/
@customElement('wa-popover')
export default class WaPopover extends WebAwesomeElement {
static shadowStyle = styles;
static css = styles;
static dependencies = { 'wa-popup': WaPopup };
@query('dialog') dialog: HTMLDialogElement;
@@ -110,6 +113,12 @@ export default class WaPopover extends WebAwesomeElement {
}
}
updated(changedProperties: PropertyValues<this>) {
if (changedProperties.has('open')) {
this.customStates.set('open', this.open);
}
}
private handleAnchorClick = () => {
// Clicks on the anchor should toggle the popover
this.open = !this.open;

View File

@@ -68,7 +68,7 @@ const SUPPORTS_POPOVER = globalThis?.HTMLElement?.prototype.hasOwnProperty('popo
*/
@customElement('wa-popup')
export default class WaPopup extends WebAwesomeElement {
static shadowStyle = styles;
static css = styles;
private anchorEl: Element | VirtualElement | null;
private cleanup: ReturnType<typeof autoUpdate> | undefined;

View File

@@ -24,7 +24,7 @@ import styles from './progress-bar.css';
*/
@customElement('wa-progress-bar')
export default class WaProgressBar extends WebAwesomeElement {
static shadowStyle = styles;
static css = styles;
private readonly localize = new LocalizeController(this);
/** The current progress as a percentage, 0 to 100. */

View File

@@ -25,7 +25,7 @@ import styles from './progress-ring.css';
*/
@customElement('wa-progress-ring')
export default class WaProgressRing extends WebAwesomeElement {
static shadowStyle = styles;
static css = styles;
private readonly localize = new LocalizeController(this);

View File

@@ -18,7 +18,7 @@ let QrCreator: _QrCreator.default;
*/
@customElement('wa-qr-code')
export default class WaQrCode extends WebAwesomeElement {
static shadowStyle = styles;
static css = styles;
@query('canvas') canvas: HTMLElement;

View File

@@ -99,12 +99,12 @@ describe('<wa-radio-group>', () => {
const secondRadio = radioGroup.querySelectorAll('wa-radio')[1];
expect(radioGroup.checkValidity()).to.be.true;
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;
expect(radioGroup.customStates.has('required')).to.be.true;
expect(radioGroup.customStates.has('optional')).to.be.false;
expect(radioGroup.customStates.has('invalid')).to.be.false;
expect(radioGroup.customStates.has('valid')).to.be.true;
expect(radioGroup.customStates.has('user-invalid')).to.be.false;
expect(radioGroup.customStates.has('user-valid')).to.be.false;
// TODO: Go back to clickOnElement when we can determine why CI is not cleaning up elements.
// await clickOnElement(secondRadio);
@@ -113,8 +113,8 @@ describe('<wa-radio-group>', () => {
await radioGroup.updateComplete
expect(radioGroup.checkValidity()).to.be.true;
expect(radioGroup.hasCustomState('user-invalid')).to.be.false;
expect(radioGroup.hasCustomState('user-valid')).to.be.true;
expect(radioGroup.customStates.has('user-invalid')).to.be.false;
expect(radioGroup.customStates.has('user-valid')).to.be.true;
});
it('should receive the correct validation attributes ("states") when invalid', async () => {
@@ -126,12 +126,12 @@ describe('<wa-radio-group>', () => {
`);
const secondRadio = radioGroup.querySelectorAll('wa-radio')[1];
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;
expect(radioGroup.customStates.has('required')).to.be.true;
expect(radioGroup.customStates.has('optional')).to.be.false;
expect(radioGroup.customStates.has('invalid')).to.be.true;
expect(radioGroup.customStates.has('valid')).to.be.false;
expect(radioGroup.customStates.has('user-invalid')).to.be.false;
expect(radioGroup.customStates.has('user-valid')).to.be.false;
// TODO: Go back to clickOnElement when we can determine why CI is not cleaning up elements.
// await clickOnElement(secondRadio);
@@ -140,8 +140,8 @@ describe('<wa-radio-group>', () => {
radioGroup.value = '';
await radioGroup.updateComplete;
expect(radioGroup.hasCustomState('user-invalid')).to.be.true;
expect(radioGroup.hasCustomState('user-valid')).to.be.false;
expect(radioGroup.customStates.has('user-invalid')).to.be.true;
expect(radioGroup.customStates.has('user-valid')).to.be.false;
});
it('should receive validation attributes ("states") even when novalidate is used on the parent form', async () => {
@@ -155,12 +155,12 @@ describe('<wa-radio-group>', () => {
`);
const radioGroup = el.querySelector<WaRadioGroup>('wa-radio-group')!;
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;
expect(radioGroup.customStates.has('required')).to.be.true;
expect(radioGroup.customStates.has('optional')).to.be.false;
expect(radioGroup.customStates.has('invalid')).to.be.true;
expect(radioGroup.customStates.has('valid')).to.be.false;
expect(radioGroup.customStates.has('user-invalid')).to.be.false;
expect(radioGroup.customStates.has('user-valid')).to.be.false;
});
it('should show a constraint validation error when setCustomValidity() is called', async () => {

View File

@@ -37,7 +37,7 @@ import styles from './radio-group.css';
*/
@customElement('wa-radio-group')
export default class WaRadioGroup extends WebAwesomeFormAssociatedElement {
static shadowStyle = [sizeStyles, formControlStyles, styles];
static css = [sizeStyles, formControlStyles, styles];
static get validators() {
const validators = isServer

View File

@@ -40,7 +40,7 @@ import styles from './radio.css';
*/
@customElement('wa-radio')
export default class WaRadio extends WebAwesomeFormAssociatedElement {
static shadowStyle = [formControlStyles, sizeStyles, styles];
static css = [formControlStyles, sizeStyles, styles];
@state() checked = false;
@@ -86,13 +86,13 @@ export default class WaRadio extends WebAwesomeFormAssociatedElement {
super.updated(changedProperties);
if (changedProperties.has('checked')) {
this.toggleCustomState('checked', this.checked);
this.customStates.set('checked', this.checked);
this.setAttribute('aria-checked', this.checked ? 'true' : 'false');
this.tabIndex = this.checked ? 0 : -1;
}
if (changedProperties.has('disabled')) {
this.toggleCustomState('disabled', this.disabled);
this.customStates.set('disabled', this.disabled);
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
}
}

View File

@@ -33,7 +33,7 @@ import styles from './rating.css';
*/
@customElement('wa-rating')
export default class WaRating extends WebAwesomeElement {
static shadowStyle = [sizeStyles, styles];
static css = [sizeStyles, styles];
private readonly localize = new LocalizeController(this);

View File

@@ -17,7 +17,7 @@ import styles from './resize-observer.css';
*/
@customElement('wa-resize-observer')
export default class WaResizeObserver extends WebAwesomeElement {
static shadowStyle = styles;
static css = styles;
private resizeObserver: ResizeObserver;
private observedElements: HTMLElement[] = [];

View File

@@ -20,7 +20,7 @@ import styles from './scroller.css';
*/
@customElement('wa-scroller')
export default class WaScroller extends WebAwesomeElement {
static shadowStyle = [styles];
static css = [styles];
private readonly localize = new LocalizeController(this);
private resizeObserver = new ResizeObserver(() => this.updateScroll());

View File

@@ -331,12 +331,12 @@ describe('<wa-select>', () => {
const secondOption = el.querySelectorAll('wa-option')[1];
expect(el.checkValidity()).to.be.true;
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;
expect(el.customStates.has('required')).to.be.true;
expect(el.customStates.has('optional')).to.be.false;
expect(el.customStates.has('invalid')).to.be.false;
expect(el.customStates.has('valid')).to.be.true;
expect(el.customStates.has('user-invalid')).to.be.false;
expect(el.customStates.has('user-valid')).to.be.false;
await el.show();
await clickOnElement(secondOption);
@@ -345,8 +345,8 @@ describe('<wa-select>', () => {
await el.updateComplete;
expect(el.checkValidity()).to.be.true;
expect(el.hasCustomState('user-invalid')).to.be.false;
expect(el.hasCustomState('user-valid')).to.be.true;
expect(el.customStates.has('user-invalid')).to.be.false;
expect(el.customStates.has('user-valid')).to.be.true;
});
it('should receive the correct validation attributes ("states") when invalid', async () => {
@@ -359,12 +359,12 @@ describe('<wa-select>', () => {
`);
const secondOption = el.querySelectorAll('wa-option')[1];
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;
expect(el.customStates.has('required')).to.be.true;
expect(el.customStates.has('optional')).to.be.false;
expect(el.customStates.has('invalid')).to.be.true;
expect(el.customStates.has('valid')).to.be.false;
expect(el.customStates.has('user-invalid')).to.be.false;
expect(el.customStates.has('user-valid')).to.be.false;
await el.show();
await clickOnElement(secondOption);
@@ -373,8 +373,8 @@ describe('<wa-select>', () => {
el.blur();
await el.updateComplete;
expect(el.hasCustomState('user-invalid')).to.be.true;
expect(el.hasCustomState('user-valid')).to.be.false;
expect(el.customStates.has('user-invalid')).to.be.true;
expect(el.customStates.has('user-valid')).to.be.false;
});
it('should receive validation attributes ("states") even when novalidate is used on the parent form', async () => {
@@ -389,12 +389,12 @@ describe('<wa-select>', () => {
`);
const select = el.querySelector<WaSelect>('wa-select')!;
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;
expect(select.customStates.has('required')).to.be.true;
expect(select.customStates.has('optional')).to.be.false;
expect(select.customStates.has('invalid')).to.be.true;
expect(select.customStates.has('valid')).to.be.false;
expect(select.customStates.has('user-invalid')).to.be.false;
expect(select.customStates.has('user-valid')).to.be.false;
});
});

View File

@@ -85,7 +85,7 @@ import styles from './select.css';
*/
@customElement('wa-select')
export default class WaSelect extends WebAwesomeFormAssociatedElement {
static shadowStyle = [appearanceStyles, formControlStyles, sizeStyles, styles];
static css = [appearanceStyles, formControlStyles, sizeStyles, styles];
static get validators() {
const validators = isServer
@@ -740,7 +740,7 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
super.updated(changedProperties);
if (changedProperties.has('value')) {
this.toggleCustomState('blank', !this.value);
this.customStates.set('blank', !this.value);
}
}

View File

@@ -17,7 +17,7 @@ import styles from './skeleton.css';
*/
@customElement('wa-skeleton')
export default class WaSkeleton extends WebAwesomeElement {
static shadowStyle = styles;
static css = styles;
/** Determines which effect the skeleton will use. */
@property({ reflect: true }) effect: 'pulse' | 'sheen' | 'none' = 'none';

View File

@@ -133,18 +133,18 @@ describe('<wa-slider>', () => {
await slider.updateComplete;
expect(slider.checkValidity()).to.be.false;
expect(slider.hasCustomState('invalid')).to.be.true;
expect(slider.hasCustomState('valid')).to.be.false;
expect(slider.hasCustomState('user-invalid')).to.be.false;
expect(slider.hasCustomState('user-valid')).to.be.false;
expect(slider.customStates.has('invalid')).to.be.true;
expect(slider.customStates.has('valid')).to.be.false;
expect(slider.customStates.has('user-invalid')).to.be.false;
expect(slider.customStates.has('user-valid')).to.be.false;
await clickOnElement(slider);
await slider.updateComplete;
slider.blur();
await slider.updateComplete;
expect(slider.hasCustomState('user-invalid')).to.be.true;
expect(slider.hasCustomState('user-valid')).to.be.false;
expect(slider.customStates.has('user-invalid')).to.be.true;
expect(slider.customStates.has('user-valid')).to.be.false;
});
it('should receive validation attributes ("states") even when novalidate is used on the parent form', async () => {
@@ -154,10 +154,10 @@ describe('<wa-slider>', () => {
slider.setCustomValidity('Invalid value');
await slider.updateComplete;
expect(slider.hasCustomState('invalid')).to.be.true;
expect(slider.hasCustomState('valid')).to.be.false;
expect(slider.hasCustomState('user-invalid')).to.be.false;
expect(slider.hasCustomState('user-valid')).to.be.false;
expect(slider.customStates.has('invalid')).to.be.true;
expect(slider.customStates.has('valid')).to.be.false;
expect(slider.customStates.has('user-invalid')).to.be.false;
expect(slider.customStates.has('user-valid')).to.be.false;
});
it('should be present in form data when using the form attribute and located outside of a <form>', async () => {

View File

@@ -45,7 +45,7 @@ import styles from './slider.css';
*/
@customElement('wa-slider')
export default class WaSlider extends WebAwesomeFormAssociatedElement {
static shadowStyle = [formControlStyles, styles];
static css = [formControlStyles, styles];
static get validators() {
return [...super.validators, MirrorValidator()];

View File

@@ -19,7 +19,7 @@ import styles from './spinner.css';
*/
@customElement('wa-spinner')
export default class WaSpinner extends WebAwesomeElement {
static shadowStyle = styles;
static css = styles;
private readonly localize = new LocalizeController(this);

View File

@@ -35,7 +35,7 @@ import styles from './split-panel.css';
*/
@customElement('wa-split-panel')
export default class WaSplitPanel extends WebAwesomeElement {
static shadowStyle = styles;
static css = styles;
private cachedPositionInPixels: number;
private isCollapsed = false;

View File

@@ -230,12 +230,12 @@ describe('<wa-switch>', () => {
const el = await fixture<HTMLFormElement>(html` <form novalidate><wa-switch required></wa-switch></form> `);
const waSwitch = el.querySelector<WaSwitch>('wa-switch')!;
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;
expect(waSwitch.customStates.has('required')).to.be.true;
expect(waSwitch.customStates.has('optional')).to.be.false;
expect(waSwitch.customStates.has('invalid')).to.be.true;
expect(waSwitch.customStates.has('valid')).to.be.false;
expect(waSwitch.customStates.has('user-invalid')).to.be.false;
expect(waSwitch.customStates.has('user-valid')).to.be.false;
});
});

View File

@@ -50,7 +50,7 @@ import styles from './switch.css';
@customElement('wa-switch')
export default class WaSwitch extends WebAwesomeFormAssociatedElement {
static shadowRootOptions = { ...WebAwesomeFormAssociatedElement.shadowRootOptions, delegatesFocus: true };
static shadowStyle = [formControlStyles, sizeStyles, styles];
static css = [formControlStyles, sizeStyles, styles];
static get validators() {
return [...super.validators, MirrorValidator()];
@@ -170,7 +170,7 @@ export default class WaSwitch extends WebAwesomeFormAssociatedElement {
this.input.checked = this.checked; // force a sync update
}
this.toggleCustomState('checked', this.checked);
this.customStates.set('checked', this.checked);
this.updateValidity();
}

View File

@@ -46,7 +46,7 @@ import styles from './tab-group.css';
*/
@customElement('wa-tab-group')
export default class WaTabGroup extends WebAwesomeElement {
static shadowStyle = styles;
static css = styles;
private activeTab?: WaTab;
private mutationObserver: MutationObserver;

View File

@@ -21,7 +21,7 @@ let id = 0;
*/
@customElement('wa-tab-panel')
export default class WaTabPanel extends WebAwesomeElement {
static shadowStyle = styles;
static css = styles;
private readonly attrId = ++id;
private readonly componentId = `wa-tab-panel-${this.attrId}`;

View File

@@ -23,7 +23,7 @@ let id = 0;
*/
@customElement('wa-tab')
export default class WaTab extends WebAwesomeElement {
static shadowStyle = styles;
static css = styles;
private readonly attrId = ++id;
private readonly componentId = `wa-tab-${this.attrId}`;

View File

@@ -28,7 +28,7 @@ import styles from './tag.css';
*/
@customElement('wa-tag')
export default class WaTag extends WebAwesomeElement {
static shadowStyle = [sizeStyles, variantStyles, appearanceStyles, styles];
static css = [sizeStyles, variantStyles, appearanceStyles, styles];
private readonly localize = new LocalizeController(this);

View File

@@ -144,12 +144,12 @@ describe('<wa-textarea>', () => {
const el = await fixture<WaTextarea>(html` <wa-textarea required value="a"></wa-textarea> `);
expect(el.checkValidity()).to.be.true;
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;
expect(el.customStates.has('required')).to.be.true;
expect(el.customStates.has('optional')).to.be.false;
expect(el.customStates.has('invalid')).to.be.false;
expect(el.customStates.has('valid')).to.be.true;
expect(el.customStates.has('user-invalid')).to.be.false;
expect(el.customStates.has('user-valid')).to.be.false;
el.focus();
await sendKeys({ press: 'b' });
@@ -158,19 +158,19 @@ describe('<wa-textarea>', () => {
await el.updateComplete;
expect(el.checkValidity()).to.be.true;
expect(el.hasCustomState('user-invalid')).to.be.false;
expect(el.hasCustomState('user-valid')).to.be.true;
expect(el.customStates.has('user-invalid')).to.be.false;
expect(el.customStates.has('user-valid')).to.be.true;
});
it('should receive the correct validation attributes ("states") when invalid', async () => {
const el = await fixture<WaTextarea>(html` <wa-textarea required></wa-textarea> `);
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;
expect(el.customStates.has('required')).to.be.true;
expect(el.customStates.has('optional')).to.be.false;
expect(el.customStates.has('invalid')).to.be.true;
expect(el.customStates.has('valid')).to.be.false;
expect(el.customStates.has('user-invalid')).to.be.false;
expect(el.customStates.has('user-valid')).to.be.false;
el.focus();
await sendKeys({ press: 'a' });
@@ -179,8 +179,8 @@ describe('<wa-textarea>', () => {
el.blur();
await el.updateComplete;
expect(el.hasCustomState('user-invalid')).to.be.true;
expect(el.hasCustomState('user-valid')).to.be.false;
expect(el.customStates.has('user-invalid')).to.be.true;
expect(el.customStates.has('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('<wa-textarea>', () => {
`);
const textarea = el.querySelector<WaTextarea>('wa-textarea')!;
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;
expect(textarea.customStates.has('required')).to.be.true;
expect(textarea.customStates.has('optional')).to.be.false;
expect(textarea.customStates.has('invalid')).to.be.true;
expect(textarea.customStates.has('valid')).to.be.false;
expect(textarea.customStates.has('user-invalid')).to.be.false;
expect(textarea.customStates.has('user-valid')).to.be.false;
});
});
@@ -237,10 +237,10 @@ describe('<wa-textarea>', () => {
await textarea.updateComplete;
expect(textarea.checkValidity()).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;
expect(textarea.customStates.has('invalid')).to.be.true;
expect(textarea.customStates.has('valid')).to.be.false;
expect(textarea.customStates.has('user-invalid')).to.be.false;
expect(textarea.customStates.has('user-valid')).to.be.false;
textarea.focus();
await sendKeys({ type: 'test' });
@@ -248,8 +248,8 @@ describe('<wa-textarea>', () => {
textarea.blur();
await textarea.updateComplete;
expect(textarea.hasCustomState('user-invalid')).to.be.true;
expect(textarea.hasCustomState('user-valid')).to.be.false;
expect(textarea.customStates.has('user-invalid')).to.be.true;
expect(textarea.customStates.has('user-valid')).to.be.false;
});
it('should be present in form data when using the form attribute and located outside of a <form>', async () => {

View File

@@ -43,7 +43,7 @@ import styles from './textarea.css';
*/
@customElement('wa-textarea')
export default class WaTextarea extends WebAwesomeFormAssociatedElement {
static shadowStyle = [formControlStyles, appearanceStyles, sizeStyles, styles];
static css = [formControlStyles, appearanceStyles, sizeStyles, styles];
static get validators() {
return [...super.validators, MirrorValidator()];
@@ -270,7 +270,7 @@ export default class WaTextarea extends WebAwesomeFormAssociatedElement {
super.updated(changedProperties);
if (changedProperties.has('value')) {
this.toggleCustomState('blank', !this.value);
this.customStates.set('blank', !this.value);
}
}

View File

@@ -40,7 +40,7 @@ import styles from './tooltip.css';
*/
@customElement('wa-tooltip')
export default class WaTooltip extends WebAwesomeElement {
static shadowStyle = styles;
static css = styles;
static dependencies = { 'wa-popup': WaPopup };
private hoverTimeout: number;

View File

@@ -128,7 +128,7 @@ describe('<wa-tree-item>', () => {
await leafItem.updateComplete;
// Assert
expect(leafItem.hasCustomState('selected')).to.be.true;
expect(leafItem.customStates.has('selected')).to.be.true;
});
});
@@ -150,7 +150,7 @@ describe('<wa-tree-item>', () => {
await leafItem.updateComplete;
// Assert
expect(leafItem.hasCustomState('expanded')).to.be.true;
expect(leafItem.customStates.has('expanded')).to.be.true;
});
});

View File

@@ -70,7 +70,7 @@ import styles from './tree-item.css';
*/
@customElement('wa-tree-item')
export default class WaTreeItem extends WebAwesomeElement {
static shadowStyle = styles;
static css = styles;
static isTreeItem(node: Node) {
return node instanceof Element && node.getAttribute('role') === 'treeitem';
@@ -188,23 +188,23 @@ export default class WaTreeItem extends WebAwesomeElement {
@watch('disabled')
handleDisabledChange() {
this.toggleCustomState('disabled', this.disabled);
this.customStates.set('disabled', this.disabled);
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
}
@watch('expanded')
handleExpandedState() {
this.toggleCustomState('expanded', this.expanded);
this.customStates.set('expanded', this.expanded);
}
@watch('indeterminate')
handleIndeterminateStateChange() {
this.toggleCustomState('indeterminate', this.indeterminate);
this.customStates.set('indeterminate', this.indeterminate);
}
@watch('selected')
handleSelectedChange() {
this.toggleCustomState('selected', this.selected);
this.customStates.set('selected', this.selected);
this.setAttribute('aria-selected', this.selected ? 'true' : 'false');
}

View File

@@ -73,7 +73,7 @@ function syncCheckboxes(changedTreeItem: WaTreeItem, initialSync = false) {
*/
@customElement('wa-tree')
export default class WaTree extends WebAwesomeElement {
static shadowStyle = styles;
static css = styles;
@query('slot:not([name])') defaultSlot: HTMLSlotElement;
@query('slot[name=expand-icon]') expandedIconSlot: HTMLSlotElement;

View File

@@ -70,7 +70,7 @@ export const viewportPropertyConverter = {
*/
@customElement('wa-viewport-demo')
export default class WaViewportDemo extends WebAwesomeElement {
static shadowStyle = styles;
static css = styles;
@query('[part~=frame]')
private viewportElement: HTMLElement;

View File

@@ -184,7 +184,7 @@ function runAllValidityTests(
control.customError = 'MyError';
await control.updateComplete;
expect(control.validity.valid).to.equal(false);
expect(control.hasCustomState('invalid')).to.equal(true);
expect(control.customStates.has('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.hasCustomState('invalid')).to.equal(true);
expect(control.customStates.has('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.hasCustomState('disabled')).to.equal(true);
expect(control.customStates.has('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.hasCustomState('disabled')).to.equal(false);
expect(control.customStates.has('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.hasCustomState('disabled')).to.equal(true);
expect(control.customStates.has('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.hasCustomState('disabled')).to.equal(false);
expect(control.customStates.has('disabled')).to.equal(false);
});
}
});

View File

@@ -1,7 +1,7 @@
import type { CSSResult, CSSResultGroup, PropertyValues } from 'lit';
import { LitElement, isServer, unsafeCSS } from 'lit';
import { property } from 'lit/decorators.js';
import componentStyles from '../styles/component/host.css';
import hostStyles from '../styles/component/host.css';
// Augment Lit's module
declare module 'lit' {
@@ -15,6 +15,31 @@ declare module 'lit' {
}
export default class WebAwesomeElement extends LitElement {
/**
* One or more CSS files to include in the component's shadow root. Host styles are automatically prepended. We use
* this instead of Lit's styles property because we're importing CSS files as strings and need to convert them using
* unsafeCSS.
*/
static css?: CSSResultGroup | CSSResult | string | (CSSResult | string)[];
/**
* Override the default styles property to fetch and convert string CSS files. Components can override this behavior
* by setting their own `static styles = []` property.
*/
static get styles(): CSSResultGroup {
const styles = Array.isArray(this.css) ? this.css : this.css ? [this.css] : [];
return [hostStyles, ...styles].map(style => (typeof style === 'string' ? unsafeCSS(style) : style));
}
#hasRecordedInitialProperties = false;
initialReflectedProperties: Map<string, unknown> = new Map();
internals: ElementInternals;
// Make localization attributes reactive
@property() dir: string;
@property() lang: string;
@property({ type: Boolean, reflect: true, attribute: 'did-ssr' }) didSSR = isServer || Boolean(this.shadowRoot);
constructor() {
super();
@@ -26,52 +51,16 @@ export default class WebAwesomeElement extends LitElement {
console.error('Element internals are not supported in your browser. Consider using a polyfill');
}
this.toggleCustomState('wa-defined');
this.customStates.set('wa-defined', true);
let Self = this.constructor as typeof WebAwesomeElement;
for (let [property, spec] of Self.elementProperties) {
if (spec.default === 'inherit' && spec.initial !== undefined && typeof property === 'string') {
this.toggleCustomState(`initial-${property}-${spec.initial}`);
this.customStates.set(`initial-${property}-${spec.initial}`, true);
}
}
}
// Make localization attributes reactive
@property() dir: string;
@property() lang: string;
/**
* One or more styles for the elements own shadow DOM.
* Shared component styles will automatically be added.
* If that is not desirable, the subclass can define its own styles property.
*/
static shadowStyle?: CSSResultGroup | CSSResult | string | (CSSResult | string)[];
/** The base styles property will only get called if the subclass does not define a styles property of its own */
static get styles(): CSSResultGroup {
const shadowStyle = this.shadowStyle
? Array.isArray(this.shadowStyle)
? this.shadowStyle
: [this.shadowStyle]
: [];
// Convert any string styles to Lits CSSResult
const shadowStyles = [componentStyles, ...shadowStyle].map(style =>
typeof style === 'string' ? unsafeCSS(style) : style,
);
return shadowStyles;
}
@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();
internals: ElementInternals;
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) {
if (!this.#hasRecordedInitialProperties) {
(this.constructor as typeof WebAwesomeElement).elementProperties.forEach(
@@ -131,43 +120,26 @@ export default class WebAwesomeElement extends LitElement {
}
}
/** Checks if states are supported by the element */
private hasStatesSupport(): boolean {
return Boolean(this.internals?.states);
}
/** Adds a custom state to the element. */
addCustomState(state: string) {
if (this.hasStatesSupport()) {
this.internals.states.add(state);
}
}
/** Removes a custom state from the element. */
deleteCustomState(state: string) {
if (this.hasStatesSupport()) {
this.internals.states.delete(state);
}
}
/** Toggles a custom state on the element. */
toggleCustomState(state: string, force?: boolean) {
if (typeof force === 'boolean') {
if (force) {
this.addCustomState(state);
/**
* Methods for setting and checking custom states.
*/
public customStates = {
/** Adds or removes the specified custom state. */
set: (customState: string, active: boolean) => {
if (!Boolean(this.internals?.states)) return;
if (active) {
this.internals.states.add(customState);
} else {
this.deleteCustomState(state);
this.internals.states.delete(customState);
}
return;
}
},
this.toggleCustomState(state, !this.hasCustomState(state));
}
/** Determines if the element has the specified custom state. */
hasCustomState(state: string): boolean {
return this.hasStatesSupport() ? this.internals.states.has(state) : false;
}
/** Determines whether or not the element currently has the specified state. */
has: (customState: string) => {
if (!Boolean(this.internals?.states)) return false;
return this.internals.states.has(customState);
},
};
/**
* Given a native event, this function cancels it and dispatches it again from the host element using the desired

View File

@@ -172,7 +172,7 @@ export class WebAwesomeFormAssociatedElement
}
if (changedProperties.has('disabled')) {
this.toggleCustomState('disabled', this.disabled);
this.customStates.set('disabled', this.disabled);
if (this.hasAttribute('disabled') || (!isServer && !this.matches(':disabled'))) {
this.toggleAttribute('disabled', this.disabled);
@@ -255,12 +255,12 @@ export class WebAwesomeFormAssociatedElement
const isValid = this.internals.validity.valid;
const hasInteracted = this.hasInteracted;
this.toggleCustomState('required', required);
this.toggleCustomState('optional', !required);
this.toggleCustomState('invalid', !isValid);
this.toggleCustomState('valid', isValid);
this.toggleCustomState('user-invalid', !isValid && hasInteracted);
this.toggleCustomState('user-valid', isValid && hasInteracted);
this.customStates.set('required', required);
this.customStates.set('optional', !required);
this.customStates.set('invalid', !isValid);
this.customStates.set('valid', isValid);
this.customStates.set('user-invalid', !isValid && hasInteracted);
this.customStates.set('user-valid', isValid && hasInteracted);
}
/**