feat(icon): add icon lazy loading system

This commit is contained in:
Lars den Bakker 2019-05-25 16:42:39 +02:00 committed by Thijs Louisse
parent da2729e323
commit f887f973f0
18 changed files with 556 additions and 331 deletions

View file

@ -1 +1,2 @@
export { LionIcon } from './src/LionIcon.js';
export { icons } from './src/icons.js';

View file

@ -38,6 +38,7 @@
},
"devDependencies": {
"@open-wc/demoing-storybook": "^1.10.4",
"@open-wc/testing": "^2.5.0"
"@open-wc/testing": "^2.5.0",
"sinon": "^7.2.2"
}
}

View file

@ -0,0 +1,63 @@
import { LionSingleton } from '@lion/core';
export class IconManager extends LionSingleton {
constructor(params = {}) {
super(params);
this.__iconResolvers = new Map();
}
/**
* Adds an icon resolver for the given namespace. An icon resolver is a
* function which takes an icon set and an icon name and returns an svg
* icon as a TemplateResult. This function can be sync or async.
*
* @param {string} namespace
* @param {(iconset: string, icon: string) => TemplateResult | Promise<TemplateResult>} iconResolver
*/
addIconResolver(namespace, iconResolver) {
if (this.__iconResolvers.has(namespace)) {
throw new Error(`An icon resolver has already been registered for namespace: ${namespace}`);
}
this.__iconResolvers.set(namespace, iconResolver);
}
/**
* Removes an icon resolver for a namespace.
* @param {string} namespace
*/
removeIconResolver(namespace) {
this.__iconResolvers.delete(namespace);
}
/**
* Resolves icon for the given parameters. Returns the icon as a svg string.
*
* @param {string} namespace
* @param {string} iconset
* @param {string} icon
* @returns {Promise<TemplateResult>}
*/
resolveIcon(namespace, iconset, icon) {
const resolver = this.__iconResolvers.get(namespace);
if (resolver) {
return resolver(iconset, icon);
}
throw new Error(`Could not find any icon resolver for namespace ${namespace}.`);
}
/**
* Resolves icon for the given icon id. Returns the icon as a svg string.
*
* @param {string} iconId
* @returns {Promise<TemplateResult>}
*/
resolveIconForId(iconId) {
const splitIconId = iconId.split(':');
if (splitIconId.length !== 3) {
throw new Error(`Incorrect iconId: ${iconId}. Format: <namespace>:<iconset>:<icon>`);
}
return this.resolveIcon(...splitIconId);
}
}

View file

@ -1,28 +1,61 @@
import { html, nothing, TemplateResult, css, render, LitElement } from '@lion/core';
import { icons } from './icons.js';
const isPromise = action => typeof action === 'object' && Promise.resolve(action) === action;
function unwrapSvg(wrappedSvgObject) {
const svgObject =
wrappedSvgObject && wrappedSvgObject.default ? wrappedSvgObject.default : wrappedSvgObject;
return typeof svgObject === 'function' ? svgObject(html) : svgObject;
}
function validateSvg(svg) {
if (!(svg === nothing || svg instanceof TemplateResult)) {
throw new Error(
'icon accepts only lit-html templates or functions like "tag => tag`<svg>...</svg>`"',
);
}
}
/**
* Custom element for rendering SVG icons
* @polymerElement
*/
export class LionIcon extends LitElement {
static get properties() {
return {
// svg is a property to ensure the setter is called if the property is set before upgrading
/**
* @desc When icons are not loaded as part of an iconset defined on iconManager,
* it's possible to directly load an svg.
* @type {TemplateResult|function}
*/
svg: {
type: Object,
},
role: {
type: String,
attribute: 'role',
reflect: true,
},
/**
* @desc The iconId allows to access icons that are registered to the IconManager
* For instance, "lion:space:alienSpaceship"
* @type {string}
*/
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"
* @type {string}
*/
iconId: {
type: String,
attribute: 'icon-id',
},
/**
* @private
*/
role: {
type: String,
attribute: 'role',
reflect: true,
},
};
}
@ -63,6 +96,10 @@ export class LionIcon extends LitElement {
if (changedProperties.has('ariaLabel')) {
this._onLabelChanged(changedProperties);
}
if (changedProperties.has('iconId')) {
this._onIconIdChanged(changedProperties.get('iconId'));
}
}
render() {
@ -85,16 +122,8 @@ export class LionIcon extends LitElement {
this.__svg = svg;
if (svg === undefined || svg === null) {
this._renderSvg(nothing);
} else if (isPromise(svg)) {
this._renderSvg(nothing); // show nothing before resolved
svg.then(resolvedSvg => {
// render only if it is still the same and was not replaced after loading started
if (svg === this.__svg) {
this._renderSvg(this.constructor.__unwrapSvg(resolvedSvg));
}
});
} else {
this._renderSvg(this.constructor.__unwrapSvg(svg));
this._renderSvg(unwrapSvg(svg));
}
}
@ -112,21 +141,24 @@ export class LionIcon extends LitElement {
}
_renderSvg(svgObject) {
this.constructor.__validateSvg(svgObject);
validateSvg(svgObject);
render(svgObject, this);
}
static __unwrapSvg(wrappedSvgObject) {
const svgObject =
wrappedSvgObject && wrappedSvgObject.default ? wrappedSvgObject.default : wrappedSvgObject;
return typeof svgObject === 'function' ? svgObject(html) : svgObject;
async _onIconIdChanged(prevIconId) {
if (!this.iconId) {
// clear if switching from iconId to no iconId
if (prevIconId) {
this.svg = null;
}
} else {
const iconIdBeforeResolve = this.iconId;
const svg = await icons.resolveIconForId(iconIdBeforeResolve);
static __validateSvg(svg) {
if (!(svg === nothing || svg instanceof TemplateResult)) {
throw new Error(
'icon accepts only lit-html templates or functions like "tag => tag`<svg>...</svg>`"',
);
// update SVG if it did not change in the meantime to avoid race conditions
if (this.iconId === iconIdBeforeResolve) {
this.svg = svg;
}
}
}
}

View file

@ -0,0 +1,8 @@
import { IconManager } from './IconManager.js';
// eslint-disable-next-line import/no-mutable-exports
export let icons = IconManager.getInstance();
export function setIcons(newIcons) {
icons = newIcons;
}

View file

@ -1,13 +0,0 @@
import { html } from '@open-wc/demoing-storybook';
import '../lion-icon.js';
export default {
title: 'Icons/System/_internal',
};
export const dynamicIcon = () => html`
<lion-icon
.svg=${import('./icons/bugs/bug05.svg.js')}
aria-label="Skinny dung beatle"
></lion-icon>
`;

View file

@ -0,0 +1,14 @@
import { icons } from '../index.js';
icons.addIconResolver('lion', (iconset, name) => {
switch (iconset) {
case 'bugs':
return import('./icons/iconset-bugs.js').then(module => module[name]);
case 'space':
return import('./icons/iconset-space.js').then(module => module[name]);
case 'misc':
return import('./icons/iconset-misc.js').then(module => module[name]);
default:
throw new Error(`Unknown iconset ${iconset}`);
}
});

View file

@ -0,0 +1,104 @@
import { Story, Meta, html } from '@open-wc/demoing-storybook';
import { bug24 } from './icons/iconset-bugs.js';
import '../lion-icon.js';
import './icon-resolvers.js';
<Meta title="Icons/Icon" />
# Icon
A web component for displaying icons.
<Story name="Default">
{html`
<style>
lion-icon {
width: 50px;
height: 50px;
}
</style>
<lion-icon icon-id="lion:space:alienSpaceship"></lion-icon>
`}
</Story>
```html
<lion-icon icon-id="lion:space:alienSpaceship"></lion-icon>
```
## How to use
### Installation
```sh
npm i --save @lion/icon
```
```js
import '@lion/icon/lion-icon.js';
```
### Displaying icons
Icons are displayed using icon sets. These are collections of icons, lazily loaded on demand for performance.
See the system documentation to learn more about icon sets.
<Story name="Iconset">
{html`
<lion-icon icon-id="lion:space:alienSpaceship"></lion-icon>
`}
</Story>
```html
<lion-icon icon-id="lion:space:alienSpaceship"></lion-icon>
```
If for some reason you don't want to lazy load icons, you can still import and use them
synchronously.
### Accessibility
It is recommended to add an `aria-label` to provide information to visually impaired users:
A `lion-icon` without an `aria-label` attribute will be automatically given an `aria-hidden` attribute.
<Story name="Accessible label">
{html`
<lion-icon icon-id="lion:misc:arrowLeft" aria-label="Pointing left"></lion-icon>
`}
</Story>
```html
<lion-icon icon-id="lion:misc:arrowLeft" aria-label="Pointing left"></lion-icon>
```
### Styling
By default, a `lion-icon` will be `1em` × `1em` (the current line-height).
`lion-icon` uses SVGs and may be styled with CSS, including using CSS properties such as `fill` and `stroke`:
<Story name="Styling">
{html`
<style>
lion-icon.custom {
width: 50px;
height: 50px;
fill: blue;
stroke: red;
}
</style>
<lion-icon icon-id="lion:bugs:bug02" aria-label="Pointing left"></lion-icon>
`}
</Story>
```html
<style>
lion-icon {
fill: blue;
stroke: lightsteelblue;
}
</style>
<lion-icon icon-id="lion:bugs:bug02" aria-label="Pointing left"></lion-icon>
```
See <a href="https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/SVG_and_CSS" target="_blank">SVG and CSS</a> on MDN web docs for more information.

View file

@ -0,0 +1,3 @@
import arrowLeft from './misc/arrowLeft.svg.js';
export { arrowLeft };

View file

@ -0,0 +1,31 @@
import alienSpaceship from './space/aliens-spaceship.svg.js';
import meteor from './space/meteor.svg.js';
import moonFlag from './space/moon-flag.svg.js';
import moon from './space/moon.svg.js';
import night from './space/night.svg.js';
import orbit from './space/orbit.svg.js';
import planet from './space/planet.svg.js';
import robot from './space/robot.svg.js';
import rocket from './space/rocket.svg.js';
import satellite from './space/satellite.svg.js';
import signal from './space/signal.svg.js';
import spaceHelmet from './space/space-helmet.svg.js';
import sun from './space/sun.svg.js';
import telescope from './space/telescope.svg.js';
export {
alienSpaceship,
meteor,
moonFlag,
moon,
night,
orbit,
planet,
robot,
rocket,
satellite,
signal,
spaceHelmet,
sun,
telescope,
};

View file

@ -1,174 +0,0 @@
import { Story, Meta, html } from '@open-wc/demoing-storybook';
import * as bugs from './icons/bugs-collection.js';
import arrowLeftSvg from './icons/arrowLeft.svg.js';
import '../lion-icon.js';
<Meta title="Icons/Icon" />
# Icon
A web component for displaying icons.
<Story name="Default">
{html`
<style>
lion-icon {
width: 50px;
height: 50px;
}
</style>
<lion-icon .svg="${bugs.bug01}"></lion-icon>
`}
</Story>
```html
<lion-icon .svg="${bug01}"></lion-icon>
```
## How to use
### Installation
```sh
npm i --save @lion/icon
```
```js
import '@lion/icon/lion-icon.js';
```
## Icon format
Icon file is an ES module with an extension `.svg.js` which exports a function like this:
```js
// bug.svg.js
export default tag => tag`
<svg focusable="false" ...>...</svg>
`;
```
Make sure you have `focusable="false"` in the icon file to prevent bugs in IE/Edge when the icon appears in tab-order.
### Accessibility
It is recommended to add an `aria-label` to provide information to visually impaired users:
A `lion-icon` without an `aria-label` attribute will be automatically be given an `aria-hidden` attribute.
<Story name="Accessible label">
{html`
<lion-icon .svg="${arrowLeftSvg}" aria-label="Pointing left"></lion-icon>
`}
</Story>
```html
<lion-icon .svg="${arrowLeftSvg}" aria-label="Pointing left"></lion-icon>
```
### Styling
By default, a `lion-icon` will be `1em` × `1em` (the current line-height).
`lion-icon` uses SVGs and may be styled with CSS, including using CSS properties such as `fill` and `stroke`:
<Story name="Styling">
{html`
<style>
lion-icon.custom {
width: 50px;
height: 50px;
fill: blue;
stroke: red;
}
</style>
<lion-icon class="custom" .svg="${bugs.bug02}" aria-label="Pointing left"></lion-icon>
`}
</Story>
```html
<style>
lion-icon {
fill: blue;
stroke: lightsteelblue;
}
</style>
<lion-icon .icon="${bug02}"></lion-icon>
```
See <a href="https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/SVG_and_CSS" target="_blank">SVG and CSS</a> on MDN web docs for more information.
### Collections
Due to the `.svg.js` format using ES Modules, it is very easy to compose and load your own icon collections.
You can bundle them like this:
```js
import bug01 from './bugs/bug01.svg.js';
import bug02 from './bugs/bug02.svg.js';
import bug05 from './bugs/bug05.svg.js';
import bug06 from './bugs/bug06.svg.js';
import bug08 from './bugs/bug08.svg.js';
import bug12 from './bugs/bug12.svg.js';
import bug19 from './bugs/bug19.svg.js';
import bug23 from './bugs/bug23.svg.js';
import bug24 from './bugs/bug24.svg.js';
export { bug01, bug02, bug05, bug06, bug08, bug12, bug19, bug23, bug24 };
```
<Story name="Collections">
{html`
<style>
.icon-collection lion-icon {
height: 50px;
width: 50px;
}
</style>
<div class="icon-collection">
<lion-icon .svg=${bugs.bug05} aria-label="Skinny dung beatle"></lion-icon>
<lion-icon .svg=${bugs.bug06} aria-label="Butterfly"></lion-icon>
<lion-icon .svg=${bugs.bug08} aria-label="Ant"></lion-icon>
<lion-icon .svg=${bugs.bug12} aria-label="Striped beatle"></lion-icon>
<lion-icon .svg=${bugs.bug19} aria-label="Beatle with long whiskers"></lion-icon>
<lion-icon .svg=${bugs.bug23} aria-label="Swim beatle"></lion-icon>
<lion-icon .svg=${bugs.bug24} aria-label="Big forrest ant"></lion-icon>
</div>
`}
</Story>
And then use them by either importing them all:
```js
import * as bugs from './icons/bugs-collection.js';
```
Or one by one:
```js
import {
bug01,
bug02,
bug05,
bug06,
bug08,
bug12,
bug19,
bug23,
bug24,
} from './icons/bugs-collection.js';
```
### Dynamic import
It is also possible to dynamically import the `.svg.js` file.
This will load the icon asynchronously.
<Story id="icons-system-internal--dynamic-icon" />
```js
<lion-icon .svg=${import('./icons/bugs/bug05.svg.js')} aria-label="Skinny dung beatle"></lion-icon>
```
The demo is currently disabled for this feature due to an issue with Storybook.

View file

@ -0,0 +1,135 @@
import { Story, Meta, html } from '@open-wc/demoing-storybook';
import * as bugs from './icons/iconset-bugs.js';
import '../lion-icon.js';
import './icon-resolvers.js';
<Meta title="Icons/System" />
# Icon system
The icon system provides a way of defining icon sets which are lazily loaded on demand when
icons are rendered on the page. This way icon imports do not block the initial render of your
application, and you don't need to worry about carefully coordinating the dynamic imports of your icons.
## Icon format
For security reasons, icons are defined using lit-html templates to guarantee XSS safety:
```js
import html from 'lit-html';
export default html`
<svg focusable="false" ...>...</svg>
`;
```
The icon can also be a function. In this case, it's possible to reuse the icons if a
rendering mechanism different from lit-html is used to process a string.
```js
export default tag => tag`
<svg focusable="false" ...>...</svg>
`;
```
This ensures the same version of lit-html and the icon system used. This is the recommended approach.
### Icon accessibility
On IE11 and some versions of Edge, SVG elements are focusable by default.
Setting `focusable="false"` on the SVG prevents this.
## Iconsets
Requesting many individual icons can be bad for performance. We should, therefore, group related icons
together in icon sets.
Iconsets are managed by the `IconManager`, where you can register icon resolvers to resolve an icon id
to the correct icon.
### Creating an icon resolver
An icon resolver is a function that receives the icon set and the icon name and subsequently
returns the icon to be rendered.
The most common use case is for this function to be async, and import the icon set on demand:
```js
function resolveLionIcon(iconset, name) {
switch (iconset) {
case 'bugs':
return import('./icons/iconset-bugs.js').then(module => module[name]);
case 'space':
return import('./icons/iconset-space.js').then(module => module[name]);
case 'misc':
return import('./icons/iconset-misc.js').then(module => module[name]);
default:
throw new Error(`Unknown iconset ${iconset}`);
}
}
```
An icon resolver can also be synchronous, returning the icon directly:
```js
const icons = {
coolIcons: {
'my-icon': html`
<svg>... icon code ...</svg>
`,
},
};
function resolveLionIcon(iconset, name) {
return coolIcons[iconSets][name];
}
```
### Registering an icon resolver
Icon resolvers are registered in the `IconManager` on a namespace. There can be only one resolver per namespace, so
make sure they are unique. A good idea is to use your package name as the namespace.
```js
import { icons } from '@lion/icon';
function resolveLionIcon(iconset, name) {
switch (iconset) {
case 'bugs':
return import('./icons/iconset-bugs.js').then(module => module[name]);
case 'space':
return import('./icons/iconset-space.js').then(module => module[name]);
case 'misc':
return import('./icons/iconset-misc.js').then(module => module[name]);
default:
throw new Error(`Unknown iconset ${iconset}`);
}
}
icons.addIconResolver('lion', resolveLionIcon);
```
### Using icon resolvers
After register an icon resolver, icons can be resolved from the manager:
```js
import { icons } from '@lion/icon';
const spaceshipIcon = await icons.resolveIcon('lion', 'space', 'alienSpaceship');
```
Icons can also be resolved from a single string, using the pattern: `namespace:iconset:name`:
```js
import { icons } from '@lion/icon';
const spaceshipIcon = await icons.resolveIconForId('lion:space:alienSpaceship');
```
This syntax is used by the `lion-icon` component, where the id can be set on an attribute:
```html
<lion-icon icon-id="lion:space:alienSpaceship"></lion-icon>
<lion-icon icon-id="lion:misc:arrowLeft"></lion-icon>
```

View file

@ -0,0 +1,74 @@
import { expect } from '@open-wc/testing';
import { stub } from 'sinon';
import { IconManager } from '../src/IconManager.js';
describe('IconManager', () => {
it('starts off with an empty map of resolvers', () => {
const manager = new IconManager();
expect(manager.__iconResolvers.size).to.equal(0);
});
it('allows adding an icon resolver', () => {
const manager = new IconManager();
const resolver = () => {};
manager.addIconResolver('foo', resolver);
expect(manager.__iconResolvers.get('foo')).to.equal(resolver);
});
it('does not allow adding a resolve for the same namespace twice', () => {
const manager = new IconManager();
manager.addIconResolver('foo', () => {});
expect(() => manager.addIconResolver('foo', () => {})).to.throw();
});
it('can resolve an icon, specifying separate parameters', async () => {
const manager = new IconManager();
const fooResolver = stub();
fooResolver.callsFake(() => 'my icon');
const barResolver = stub();
manager.addIconResolver('foo', fooResolver);
manager.addIconResolver('bar', barResolver);
const icon = await manager.resolveIcon('foo', 'lorem', 'ipsum');
expect(fooResolver.callCount).to.equal(1);
expect(barResolver.callCount).to.equal(0);
expect(fooResolver.withArgs('lorem', 'ipsum').callCount).to.equal(1);
expect(icon).to.equal('my icon');
});
it('throws when an incorrect namespace is given', async () => {
const manager = new IconManager();
const fooResolver = stub();
fooResolver.callsFake(() => 'my icon');
manager.addIconResolver('foo', fooResolver);
expect(() => manager.resolveIcon('bar', 'lorem', 'ipsum')).to.throw();
});
it('can resolve an icon, specifying parameters as a single string', async () => {
const manager = new IconManager();
const fooResolver = stub();
fooResolver.callsFake(() => 'my icon');
manager.addIconResolver('foo', fooResolver);
const icon = await manager.resolveIconForId('foo:lorem:ipsum');
expect(fooResolver.callCount).to.equal(1);
expect(fooResolver.withArgs('lorem', 'ipsum').callCount).to.equal(1);
expect(icon).to.equal('my icon');
});
it('throws when an incorrectly formatted icon id is given', async () => {
const manager = new IconManager();
const fooResolver = stub();
fooResolver.callsFake(() => 'my icon');
manager.addIconResolver('foo', fooResolver);
expect(() => manager.resolveIconForId('lorem:ipsum')).to.throw();
expect(() => manager.resolveIconForId('lorem')).to.throw();
expect(() => manager.resolveIconForId('foo:lorem:ipsum:bar')).to.throw();
});
});

View file

@ -1,5 +1,6 @@
import { expect, fixture, fixtureSync, aTimeout, html } from '@open-wc/testing';
import { until, render } from '@lion/core';
import { until } from '@lion/core';
import { icons } from '../src/icons.js';
import heartSvg from './heart.svg.js';
import hammerSvg from './hammer.svg.js';
@ -142,45 +143,6 @@ describe('lion-icon', () => {
expect(elHammer.getAttribute('aria-hidden')).to.equal('false');
});
it('supports dynamic icons using promises', async () => {
const el = await fixture(
html`
<lion-icon
.svg=${import('./heart.svg.js').then(e => e.default)}
aria-label="Love"
></lion-icon>
`,
);
await el.svg;
await el.updateComplete;
expect(el.children[0].getAttribute('data-test-id')).to.equal('svg-heart');
});
it('uses the default export, by default', async () => {
const el = await fixture(
html`
<lion-icon .svg=${import('./heart.svg.js')} aria-label="Love"></lion-icon>
`,
);
await el.svg;
await el.updateComplete;
expect(el.children[0].getAttribute('data-test-id')).to.equal('svg-heart');
});
it('supports dynamic icon bundles', async () => {
const el = await fixture(
html`
<lion-icon
.svg=${import('./myIcon.bundle.js').then(e => e.heart)}
aria-label="Love"
></lion-icon>
`,
);
await el.svg;
await el.updateComplete;
expect(el.children[0].getAttribute('data-test-id')).to.equal('svg-heart');
});
it('supports dynamic icons using until directive', async () => {
const svgLoading = new Promise(resolve => {
window.addEventListener('importDone', resolve);
@ -233,84 +195,67 @@ describe('lion-icon', () => {
expect(el.innerHTML).to.equal('<!----><!---->'); // don't use lightDom.to.equal(''), it gives false positives
});
describe('race conditions with dynamic promisified icons', () => {
async function prepareRaceCondition(...svgs) {
const container = fixtureSync(`<div></div>`);
const resolves = svgs.map(svg => {
let resolveSvg;
const svgProperty =
Promise.resolve(svg) === svg
? new Promise(resolve => {
resolveSvg = () => resolve(svg);
})
: svg;
render(
it('supports icons using an icon id', async () => {
try {
icons.addIconResolver('foo', () => heartSvg);
const el = await fixture(
html`
<lion-icon .svg=${svgProperty}></lion-icon>
<lion-icon icon-id="foo:lorem:ipsum"></lion-icon>
`,
container,
);
return resolveSvg;
});
const icon = container.children[0];
await icon.updateComplete;
return [icon, ...resolves];
expect(el.children[0].dataset.testId).to.equal('svg-heart');
} finally {
icons.removeIconResolver('foo');
}
});
it('renders in the order of rendering instead of the order of resolution', async () => {
let resolveHeartSvg;
let resolveHammerSvg;
let icon;
let svg;
[icon, resolveHeartSvg, resolveHammerSvg] = await prepareRaceCondition(
Promise.resolve(heartSvg),
Promise.resolve(hammerSvg),
it('clears rendered icon when icon id is removed', async () => {
try {
icons.addIconResolver('foo', () => heartSvg);
const el = await fixture(
html`
<lion-icon icon-id="foo:lorem:ipsum"></lion-icon>
`,
);
resolveHeartSvg();
resolveHammerSvg();
await aTimeout();
[svg] = icon.children;
expect(svg).to.exist;
expect(svg.getAttribute('data-test-id')).to.equal('svg-hammer');
[icon, resolveHeartSvg, resolveHammerSvg] = await prepareRaceCondition(
Promise.resolve(heartSvg),
Promise.resolve(hammerSvg),
);
resolveHammerSvg();
resolveHeartSvg();
await aTimeout();
[svg] = icon.children;
expect(svg).to.exist;
expect(svg.getAttribute('data-test-id')).to.equal('svg-hammer');
await el.updateComplete;
el.removeAttribute('icon-id');
await el.updateComplete;
expect(el.children.length).to.equal(0);
} finally {
icons.removeIconResolver('foo');
}
});
it('renders if a resolved promise was replaced by a string', async () => {
const [icon, resolveHeartSvg] = await prepareRaceCondition(
Promise.resolve(heartSvg),
hammerSvg,
it('does not create race conditions when icon changed while resolving icon id', async () => {
try {
icons.addIconResolver(
'foo',
() => new Promise(resolve => setTimeout(() => resolve(heartSvg), 10)),
);
icons.addIconResolver(
'bar',
() => new Promise(resolve => setTimeout(() => resolve(hammerSvg), 4)),
);
const el = await fixture(
html`
<lion-icon icon-id="foo:lorem:ipsum"></lion-icon>
`,
);
resolveHeartSvg();
await aTimeout();
const [svg] = icon.children;
expect(svg).to.exist;
expect(svg.getAttribute('data-test-id')).to.equal('svg-hammer');
});
it('does not render if a resolved promise was replaced by another unresolved promise', async () => {
const [icon, resolveHeartSvg] = await prepareRaceCondition(
Promise.resolve(heartSvg),
Promise.resolve(hammerSvg),
);
resolveHeartSvg();
await aTimeout();
const [svg] = icon.children;
expect(svg).to.not.exist;
});
await el.updateComplete;
el.iconId = 'bar:lorem:ipsum';
await el.updateComplete;
await aTimeout(4);
// heart is still loading at this point, but hammer came later so that should be later
expect(el.children[0].dataset.testId).to.equal('svg-hammer');
await aTimeout(10);
// heart finished loading, but it should not be rendered because hammer came later
expect(el.children[0].dataset.testId).to.equal('svg-hammer');
} finally {
icons.removeIconResolver('foo');
icons.removeIconResolver('bar');
}
});
});

View file

@ -1 +0,0 @@
export { default as heart } from './heart.svg.js';

View file

@ -4,10 +4,12 @@ import { Story, Meta, html } from '@open-wc/demoing-storybook';
# Icons
Icon system for managing iconsets, taking into account performance, maintainability and scalability.
Icons are SVGs so they can be easily scaled and styled with CSS.
## Packages
| Package | Version | Description |
| --------------------------------------------- | --------------------------------------------------------------------------------------------------- | --------------- |
| --------------------------------------------- | ------------------------------------------------------------------------------------------------ | ----------- |
| [icon](?path=/docs/icons-icon--default-story) | [![icon](https://img.shields.io/npm/v/@lion/icon.svg)](https://www.npmjs.com/package/@lion/icon) | Icon |