Compare commits

...

4 Commits

Author SHA1 Message Date
Lea Verou
2c42521fc3 Update webawesome-element.ts 2025-01-08 10:29:45 -05:00
Lea Verou
9772192d23 Failed attempt to use @bramus/style-observer 2025-01-08 10:24:30 -05:00
Lea Verou
f601b8aaf4 Add dynamic docs since this is not in alpha 2025-01-08 10:23:45 -05:00
Lea Verou
d18edcc941 CSS properties to set component attributes
- Base class abstraction
- Use in `<wa-icon>` (docs excluded from alpha)
2025-01-07 18:25:39 -05:00
6 changed files with 199 additions and 5 deletions

View File

@@ -24,6 +24,63 @@ Many Font Awesome Pro icon families have variants such as `thin`, `light`, `regu
<wa-icon family="brands" name="web-awesome"></wa-icon>
```
<div data-alpha="remove">
### Setting icon info 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).
```html {.example}
<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.
<button id="toggle_icon">Toggle&nbsp;<wa-icon name="circle-exclamation"></wa-icon></button>
</wa-callout>
<style>
wa-callout {
--wa-icon-variant: regular;
--wa-icon-name: info-circle;
&[variant="warning"] {
--wa-icon-name: triangle-exclamation;
}
}
</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 dont 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.
</div>
### Colors
Icons inherit their color from the current text color. Thus, you can set the `color` property on the `<wa-icon>` element or an ancestor to change the color.

10
package-lock.json generated
View File

@@ -9,6 +9,7 @@
"version": "3.0.0-alpha.7",
"license": "MIT",
"dependencies": {
"@bramus/style-observer": "^1.3.0",
"@ctrl/tinycolor": "^4.1.0",
"@floating-ui/dom": "^1.6.12",
"@shoelace-style/animations": "^1.2.0",
@@ -676,6 +677,15 @@
"node": ">=4"
}
},
"node_modules/@bramus/style-observer": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@bramus/style-observer/-/style-observer-1.3.0.tgz",
"integrity": "sha512-IQjYId9X7xgz0NeKGatC37lqsdS+cE5bfdB9jKh7+zJnA9BqENee2C48boDIRQrTED4JxleRZGhTY86S1/l7QA==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/@cspell/cspell-bundled-dicts": {
"version": "6.31.3",
"resolved": "https://registry.npmjs.org/@cspell/cspell-bundled-dicts/-/cspell-bundled-dicts-6.31.3.tgz",

View File

@@ -64,6 +64,7 @@
"node": ">=14.17.0"
},
"dependencies": {
"@bramus/style-observer": "^1.3.0",
"@ctrl/tinycolor": "^4.1.0",
"@floating-ui/dom": "^1.6.12",
"@shoelace-style/animations": "^1.2.0",

View File

@@ -31,3 +31,24 @@ svg {
width: 1em;
justify-content: center;
}
@property --wa-icon-family {
syntax: '<custom-ident> | auto';
inherits: true;
initial-value: 'auto';
}
@property --wa-icon-variant {
syntax: '<custom-ident> | auto';
inherits: true;
initial-value: 'auto';
}
@property --wa-icon-library {
syntax: '<custom-ident> | auto';
inherits: true;
initial-value: 'auto';
}
@property --wa-icon-name {
syntax: '<custom-ident> | auto';
inherits: true;
initial-value: 'auto';
}

View File

@@ -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({ reflect: true }) 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({ reflect: true }) 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({ reflect: true }) 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;
@@ -80,10 +80,11 @@ export default class WaIcon extends WebAwesomeElement {
@property() label = '';
/** The name of a registered custom icon library. */
@property({ reflect: true }) library = 'default';
@property({ cssProperty: true }) library = 'default';
connectedCallback() {
super.connectedCallback();
watchIcon(this);
}

View File

@@ -1,7 +1,19 @@
import type { CSSResult, CSSResultGroup, PropertyValues } from 'lit';
import { CSSStyleObserver } from '@bramus/style-observer';
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?: true | string;
}
}
export default class WebAwesomeElement extends LitElement {
constructor() {
@@ -52,6 +64,18 @@ export default class WebAwesomeElement extends LitElement {
internals: ElementInternals;
#computedStyle: CSSStyleDeclaration | null;
#setVia: Record<PropertyKey, 'css' | 'attribute' | 'js'> = {};
#setting = new Set<PropertyKey>();
connectedCallback() {
super.connectedCallback();
// FIXME this is currently static.
// It will only update when the element is connected, not when a relevant CSS property changes.
this.updateCSSProperties();
}
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) {
if (!this.#hasRecordedInitialProperties) {
(this.constructor as typeof WebAwesomeElement).elementProperties.forEach(
@@ -111,6 +135,21 @@ export default class WebAwesomeElement extends LitElement {
}
}
updated(changedProperties: PropertyValues<this>) {
super.updated(changedProperties);
let Self = this.constructor as typeof WebAwesomeElement;
if (Self.cssAttributeProperties.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 its 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 +187,69 @@ 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.cssAttributeProperties.size === 0) {
return;
}
if (!Self.styleObserver) {
// First time, init stuff
// First, replace `true` with actual CSS property names
for (let [name, cssProperty] of Self.cssAttributeProperties) {
if (cssProperty === true) {
// Default name
cssProperty = `--${this.tagName.toLowerCase()}-${name}`;
Self.cssAttributeProperties.set(name, cssProperty);
}
}
// Then we observe them
let cssProperties = [...Self.cssAttributeProperties.values()] as string[];
Self.styleObserver = new CSSStyleObserver(cssProperties, (...args) => {
console.log(...args);
this.updateCSSProperties();
});
}
this.#computedStyle ??= getComputedStyle(this);
const tagName = this.tagName.toLowerCase();
for (let [name, cssProperty] of Self.cssAttributeProperties) {
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
cssProperty = cssProperty === true ? `--${tagName}-${name}` : cssProperty;
const value = this.#computedStyle?.getPropertyValue(cssProperty);
if (value && value !== 'auto') {
this.#setVia[name] = 'css';
this.#setting.add(name);
// @ts-ignore
this[name] = value.trim();
this.updateComplete.then(() => {
this.#setting.delete(name);
});
}
}
}
}
// Subclasses will get their own copy automagically (see below)
protected static cssAttributeProperties = new Map<PropertyKey, true | string>();
protected static styleObserver: CSSStyleObserver | undefined;
static createProperty(name: PropertyKey, options?: PropertyDeclaration): void {
super.createProperty(name, options);
if (options?.cssProperty) {
if (this.cssAttributeProperties === WebAwesomeElement.cssAttributeProperties) {
// Each class needs its own, otherwise they'd share the same object
this.cssAttributeProperties = new Map();
}
this.cssAttributeProperties.set(name, options.cssProperty);
}
}
}