diff --git a/packages/icon/index.js b/packages/icon/index.js index 119bffad6..8d039f127 100644 --- a/packages/icon/index.js +++ b/packages/icon/index.js @@ -1 +1,2 @@ export { LionIcon } from './src/LionIcon.js'; +export { icons } from './src/icons.js'; diff --git a/packages/icon/package.json b/packages/icon/package.json index b07555115..689188e02 100644 --- a/packages/icon/package.json +++ b/packages/icon/package.json @@ -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" } } diff --git a/packages/icon/src/IconManager.js b/packages/icon/src/IconManager.js new file mode 100644 index 000000000..fefd7c142 --- /dev/null +++ b/packages/icon/src/IconManager.js @@ -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} 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} + */ + 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} + */ + resolveIconForId(iconId) { + const splitIconId = iconId.split(':'); + if (splitIconId.length !== 3) { + throw new Error(`Incorrect iconId: ${iconId}. Format: ::`); + } + + return this.resolveIcon(...splitIconId); + } +} diff --git a/packages/icon/src/LionIcon.js b/packages/icon/src/LionIcon.js index 3a36a28b3..10e528bcf 100644 --- a/packages/icon/src/LionIcon.js +++ b/packages/icon/src/LionIcon.js @@ -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`...`"', + ); + } +} /** * 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`...`"', - ); + // update SVG if it did not change in the meantime to avoid race conditions + if (this.iconId === iconIdBeforeResolve) { + this.svg = svg; + } } } } diff --git a/packages/icon/src/icons.js b/packages/icon/src/icons.js new file mode 100644 index 000000000..825506836 --- /dev/null +++ b/packages/icon/src/icons.js @@ -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; +} diff --git a/packages/icon/stories/dynamic-icon.stories.js b/packages/icon/stories/dynamic-icon.stories.js deleted file mode 100644 index f8d90d110..000000000 --- a/packages/icon/stories/dynamic-icon.stories.js +++ /dev/null @@ -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` - -`; diff --git a/packages/icon/stories/icon-resolvers.js b/packages/icon/stories/icon-resolvers.js new file mode 100644 index 000000000..ca6267d84 --- /dev/null +++ b/packages/icon/stories/icon-resolvers.js @@ -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}`); + } +}); diff --git a/packages/icon/stories/icon.stories.mdx b/packages/icon/stories/icon.stories.mdx new file mode 100644 index 000000000..2eba24da7 --- /dev/null +++ b/packages/icon/stories/icon.stories.mdx @@ -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'; + + + +# Icon + +A web component for displaying icons. + + + {html` + + + `} + + +```html + +``` + +## 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. + + + {html` + + `} + + +```html + +``` + +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. + + + {html` + + `} + + +```html + +``` + +### 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`: + + + {html` + + + `} + + +```html + + +``` + +See SVG and CSS on MDN web docs for more information. diff --git a/packages/icon/stories/icons/bugs-collection.js b/packages/icon/stories/icons/iconset-bugs.js similarity index 100% rename from packages/icon/stories/icons/bugs-collection.js rename to packages/icon/stories/icons/iconset-bugs.js diff --git a/packages/icon/stories/icons/iconset-misc.js b/packages/icon/stories/icons/iconset-misc.js new file mode 100644 index 000000000..7541a4a01 --- /dev/null +++ b/packages/icon/stories/icons/iconset-misc.js @@ -0,0 +1,3 @@ +import arrowLeft from './misc/arrowLeft.svg.js'; + +export { arrowLeft }; diff --git a/packages/icon/stories/icons/iconset-space.js b/packages/icon/stories/icons/iconset-space.js new file mode 100755 index 000000000..56b4a217b --- /dev/null +++ b/packages/icon/stories/icons/iconset-space.js @@ -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, +}; diff --git a/packages/icon/stories/icons/arrowLeft.svg.js b/packages/icon/stories/icons/misc/arrowLeft.svg.js similarity index 100% rename from packages/icon/stories/icons/arrowLeft.svg.js rename to packages/icon/stories/icons/misc/arrowLeft.svg.js diff --git a/packages/icon/stories/index.stories.mdx b/packages/icon/stories/index.stories.mdx deleted file mode 100644 index 0c7ce5e89..000000000 --- a/packages/icon/stories/index.stories.mdx +++ /dev/null @@ -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'; - - - -# Icon - -A web component for displaying icons. - - - {html` - - - `} - - -```html - -``` - -## 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` - ... -`; -``` - -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. - - - {html` - - `} - - -```html - -``` - -### 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`: - - - {html` - - - `} - - -```html - - -``` - -See SVG and CSS 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 }; -``` - - - {html` - -
- - - - - - - -
- `} -
- -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. - - - -```js - -``` - -The demo is currently disabled for this feature due to an issue with Storybook. diff --git a/packages/icon/stories/system.stories.mdx b/packages/icon/stories/system.stories.mdx new file mode 100644 index 000000000..9705b552c --- /dev/null +++ b/packages/icon/stories/system.stories.mdx @@ -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'; + + + +# 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` + ... +`; +``` + +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` + ... +`; +``` + +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` + ... icon code ... + `, + }, +}; + +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 + + +``` diff --git a/packages/icon/test/IconManager.test.js b/packages/icon/test/IconManager.test.js new file mode 100644 index 000000000..09c6ee1e1 --- /dev/null +++ b/packages/icon/test/IconManager.test.js @@ -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(); + }); +}); diff --git a/packages/icon/test/lion-icon.test.js b/packages/icon/test/lion-icon.test.js index 00b09ae32..04dbf647e 100644 --- a/packages/icon/test/lion-icon.test.js +++ b/packages/icon/test/lion-icon.test.js @@ -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` - e.default)} - aria-label="Love" - > - `, - ); - 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` - - `, - ); - 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` - e.heart)} - aria-label="Love" - > - `, - ); - 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(`
`); - const resolves = svgs.map(svg => { - let resolveSvg; + it('supports icons using an icon id', async () => { + try { + icons.addIconResolver('foo', () => heartSvg); + const el = await fixture( + html` + + `, + ); - const svgProperty = - Promise.resolve(svg) === svg - ? new Promise(resolve => { - resolveSvg = () => resolve(svg); - }) - : svg; - - render( - html` - - `, - 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` + + `, ); - resolveHeartSvg(); - resolveHammerSvg(); - 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'); + } + }); - [icon, resolveHeartSvg, resolveHammerSvg] = await prepareRaceCondition( - Promise.resolve(heartSvg), - Promise.resolve(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` + + `, ); - resolveHammerSvg(); - resolveHeartSvg(); - await aTimeout(); - [svg] = icon.children; - expect(svg).to.exist; - expect(svg.getAttribute('data-test-id')).to.equal('svg-hammer'); - }); - it('renders if a resolved promise was replaced by a string', async () => { - const [icon, resolveHeartSvg] = await prepareRaceCondition( - Promise.resolve(heartSvg), - hammerSvg, - ); - resolveHeartSvg(); - await aTimeout(); - const [svg] = icon.children; - expect(svg).to.exist; - expect(svg.getAttribute('data-test-id')).to.equal('svg-hammer'); - }); + await el.updateComplete; + el.iconId = 'bar:lorem:ipsum'; + await el.updateComplete; + await aTimeout(4); - 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; - }); + // 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'); + } }); }); diff --git a/packages/icon/test/myIcon.bundle.js b/packages/icon/test/myIcon.bundle.js deleted file mode 100644 index 82abcddd3..000000000 --- a/packages/icon/test/myIcon.bundle.js +++ /dev/null @@ -1 +0,0 @@ -export { default as heart } from './heart.svg.js'; diff --git a/packages/intros/stories/icon-intro.stories.mdx b/packages/intros/stories/icon-intro.stories.mdx index b04e3fb33..a878edcee 100644 --- a/packages/intros/stories/icon-intro.stories.mdx +++ b/packages/intros/stories/icon-intro.stories.mdx @@ -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 | +| 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 |