lion/packages/ui/components/icon/LionIcon.js
2022-10-31 16:55:07 +01:00

205 lines
4.7 KiB
JavaScript

import { css, html, LitElement, nothing, render } from 'lit';
import { isTemplateResult } from 'lit/directive-helpers.js';
import { icons } from './icons.js';
/**
* @typedef {import('@lion/core').TemplateResult} TemplateResult
* @typedef {(tag: (strings: TemplateStringsArray, ... expr: string[]) => string) => string} TagFunction
*/
/**
* @param {?} wrappedSvgObject
*/
function unwrapSvg(wrappedSvgObject) {
const svgObject =
wrappedSvgObject && wrappedSvgObject.default ? wrappedSvgObject.default : wrappedSvgObject;
return typeof svgObject === 'function' ? svgObject(html) : svgObject;
}
/**
* @param {TemplateResult|nothing} svg
*/
function validateSvg(svg) {
if (!(svg === nothing || isTemplateResult(svg))) {
throw new Error(
'icon accepts only lit-html templates or functions like "tag => tag`<svg>...</svg>`"',
);
}
}
/**
* Custom element for rendering SVG icons
*/
export class LionIcon extends LitElement {
static get properties() {
return {
/**
* @desc When icons are not loaded as part of an iconset defined on iconManager,
* it's possible to directly load an svg.
*/
svg: {
attribute: false,
},
/**
* @desc The iconId allows to access icons that are registered to the IconManager
* For instance, "lion:space:alienSpaceship"
*/
ariaLabel: {
type: String,
attribute: 'aria-label',
reflect: true,
},
/**
* @desc The iconId allows to access icons that are registered to the IconManager
* For instance, "lion:space:alienSpaceship"
*/
iconId: {
type: String,
attribute: 'icon-id',
},
/**
* @private
*/
role: {
type: String,
attribute: 'role',
reflect: true,
},
};
}
static get styles() {
return [
css`
:host {
box-sizing: border-box;
display: inline-block;
width: 1em;
height: 1em;
}
:host([hidden]) {
display: none;
}
:host:first-child {
margin-left: 0;
}
:host:last-child {
margin-right: 0;
}
::slotted(svg) {
display: block;
width: 100%;
height: 100%;
}
`,
];
}
constructor() {
super();
this.role = 'img';
this.ariaLabel = '';
this.iconId = '';
/**
* @private
* @type {TemplateResult|nothing|TagFunction}
*/
this.__svg = nothing;
}
/** @param {import('@lion/core').PropertyValues} changedProperties */
update(changedProperties) {
super.update(changedProperties);
if (changedProperties.has('ariaLabel')) {
this._onLabelChanged();
}
if (changedProperties.has('iconId')) {
this._onIconIdChanged(/** @type {string} */ (changedProperties.get('iconId')));
}
}
render() {
return html`<slot></slot>`;
}
connectedCallback() {
// ensures that aria-hidden is set if there is no aria-label attribute
this._onLabelChanged();
super.connectedCallback();
}
/**
* On IE11, svgs without focusable false appear in the tab order
* so make sure to have <svg focusable="false"> in svg files
* @param {TemplateResult|nothing|TagFunction} svg
*/
set svg(svg) {
this.__svg = svg;
if (svg === undefined || svg === null) {
this._renderSvg(nothing);
} else {
this._renderSvg(unwrapSvg(svg));
}
}
/**
* @type {TemplateResult|nothing|TagFunction}
*/
get svg() {
return this.__svg;
}
/** @protected */
_onLabelChanged() {
if (this.ariaLabel) {
this.setAttribute('aria-hidden', 'false');
} else {
this.setAttribute('aria-hidden', 'true');
this.removeAttribute('aria-label');
}
}
/**
* @param {TemplateResult | nothing} svgObject
* @protected
*/
_renderSvg(svgObject) {
validateSvg(svgObject);
render(svgObject, this);
if (this.firstElementChild) {
this.firstElementChild.setAttribute('aria-hidden', 'true');
}
}
/** @protected */
// eslint-disable-next-line class-methods-use-this
get _iconManager() {
return icons;
}
/**
* @param {string} prevIconId
* @protected
*/
async _onIconIdChanged(prevIconId) {
if (!this.iconId) {
// clear if switching from iconId to no iconId
if (prevIconId) {
this.svg = nothing;
}
} else {
const iconIdBeforeResolve = this.iconId;
const svg = await this._iconManager.resolveIconForId(iconIdBeforeResolve);
// update SVG if it did not change in the meantime to avoid race conditions
if (this.iconId === iconIdBeforeResolve) {
this.svg = svg;
}
}
}
}