lion/packages/icon/src/LionIcon.js
Mikhail Bashkirov a6b0780d4f feat(icon): enforce icon security using tagged templates
BREAKING: Icon definition requires a function using a tag passed via arguments:

```js
// myicon.svg.js

// before
export default '<svg>...</svg>';

// after
export default tag => tag`<svg>...</svg>`;
```

Application developers have an alternative shortcut to use in-place with lit-html:

```js
// MyComponent.js

// before
render() {
  return html`
    <lion-icon .svg="${'<svg>...</svg>'}"></lion-icon>
  `;
}

// after
render() {
  return html`
    <lion-icon .svg="${html`<svg>...</svg>`}"></lion-icon>
  `;
}
```
2019-07-01 17:48:05 +02:00

132 lines
3 KiB
JavaScript

import { html, nothing, TemplateResult, css, render, LitElement } from '@lion/core';
const isPromise = action => typeof action === 'object' && Promise.resolve(action) === action;
/**
* 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
svg: {
type: Object,
},
role: {
type: String,
attribute: 'role',
reflect: true,
},
ariaLabel: {
type: String,
attribute: 'aria-label',
reflect: true,
},
};
}
static get styles() {
return [
css`
:host {
box-sizing: border-box;
display: inline-block;
width: 1em;
height: 1em;
}
: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';
}
update(changedProperties) {
super.update(changedProperties);
if (changedProperties.has('ariaLabel')) {
this._onLabelChanged(changedProperties);
}
}
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
*/
set svg(svg) {
this.__svg = svg;
if (svg === undefined) {
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));
}
}
get svg() {
return this.__svg;
}
_onLabelChanged() {
if (this.ariaLabel) {
this.setAttribute('aria-hidden', 'false');
} else {
this.setAttribute('aria-hidden', 'true');
this.removeAttribute('aria-label');
}
}
_renderSvg(svgObject) {
this.constructor.__validateSvg(svgObject);
render(svgObject, this);
}
static __unwrapSvg(wrappedSvgObject) {
const svgObject =
wrappedSvgObject && wrappedSvgObject.default ? wrappedSvgObject.default : wrappedSvgObject;
return typeof svgObject === 'function' ? svgObject(html) : svgObject;
}
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>`"',
);
}
}
}