diff --git a/src/components/icon/icon.ts b/src/components/icon/icon.ts index 184038905..2fee60f9d 100644 --- a/src/components/icon/icon.ts +++ b/src/components/icon/icon.ts @@ -3,13 +3,12 @@ import { customElement, property, state } from 'lit/decorators.js'; import { isTemplateResult } from 'lit/directive-helpers.js'; import { WaErrorEvent } from '../../events/error.js'; import { WaLoadEvent } from '../../events/load.js'; -import { getComputedStyle } from '../../internal/computedStyle.js'; import { watch } from '../../internal/watch.js'; import WebAwesomeElement from '../../internal/webawesome-element.js'; import styles from './icon.css'; import { getIconLibrary, unwatchIcon, watchIcon, type IconLibrary } from './library.js'; -import type { HTMLTemplateResult, PropertyDeclaration, PropertyValues } from 'lit'; +import type { HTMLTemplateResult, PropertyValues } from 'lit'; const CACHEABLE_ERROR = Symbol(); const RETRYABLE_ERROR = Symbol(); @@ -49,21 +48,21 @@ export default class WaIcon extends WebAwesomeElement { @state() private svg: SVGElement | HTMLTemplateResult | null = null; /** The name of the icon to draw. Available names depend on the icon library being used. */ - @property() name?: string; + @property({ cssProperty: true }) name?: string; /** * The family of icons to choose from. For Font Awesome Free (default), valid options include `classic` and `brands`. * For Font Awesome Pro subscribers, valid options include, `classic`, `sharp`, `duotone`, and `brands`. Custom icon * libraries may or may not use this property. */ - @property() family: string; + @property({ cssProperty: true }) family: string; /** * The name of the icon's variant. For Font Awesome, valid options include `thin`, `light`, `regular`, and `solid` for * the `classic` and `sharp` families. Some variants require a Font Awesome Pro subscription. Custom icon libraries * may or may not use this property. */ - @property() variant: string; + @property({ cssProperty: true }) variant: string; /** Draws the icon in a fixed-width both. */ @property({ attribute: 'fixed-width', type: Boolean, reflect: true }) fixedWidth: false; @@ -81,51 +80,14 @@ export default class WaIcon extends WebAwesomeElement { @property() label = ''; /** The name of a registered custom icon library. */ - @property() library = 'default'; - - #computedStyle: CSSStyleDeclaration | null; - // Ideally we want to move this config to the decorators - static cssPropertyAttributes = new Set(['family', 'name', 'variant', 'library']); - #setVia: Record = {}; - #setting = new Set(); + @property({ cssProperty: true }) library = 'default'; connectedCallback() { super.connectedCallback(); - if (WaIcon.cssPropertyAttributes.size > 0) { - this.updateCSSProperties(); - } - watchIcon(this); } - private updateCSSProperties() { - this.#computedStyle ??= getComputedStyle(this); - - // FIXME this is currently static. It will only update when the element is connected, and not when the CSS property changes. - for (let name of WaIcon.cssPropertyAttributes) { - this.updateCSSProperty(name as 'family' | 'name' | 'variant' | 'library'); - } - } - - private updateCSSProperty(name: 'family' | 'name' | 'variant' | 'library') { - // FIXME currently this means that CSS properties will override JS properties. This is not ideal. - if (!this.hasAttribute(name) && this.#setVia[name] !== 'js') { - // Check if supplied as a CSS custom property - // TODO !important should override attribute values - const value = this.#computedStyle?.getPropertyValue(`--wa-icon-${name}`); - - if (value) { - this.#setVia[name] = 'css'; - this.#setting.add(name); - this[name] = value.trim(); - this.updateComplete.then(() => { - this.#setting.delete(name); - }); - } - } - } - firstUpdated() { this.initialRender = true; this.setIcon(); @@ -268,15 +230,6 @@ export default class WaIcon extends WebAwesomeElement { updated(changedProperties: PropertyValues) { super.updated(changedProperties); - if (WaIcon.cssPropertyAttributes.size > 0) { - for (let [name] of changedProperties) { - if (typeof name === 'string' && this.#setVia[name] === 'css' && !this.#setting.has(name)) { - // A property is being set via JS and it’s NOT because we're reflecting a CSS property - this.#setVia[name] = 'js'; - } - } - } - // Sometimes (like with SSR -> hydration) mutators dont get applied due to race conditions. This ensures mutators get re-applied. const library = getIconLibrary(this.library); diff --git a/src/internal/webawesome-element.ts b/src/internal/webawesome-element.ts index 12e85ee23..1ceaf92b0 100644 --- a/src/internal/webawesome-element.ts +++ b/src/internal/webawesome-element.ts @@ -1,7 +1,18 @@ -import type { CSSResult, CSSResultGroup, PropertyValues } from 'lit'; +import type { CSSResult, CSSResultGroup, PropertyDeclaration, PropertyValues } from 'lit'; import { LitElement, isServer, unsafeCSS } from 'lit'; import { property } from 'lit/decorators.js'; import componentStyles from '../styles/shadow/component.css'; +import { getComputedStyle } from './computedStyle.js'; + +// Augment Lit's module +declare module 'lit' { + interface PropertyDeclaration { + /** + * Indicates whether the property should reflect to a CSS custom property. + */ + cssProperty?: boolean; + } +} export default class WebAwesomeElement extends LitElement { constructor() { @@ -52,6 +63,16 @@ export default class WebAwesomeElement extends LitElement { internals: ElementInternals; + #computedStyle: CSSStyleDeclaration | null; + #setVia: Record = {}; + #setting = new Set(); + + connectedCallback() { + super.connectedCallback(); + + this.updateCSSProperties(); + } + attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) { if (!this.#hasRecordedInitialProperties) { (this.constructor as typeof WebAwesomeElement).elementProperties.forEach( @@ -111,6 +132,21 @@ export default class WebAwesomeElement extends LitElement { } } + updated(changedProperties: PropertyValues) { + super.updated(changedProperties); + + let Self = this.constructor as typeof WebAwesomeElement; + + if (Self.cssPropertyAttributes.size > 0) { + for (let [name] of changedProperties) { + if (typeof name === 'string' && this.#setVia[name] === 'css' && !this.#setting.has(name)) { + // A property is being set via JS and it’s NOT because we're reflecting a CSS property + this.#setVia[name] = 'js'; + } + } + } + } + /** Checks if states are supported by the element */ private hasStatesSupport(): boolean { return Boolean(this.internals?.states); @@ -148,4 +184,51 @@ export default class WebAwesomeElement extends LitElement { hasCustomState(state: string): boolean { return this.hasStatesSupport() ? this.internals.states.has(state) : false; } + + protected updateCSSProperties() { + const Self = this.constructor as typeof WebAwesomeElement; + if (Self.cssPropertyAttributes.size === 0) { + return; + } + + this.#computedStyle ??= getComputedStyle(this); + + // FIXME this is currently static. It will only update when the element is connected, and not when the CSS property changes. + const tagName = this.tagName.toLowerCase(); + for (let name of Self.cssPropertyAttributes) { + // FIXME currently this means that CSS properties will override JS properties. This is not ideal. + if (typeof name === 'string' && !this.hasAttribute(name) && this.#setVia[name] !== 'js') { + // Check if supplied as a CSS custom property + // TODO !important should override attribute values + const value = this.#computedStyle?.getPropertyValue(`--${tagName}-${name}`); + + if (value) { + this.#setVia[name] = 'css'; + this.#setting.add(name); + // @ts-ignore + this[name] = value.trim(); + this.updateComplete.then(() => { + this.#setting.delete(name); + }); + } + } + } + } + + // Subclasses will override this + static cssPropertyAttributes = new Set(); + + static createProperty(name: PropertyKey, options?: PropertyDeclaration): void { + super.createProperty(name, options); + + if (options?.cssProperty) { + if (this.cssPropertyAttributes === WebAwesomeElement.cssPropertyAttributes) { + // Each class needs its own, otherwise they'd share the same Set + this.cssPropertyAttributes = new Set(); + } + + // let cssProperty = options.cssProperty === true ? `--${this.tagName}-${name}` : options.cssProperty; + this.cssPropertyAttributes.add(name); + } + } }