mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-12 12:09:26 +00:00
Refactor logic, update to use style-observer to monitor changes dynamically
This commit is contained in:
@@ -26,28 +26,46 @@ Many Font Awesome Pro icon families have variants such as `thin`, `light`, `regu
|
||||
|
||||
<div data-alpha="remove">
|
||||
|
||||
### Setting icon info via CSS
|
||||
### Setting defaults via CSS
|
||||
|
||||
You can also set the icon's family, name, and variant via CSS custom properties.
|
||||
This can be useful when you want to set the icon dynamically (e.g. in response to a CSS pseudo-class or media query) or set defaults for a group of icons (e.g. icons inside callouts or all icons for a given theme).
|
||||
You can use certain CSS custom properties to set icon defaults, not just on the icon itself, but any ancestor.
|
||||
This can be useful when you want certain parameters to vary based on context, e.g. icons inside callouts or all icons for a given theme.
|
||||
|
||||
:::warning
|
||||
These CSS properties are intended to set **defaults**, and thus only make a difference when the corresponding attributes are not set.
|
||||
In future versions of Web Awesome, we may change this behavior to allow CSS properties to override attributes if `!important` is used.
|
||||
:::
|
||||
|
||||
For example, here is how you can use CSS custom properties to set a default icon for each type of callout:
|
||||
|
||||
```html {.example}
|
||||
<wa-callout>
|
||||
<!-- Look ma, no attributes! -->
|
||||
<wa-icon slot="icon"></wa-icon>
|
||||
This is a callout.
|
||||
This is a normal callout.
|
||||
</wa-callout>
|
||||
|
||||
<wa-callout variant="danger">
|
||||
<wa-icon slot="icon" name="dumpster-fire" variant="solid"></wa-icon>
|
||||
This is a callout with an explicit icon.
|
||||
This is a callout with an explicit icon, which overrides these defaults.
|
||||
</wa-callout>
|
||||
|
||||
<wa-callout variant="warning">
|
||||
<!-- Look ma, no attributes! -->
|
||||
<wa-icon slot="icon"></wa-icon>
|
||||
Here be dragons.
|
||||
<button id="toggle_icon">Toggle <wa-icon name="circle-exclamation"></wa-icon></button>
|
||||
</wa-callout>
|
||||
|
||||
<wa-callout variant="danger">
|
||||
<!-- Look ma, no attributes! -->
|
||||
<wa-icon slot="icon"></wa-icon>
|
||||
Here be more dragons.
|
||||
</wa-callout>
|
||||
|
||||
<wa-callout variant="success">
|
||||
<!-- Look ma, no attributes! -->
|
||||
<wa-icon slot="icon"></wa-icon>
|
||||
Success!
|
||||
</wa-callout>
|
||||
|
||||
<style>
|
||||
@@ -58,26 +76,37 @@ wa-callout {
|
||||
&[variant="warning"] {
|
||||
--wa-icon-name: triangle-exclamation;
|
||||
}
|
||||
|
||||
&[variant="danger"] {
|
||||
--wa-icon-name: circle-exclamation;
|
||||
}
|
||||
|
||||
&[variant="success"] {
|
||||
--wa-icon-name: circle-check;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
toggle_icon.addEventListener('click', e => {
|
||||
let callout = e.target.closest('wa-callout');
|
||||
let value = callout.style.getPropertyValue('--wa-icon-name').trim();
|
||||
if (value) {
|
||||
callout.style.removeProperty('--wa-icon-name');
|
||||
}
|
||||
else {
|
||||
callout.style.setProperty('--wa-icon-name', 'circle-exclamation');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
Notes:
|
||||
- If you specify attributes, they will override the CSS custom properties, which provides a way to set defaults and then override them as needed.
|
||||
- CSS custom properties inherit — so if you set a `--wa-icon-*` custom property on an element, it will affect *all* icons within it that don’t override these values (either via attributes or CSS custom properties).
|
||||
- These CSS properties are currently not reactive and will only be read when the component is first connected.
|
||||
You can even set icons dynamically, as a response to user interaction or media queries.
|
||||
For example, here's how we can change the icon on hover:
|
||||
|
||||
```html {.example}
|
||||
<wa-button class="github" href="https://github.com/webawesome/webawesome"><wa-icon slot="prefix" fixed-width></wa-icon> GitHub Repo</wa-button>
|
||||
<style>
|
||||
.github {
|
||||
--wa-icon-name: github;
|
||||
--wa-icon-family: brands;
|
||||
|
||||
&:hover {
|
||||
--wa-icon-name: arrow-up-right-from-square;
|
||||
--wa-icon-family: classic;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
60
docs/docs/components/test.md
Normal file
60
docs/docs/components/test.md
Normal file
@@ -0,0 +1,60 @@
|
||||
---
|
||||
layout: blank
|
||||
---
|
||||
|
||||
{#<wa-callout>
|
||||
<!-- Look ma, no attributes! -->
|
||||
<wa-icon slot="icon"></wa-icon>
|
||||
This is a callout.
|
||||
</wa-callout>
|
||||
|
||||
<wa-callout variant="danger">
|
||||
<wa-icon slot="icon" name="dumpster-fire" variant="solid"></wa-icon>
|
||||
This is a callout with an explicit icon.
|
||||
</wa-callout>
|
||||
|
||||
<wa-callout variant="warning">
|
||||
<!-- Look ma, no attributes! -->
|
||||
<wa-icon slot="icon"></wa-icon>
|
||||
Here be dragons.
|
||||
<wa-button id="toggle_icon">Toggle <wa-icon id="poo_icon" name="poo" slot="suffix"></wa-icon></wa-button>
|
||||
</wa-callout>
|
||||
#}
|
||||
|
||||
|
||||
|
||||
<style>
|
||||
wa-callout {
|
||||
--wa-icon-variant: regular;
|
||||
--wa-icon-name: info-circle;
|
||||
|
||||
&[variant="warning"] {
|
||||
--wa-icon-name: triangle-exclamation;
|
||||
}
|
||||
}
|
||||
|
||||
wa-button, button {
|
||||
--wa-icon-variant: regular;
|
||||
|
||||
&:hover {
|
||||
--wa-icon-variant: solid;
|
||||
}
|
||||
}
|
||||
|
||||
.github {
|
||||
--wa-icon-name: github;
|
||||
--wa-icon-family: brands;
|
||||
|
||||
&:hover {
|
||||
--wa-icon-name: arrow-up-right-from-square !important;
|
||||
--wa-icon-family: classic !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
</style>
|
||||
<wa-button id="toggle_icon">Toggle <wa-icon id="poo_icon" name="poo" slot="suffix"></wa-icon></wa-button>
|
||||
<button>Toggle <wa-icon id="poo_icon" name="poo" slot="suffix"></wa-icon></button>
|
||||
|
||||
<wa-button class="github"><wa-icon slot="prefix" fixed-width></wa-icon> GitHub</wa-button>
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
title: CSS Properties Benchmark
|
||||
unlisted: true
|
||||
wide: true
|
||||
---
|
||||
|
||||
{% set icons = {
|
||||
@@ -22,7 +23,7 @@ unlisted: true
|
||||
|
||||
<style>
|
||||
.icon-tests {
|
||||
font-size: .2rem;
|
||||
font-size: .5rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
@@ -34,7 +35,7 @@ wa-icon {
|
||||
}
|
||||
</style>
|
||||
|
||||
{% set repetitions = 500 %}
|
||||
{% set repetitions = 200 %}
|
||||
|
||||
<h2>Setting everything via attributes</h2>
|
||||
|
||||
@@ -47,7 +48,8 @@ wa-icon {
|
||||
</div>
|
||||
|
||||
<h2>Setting variant & family via CSS</h2>
|
||||
<div class="icon-tests" style="--wa-icon-variant: solid; --wa-icon-family: classic">
|
||||
|
||||
<div class="icon-tests" style="--wa-icon-variant: regular; --wa-icon-family: classic">
|
||||
{% for icon, svg in icons %}
|
||||
{% for i in range(repetitions) %}
|
||||
<wa-icon name="{{ icon }}"></wa-icon>
|
||||
|
||||
9
package-lock.json
generated
9
package-lock.json
generated
@@ -15,7 +15,8 @@
|
||||
"@shoelace-style/localize": "^3.2.1",
|
||||
"composed-offset-position": "^0.0.6",
|
||||
"lit": "^3.2.1",
|
||||
"qr-creator": "^1.0.0"
|
||||
"qr-creator": "^1.0.0",
|
||||
"style-observer": "^0.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@11ty/eleventy": "3.0.0",
|
||||
@@ -13085,6 +13086,12 @@
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/style-observer": {
|
||||
"version": "0.0.7",
|
||||
"resolved": "https://registry.npmjs.org/style-observer/-/style-observer-0.0.7.tgz",
|
||||
"integrity": "sha512-t75H3CRy+vd5q3yqyrf/De4tkz33hPQTiCcfh0NTesI5G7kJnZ227LEYTwqjKTtaFOCJvqZcYFHpJlF8bsk3bQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
|
||||
@@ -73,7 +73,8 @@
|
||||
"@shoelace-style/localize": "^3.2.1",
|
||||
"composed-offset-position": "^0.0.6",
|
||||
"lit": "^3.2.1",
|
||||
"qr-creator": "^1.0.0"
|
||||
"qr-creator": "^1.0.0",
|
||||
"style-observer": "^0.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@11ty/eleventy": "3.0.0",
|
||||
|
||||
@@ -48,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({ cssProperty: true }) name?: string;
|
||||
@property({ cssProperty: '--wa-icon-name' }) 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({ cssProperty: true }) family: string;
|
||||
@property({ cssProperty: '--wa-icon-family' }) 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({ cssProperty: true }) variant: string;
|
||||
@property({ cssProperty: '--wa-icon-variant' }) variant: string;
|
||||
|
||||
/** Draws the icon in a fixed-width both. */
|
||||
@property({ attribute: 'fixed-width', type: Boolean, reflect: true }) fixedWidth: false;
|
||||
@@ -80,7 +80,7 @@ export default class WaIcon extends WebAwesomeElement {
|
||||
@property() label = '';
|
||||
|
||||
/** The name of a registered custom icon library. */
|
||||
@property({ cssProperty: true }) library = 'default';
|
||||
@property({ cssProperty: '--wa-icon-library', default: 'default' }) library = 'default';
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
@@ -88,7 +88,8 @@ export default class WaIcon extends WebAwesomeElement {
|
||||
watchIcon(this);
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
firstUpdated(changedProperties: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProperties);
|
||||
this.initialRender = true;
|
||||
this.setIcon();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import type { CSSResult, CSSResultGroup, PropertyDeclaration, PropertyValues } from 'lit';
|
||||
import { LitElement, defaultConverter, isServer, unsafeCSS } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
// @ts-ignore
|
||||
import { ElementStyleObserver } from 'style-observer';
|
||||
import componentStyles from '../styles/shadow/component.css';
|
||||
import { getComputedStyle } from './computedStyle.js';
|
||||
|
||||
// Augment Lit's module
|
||||
declare module 'lit' {
|
||||
@@ -11,6 +14,11 @@ declare module 'lit' {
|
||||
*/
|
||||
default?: any;
|
||||
initial?: any;
|
||||
|
||||
/**
|
||||
* Indicates whether the property should reflect to a CSS custom property.
|
||||
*/
|
||||
cssProperty?: string;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +80,100 @@ export default class WebAwesomeElement extends LitElement {
|
||||
|
||||
internals: ElementInternals;
|
||||
|
||||
/** Metadata about CSS-settable props on this element */
|
||||
private cssProps: Record<PropertyKey, { setVia?: 'css' | 'attribute' | 'js'; updating?: boolean }> = {};
|
||||
private computedStyle: CSSStyleDeclaration | null = null;
|
||||
private styleObserver: ElementStyleObserver | null = null;
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
|
||||
// Set the initial computed styles
|
||||
const Self = this.constructor as typeof WebAwesomeElement;
|
||||
let cssProps = Object.keys(Self.cssProps);
|
||||
|
||||
if (cssProps.length > 0) {
|
||||
let properties: string[] = [];
|
||||
|
||||
if (Object.keys(this.cssProps).length === 0) {
|
||||
// First time connected, initialize
|
||||
// @ts-ignore
|
||||
this.cssProps = Object.fromEntries(
|
||||
cssProps.map(property => {
|
||||
let setVia = this.getSetVia(property);
|
||||
return [property, { setVia }];
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
for (let property in this.cssProps) {
|
||||
let setVia = this.cssProps[property].setVia;
|
||||
if (!setVia || setVia === 'css') {
|
||||
// No attribute set, observe CSS property
|
||||
properties.push(property);
|
||||
}
|
||||
}
|
||||
|
||||
this.handleCSSPropertyChange(properties);
|
||||
|
||||
this.styleObserver ??= new ElementStyleObserver(this, (records: object[]) => {
|
||||
let cssProperties = new Set(records.map((record: { property: string }) => record.property));
|
||||
|
||||
// Map CSS properties to prop names
|
||||
let properties = cssProps.filter(property => {
|
||||
let cssProperty = Self.cssProps[property].cssProperty as string;
|
||||
return cssProperties.has(cssProperty);
|
||||
});
|
||||
|
||||
this.handleCSSPropertyChange(properties);
|
||||
});
|
||||
this.styleObserver.unobserve();
|
||||
this.styleObserver.observe(properties.map(property => Self.cssProps[property].cssProperty as string));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Respond to CSS property changes for CSS properties reflecting props
|
||||
* @param [properties] - Prop names. Defaults to all CSS-reflected props.
|
||||
* @void
|
||||
*/
|
||||
handleCSSPropertyChange(properties?: PropertyKey | PropertyKey[]) {
|
||||
const Self = this.constructor as typeof WebAwesomeElement;
|
||||
|
||||
properties ??= Object.keys(Self.cssProps);
|
||||
properties = Array.isArray(properties) ? properties : [properties];
|
||||
|
||||
if (properties.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.computedStyle ??= getComputedStyle(this);
|
||||
|
||||
for (let property of properties) {
|
||||
let propOptions = Self.cssProps[property];
|
||||
let cssProperty = propOptions?.cssProperty;
|
||||
let meta = this.cssProps[property];
|
||||
|
||||
if (!cssProperty || (meta.setVia && meta.setVia !== 'css')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const value = this.computedStyle?.getPropertyValue(cssProperty);
|
||||
// if (property === 'variant' && !value) debugger;
|
||||
|
||||
if (value) {
|
||||
meta.setVia = 'css';
|
||||
meta.updating = true;
|
||||
// @ts-ignore
|
||||
this[property] = value.trim();
|
||||
|
||||
this.updateComplete.then(() => {
|
||||
meta.updating = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) {
|
||||
if (!this.#hasRecordedInitialProperties) {
|
||||
(this.constructor as typeof WebAwesomeElement).elementProperties.forEach(
|
||||
@@ -115,6 +217,46 @@ export default class WebAwesomeElement extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private getSetVia(property: PropertyKey): 'css' | 'js' | 'attribute' | undefined {
|
||||
let Self = this.constructor as typeof WebAwesomeElement;
|
||||
let setVia;
|
||||
let propOptions = Self.cssProps[property];
|
||||
let attribute = typeof propOptions.attribute === 'string' ? propOptions.attribute : (property as string);
|
||||
|
||||
if (propOptions.attribute !== false && this.hasAttribute(attribute)) {
|
||||
setVia = 'attribute';
|
||||
} else {
|
||||
// @ts-ignore
|
||||
let value = this[property as PropertyKey];
|
||||
if (value !== undefined && value !== propOptions.default) {
|
||||
setVia = 'js';
|
||||
}
|
||||
}
|
||||
|
||||
return setVia as 'attribute' | 'js' | 'css' | undefined;
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues<this>) {
|
||||
super.updated(changedProperties);
|
||||
|
||||
let Self = this.constructor as typeof WebAwesomeElement;
|
||||
let cssProps = Object.keys(Self.cssProps);
|
||||
|
||||
if (cssProps.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let [property] of changedProperties) {
|
||||
let meta = this.cssProps[property];
|
||||
|
||||
if (meta && typeof property === 'string' && !(meta.setVia === 'css' && meta.updating)) {
|
||||
// A prop is being set via JS or an attribute that was previously set via CSS
|
||||
// and it's not because we're in the middle of an update
|
||||
meta.setVia = this.getSetVia(property);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected update(changedProperties: PropertyValues<this>): void {
|
||||
try {
|
||||
super.update(changedProperties);
|
||||
@@ -230,6 +372,11 @@ export default class WebAwesomeElement extends LitElement {
|
||||
*/
|
||||
static rectProxy: undefined | string;
|
||||
|
||||
/**
|
||||
* Props that can be set via CSS custom properties
|
||||
*/
|
||||
static cssProps: Record<PropertyKey, PropertyDeclaration> = {};
|
||||
|
||||
static createProperty(name: PropertyKey, options?: PropertyDeclaration): void {
|
||||
if (options) {
|
||||
if (options.initial !== undefined && options.default === undefined) {
|
||||
@@ -256,8 +403,18 @@ export default class WebAwesomeElement extends LitElement {
|
||||
|
||||
super.createProperty(name, options);
|
||||
|
||||
// Wrap the default accessor with logic to return the default value if the value is null
|
||||
if (options) {
|
||||
if (options.cssProperty) {
|
||||
// Add to the set of CSS-settable props
|
||||
if (this.cssProps === WebAwesomeElement.cssProps) {
|
||||
// Each class needs its own, otherwise they'd share the same object
|
||||
this.cssProps = {};
|
||||
}
|
||||
|
||||
this.cssProps[name] = options;
|
||||
}
|
||||
|
||||
// Wrap the default accessor with logic to return the default value if the value is null
|
||||
if (options.default !== undefined) {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(this.prototype, name as string);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user