Files
webawesome/src/components/icon/icon.ts

256 lines
7.8 KiB
TypeScript
Raw Normal View History

2024-04-17 11:20:27 -04:00
import { html } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
2024-04-17 11:20:27 -04:00
import { isTemplateResult } from 'lit/directive-helpers.js';
import { WaErrorEvent } from '../../events/error.js';
import { WaLoadEvent } from '../../events/load.js';
2024-04-17 11:20:27 -04:00
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, PropertyValues } from 'lit';
2024-04-17 11:20:27 -04:00
const CACHEABLE_ERROR = Symbol();
const RETRYABLE_ERROR = Symbol();
type SVGResult = HTMLTemplateResult | SVGSVGElement | typeof RETRYABLE_ERROR | typeof CACHEABLE_ERROR;
let parser: DOMParser;
const iconCache = new Map<string, Promise<SVGResult>>();
interface IconSource {
url?: string;
fromLibrary: boolean;
}
/**
* @summary Icons are symbols that can be used to represent various options within an application.
2024-06-20 11:26:24 -04:00
* @documentation https://backers.webawesome.com/docs/components/icon
2024-04-17 11:20:27 -04:00
* @status stable
* @since 2.0
*
* @event wa-load - Emitted when the icon has loaded. When using `spriteSheet: true` this will not emit.
* @event wa-error - Emitted when the icon fails to load due to an error. When using `spriteSheet: true` this will not emit.
*
* @csspart svg - The internal SVG element.
* @csspart use - The `<use>` element generated when using `spriteSheet: true`
2024-06-11 12:44:15 -04:00
*
* @cssproperty [--primary-color=currentColor] - Sets a duotone icon's primary color.
* @cssproperty [--primary-opacity=1] - Sets a duotone icon's primary opacity.
* @cssproperty [--secondary-color=currentColor] - Sets a duotone icon's secondary color.
* @cssproperty [--secondary-opacity=0.4] - Sets a duotone icon's secondary opacity.
2024-04-17 11:20:27 -04:00
*/
@customElement('wa-icon')
export default class WaIcon extends WebAwesomeElement {
static shadowStyle = styles;
2024-04-17 11:20:27 -04:00
private initialRender = false;
@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;
/**
2024-06-11 14:38:11 -04:00
* 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.
2024-04-17 11:20:27 -04:00
*/
@property({ reflect: true }) family: string;
/**
* The name of the icon's variant. For Font Awesome, valid options include `thin`, `light`, `regular`, and `solid` for
2024-06-11 14:38:11 -04:00
* the `classic` and `sharp` families. Some variants require a Font Awesome Pro subscription. Custom icon libraries
* may or may not use this property.
2024-04-17 11:20:27 -04:00
*/
@property({ reflect: true }) variant: string;
2024-06-18 10:54:55 -04:00
/** Draws the icon in a fixed-width both. */
@property({ attribute: 'fixed-width', type: Boolean, reflect: true }) fixedWidth: false;
2024-04-17 11:20:27 -04:00
/**
* An external URL of an SVG file. Be sure you trust the content you are including, as it will be executed as code and
* can result in XSS attacks.
*/
@property() src?: string;
/**
* An alternate description to use for assistive devices. If omitted, the icon will be considered presentational and
* ignored by assistive devices.
*/
@property() label = '';
/** The name of a registered custom icon library. */
@property({ reflect: true }) library = 'default';
connectedCallback() {
super.connectedCallback();
watchIcon(this);
}
firstUpdated() {
this.initialRender = true;
this.setIcon();
}
disconnectedCallback() {
super.disconnectedCallback();
unwatchIcon(this);
}
private getIconSource(): IconSource {
const library = getIconLibrary(this.library);
if (this.name && library) {
return {
url: library.resolver(this.name, this.family, this.variant),
fromLibrary: true,
2024-04-17 11:20:27 -04:00
};
}
return {
url: this.src,
fromLibrary: false,
2024-04-17 11:20:27 -04:00
};
}
2024-06-11 10:51:49 -04:00
/** Given a URL, this function returns the resulting SVG element or an appropriate error symbol. */
private async resolveIcon(url: string, library?: IconLibrary): Promise<SVGResult> {
let fileData: Response;
if (library?.spriteSheet) {
this.svg = html`<svg part="svg">
<use part="use" href="${url}"></use>
</svg>`;
// Using a templateResult requires the SVG to be written to the DOM first before we can grab the SVGElement
// to be passed to the library's mutator function.
await this.updateComplete;
const svg = this.shadowRoot!.querySelector("[part='svg']")!;
if (typeof library.mutator === 'function') {
library.mutator(svg as SVGElement);
}
return this.svg;
}
try {
fileData = await fetch(url, { mode: 'cors' });
if (!fileData.ok) return fileData.status === 410 ? CACHEABLE_ERROR : RETRYABLE_ERROR;
} catch {
return RETRYABLE_ERROR;
}
try {
const div = document.createElement('div');
div.innerHTML = await fileData.text();
const svg = div.firstElementChild;
if (svg?.tagName?.toLowerCase() !== 'svg') return CACHEABLE_ERROR;
if (!parser) parser = new DOMParser();
const doc = parser.parseFromString(svg.outerHTML, 'text/html');
const svgEl = doc.body.querySelector('svg');
if (!svgEl) return CACHEABLE_ERROR;
svgEl.part.add('svg');
return document.adoptNode(svgEl);
} catch {
return CACHEABLE_ERROR;
}
}
2024-04-17 11:20:27 -04:00
@watch('label')
handleLabelChange() {
const hasLabel = typeof this.label === 'string' && this.label.length > 0;
if (hasLabel) {
this.setAttribute('role', 'img');
this.setAttribute('aria-label', this.label);
this.removeAttribute('aria-hidden');
} else {
this.removeAttribute('role');
this.removeAttribute('aria-label');
this.setAttribute('aria-hidden', 'true');
}
}
@watch(['family', 'name', 'library', 'variant', 'src'])
2024-04-17 11:20:27 -04:00
async setIcon() {
const { url, fromLibrary } = this.getIconSource();
const library = fromLibrary ? getIconLibrary(this.library) : undefined;
if (!url) {
this.svg = null;
return;
}
let iconResolver = iconCache.get(url);
if (!iconResolver) {
iconResolver = this.resolveIcon(url, library);
iconCache.set(url, iconResolver);
}
// If we haven't rendered yet, exit early. This avoids unnecessary work due to watching multiple props.
if (!this.initialRender) {
return;
}
const svg = await iconResolver;
if (svg === RETRYABLE_ERROR) {
iconCache.delete(url);
}
if (url !== this.getIconSource().url) {
// If the url has changed while fetching the icon, ignore this request
return;
}
if (isTemplateResult(svg)) {
this.svg = svg;
return;
}
switch (svg) {
case RETRYABLE_ERROR:
case CACHEABLE_ERROR:
this.svg = null;
this.dispatchEvent(new WaErrorEvent());
2024-04-17 11:20:27 -04:00
break;
default:
this.svg = svg.cloneNode(true) as SVGElement;
library?.mutator?.(this.svg);
this.dispatchEvent(new WaLoadEvent());
2024-04-17 11:20:27 -04:00
}
}
Initial SSR implementation (#157) * continued ssr work * continued ssr work * prettier * all components now rendering * everything finally works * fix type issues * working on breadcrumb * working on breadcrumb * radio group * convert all tests to ssr * prettier * test suite finally passing * add layout stuff * add changelog * fix TS issue * fix tests * fixing deploy stuff * get QR code displaying * fix tests * fix tests * prettier * condense hydration stuff * prettier * comment out range test * fixing issues * use base fixtures * fixing examples * dont vendor * fix import of hydration support * adding notes * add notesg * add ssr loader * fix build * prettier * add notes * add notes * prettier * fixing bundled stuff * remove cdn * remove cdn * prettier * fiixng tests * prettier * split jobs?? * prettier * fix build stuff * add reset mouse and await aTimeout * prettier * fix improper tests * prettier * bail on first * fix linting * only test form with client * redundancy on ssr-loader?? * maybe this will work * prettier * try callout now * fix form.test.ts * fix form.test.ts * prettier * fix forms * fix forms * try again * prettier * add some awaits * prettier * comment out broken SSR tests * prettier * comment out broken SSR tests * prettier * dont skip in CI * upgrade playwright to beta * prettier * try some trickery * try some trickery * await updateComplete * try to fix form.test.ts * import hydrateable elements 1 time * prettier * fix input defaultValue issues * fix form controls to behave like their native counterpartS * add changelog entry * prettier * fix unexpected behavior with range / button
2024-09-11 10:25:42 -04:00
updated(changedProperties: PropertyValues<this>) {
super.updated(changedProperties);
// Sometimes (like with SSR -> hydration) mutators dont get applied due to race conditions. This ensures mutators get re-applied.
const library = getIconLibrary(this.library);
const svg = this.shadowRoot?.querySelector('svg');
if (svg) {
library?.mutator?.(svg);
}
}
2024-04-17 11:20:27 -04:00
render() {
Initial SSR implementation (#157) * continued ssr work * continued ssr work * prettier * all components now rendering * everything finally works * fix type issues * working on breadcrumb * working on breadcrumb * radio group * convert all tests to ssr * prettier * test suite finally passing * add layout stuff * add changelog * fix TS issue * fix tests * fixing deploy stuff * get QR code displaying * fix tests * fix tests * prettier * condense hydration stuff * prettier * comment out range test * fixing issues * use base fixtures * fixing examples * dont vendor * fix import of hydration support * adding notes * add notesg * add ssr loader * fix build * prettier * add notes * add notes * prettier * fixing bundled stuff * remove cdn * remove cdn * prettier * fiixng tests * prettier * split jobs?? * prettier * fix build stuff * add reset mouse and await aTimeout * prettier * fix improper tests * prettier * bail on first * fix linting * only test form with client * redundancy on ssr-loader?? * maybe this will work * prettier * try callout now * fix form.test.ts * fix form.test.ts * prettier * fix forms * fix forms * try again * prettier * add some awaits * prettier * comment out broken SSR tests * prettier * comment out broken SSR tests * prettier * dont skip in CI * upgrade playwright to beta * prettier * try some trickery * try some trickery * await updateComplete * try to fix form.test.ts * import hydrateable elements 1 time * prettier * fix input defaultValue issues * fix form controls to behave like their native counterpartS * add changelog entry * prettier * fix unexpected behavior with range / button
2024-09-11 10:25:42 -04:00
if (this.hasUpdated) {
return this.svg;
}
// @TODO: 16x16 is generally a safe bet. Perhaps be user setable?? `size="16x16"`, size="20x16". We just want to avoid "blowouts" with SSR.
return html`<svg part="svg" fill="currentColor" width="16" height="16"></svg>`;
2024-04-17 11:20:27 -04:00
}
}
declare global {
interface HTMLElementTagNameMap {
2023-09-08 13:45:49 -04:00
'wa-icon': WaIcon;
}
}