diff --git a/packages/library/components/image/src/image-component.js b/packages/library/components/image/src/image-component.js
index a49489ff..291b290c 100644
--- a/packages/library/components/image/src/image-component.js
+++ b/packages/library/components/image/src/image-component.js
@@ -1,12 +1,16 @@
-import { MuonElement, css, html, unsafeCSS } from '@muon/library';
+import { MuonElement, css, html, unsafeCSS, styleMap, classMap } from '@muon/library';
+import { imageInlineLoader, imageBackgroundLoader } from '@muon/library/directives/image-loader';
import {
- IMAGE_TYPE
+ IMAGE_TYPE,
+ IMAGE_RATIOS,
+ IMAGE_RATIO,
+ IMAGE_PLACEHOLDER
} from '@muon/library/build/tokens/es6/muon-tokens';
import styles from './styles.css';
/**
- * Lazy loading images
+ * Loading images with default lazy loading
*
* @element image
*
@@ -17,10 +21,13 @@ export class Image extends MuonElement {
static get properties() {
return {
background: { type: Boolean },
- backgroundsize: { type: String },
- src: { type: String, reflect: true },
+ backgroundsize: { type: String, attribute: 'background-size' },
+ src: { type: String },
alt: { type: String },
- ratio: { type: String } // 1x1, 4x3, 16x9:
+ ratio: { type: String },
+ placeholder: { type: String },
+ loading: { type: String },
+ _ratios: { type: Array, state: true }
};
}
@@ -34,120 +41,57 @@ export class Image extends MuonElement {
this.type = IMAGE_TYPE;
this.background = false;
this.backgroundsize = 'cover'; // cover, contain
- this.ratio = '';
- }
-
- backgroundStyles() {
- const preImg = `${this.src}.thumb.48.48.png`;
+ this.alt = '';
+ this.ratio = IMAGE_RATIO;
+ this.placeholder = IMAGE_PLACEHOLDER;
+ this.loading = 'lazy'; // eager|lazy
+ this._ratios = IMAGE_RATIOS;
- return html`
-
- `;
}
- updated() {
- const src = this.src;
-
- if (src) {
- const options = {
- threshold: 0.01,
- rootMargin: '150px'
- };
-
- const fetchImage = (url) => {
- return new Promise((resolve, reject) => {
- const image = new Image();
-
- image.src = url;
- image.onload = resolve;
- image.onerror = reject;
-
- return resolve();
- });
- };
-
- const elementView = (target, image) => {
- target.style.backgroundImage = `url(${image})`;
- target.style.backgroundSize = `${this.backgroundsize}`;
- };
-
- const imgView = (target, image) => {
- target.src = image;
- target.alt = this.alt || '';
- };
-
- const switchImage = (target, image, io) => {
- if (this.background) {
- elementView(target, image);
- } else {
- imgView(target, image);
- }
-
- const blurElem = this.shadowRoot.querySelector('.blur');
-
- if (io && target) {
- io.unobserve(target); // only unobserve after the image has loaded
- }
-
- if (blurElem) {
- blurElem.classList.remove('blur');
- }
- };
-
- const io = new IntersectionObserver((entries) => {
- entries.forEach((entry) => {
- if (entry.intersectionRatio > 0) {
- const target = entry.target;
-
- fetchImage(src).then(() => {
- switchImage(target, src, io);
- }).catch(() => {
- fetchImage(src).then(() => {
- switchImage(target, src, io);
- }).catch((e) => {
- console.info(e); // to stop headless builds from breaking
- });
- });
- }
- });
- }, options);
-
- const targetElements = this.shadowRoot.querySelectorAll('.image-lazy, .image-holder');
- for (const element of targetElements) {
- io.observe(element);
- }
- }
+ get placeholderImage() {
+ return this.placeholder.replace('(src)', this.src); // @TODO: test alternative ways for this
}
get standardTemplate() {
const isBackground = this.background;
- let ratioClass = 'no-ratio';
- if (isBackground) {
- ratioClass = 'ar16x9'; // without a default size background images won't show
+ if (!this._ratios.includes(this.ratio)) {
+ this.ratio = IMAGE_RATIO; // @TODO: add fallback `|| this._ratios[0]`
}
- if (this.ratio.length !== 0) {
- ratioClass = `ar${this.ratio}`;
+ if (isBackground) {
+ this.ratio = this.ratio?.length > 0 ? this.ratio : '16 / 9'; // without a default size background images won't show
}
+ const [x, y] = this.ratio.split(' / ');
+ const styles = {
+ '--image-ratio': CSS?.supports('aspect-ratio', '1 / 1') && this.ratio ? this.ratio : undefined,
+ '--image-padding': CSS?.supports('aspect-ratio', '1 / 1') || !x && !y ? undefined : `${y / x * 100}%`,
+ '--background-size': isBackground ? this.backgroundsize : undefined
+ };
+
+ const classes = {
+ image: true,
+ 'no-ratio': !this.ratio || this.ratio?.length < 1,
+ 'is-background': isBackground
+ };
+
if (this.src && this.src.length > 0) {
- const preImg = `${this.src}.thumb.48.48.png`;
- const lazyLoading = window.chrome ? `loading="lazy"` : ``;
+ const imageObj = {
+ src: this.src,
+ alt: this.alt,
+ placeholder: this.placeholderImage,
+ loading: this.loading
+ };
return html`
- ${isBackground ? this.backgroundStyles() : ''}
-
- ${isBackground ? html`
` : html`

`}
+
+ ${isBackground ? imageBackgroundLoader(imageObj) : imageInlineLoader(imageObj)}
`;
} else {
- return html`
`;
+ return html`
`;
}
}
}
diff --git a/packages/library/components/image/src/styles.css b/packages/library/components/image/src/styles.css
index 3b114703..53ada548 100644
--- a/packages/library/components/image/src/styles.css
+++ b/packages/library/components/image/src/styles.css
@@ -10,35 +10,26 @@
width: 100%;
overflow: hidden;
box-sizing: border-box;
- min-width: 1px;
- min-height: 1px;
+ min-width: 1px; /* Enforces a size to be observable */
+ min-height: 1px; /* Enforces a size to be observable */
}
& .image {
- transition: all 0.5s;
position: relative;
+ aspect-ratio: var(--image-ratio);
- &.ar16x9 {
- padding-bottom: 56.25%;
+ @supports not (aspect-ratio: 1/1) {
+ padding-top: var(--image-padding);
}
- &.ar1x1 {
- padding-bottom: 100%;
- }
-
- &.ar4x3 {
- padding-bottom: 75%;
- }
-
- &.blur {
+ & .blur {
filter: blur(0.7em);
transform: scale(1.03);
transition-timing-function: ease-out;
}
- & .image-holder {
- background-repeat: no-repeat;
- background-position: center center;
+ & .blur-out {
+ animation: blurOut 0.5s ease-out;
}
& .image-holder,
@@ -50,6 +41,15 @@
right: 0;
}
+ &.is-background {
+ & .image-holder {
+ background-repeat: no-repeat;
+ background-position: center center;
+ background-image: var(--background-image);
+ background-size: var(--background-size);
+ }
+ }
+
&.no-ratio {
padding: 0;
@@ -59,3 +59,13 @@
}
}
}
+
+@keyframes blurOut {
+ 0% {
+ filter: blur(0.7em);
+ }
+
+ 100% {
+ filter: blur(0);
+ }
+}
diff --git a/packages/library/components/image/src/token.json b/packages/library/components/image/src/token.json
index 7c240877..b72c9dca 100644
--- a/packages/library/components/image/src/token.json
+++ b/packages/library/components/image/src/token.json
@@ -1,7 +1,23 @@
{
"image": {
"type": {
- "value": "standard"
+ "description": "Default value for `type` property which renders the template",
+ "value": "standard",
+ "type": "string"
+ },
+ "ratios": {
+ "description": "Default value for `ratios` available for ratio",
+ "value": ["1 / 1", "4 / 3", "16 / 9"]
+ },
+ "ratio": {
+ "description": "Default value for `ratio` property which decides the ratio size for the image",
+ "value": "16 / 9",
+ "type": "string"
+ },
+ "placeholder": {
+ "description": "Default value for `placeholder` property which creates the thumbnail placeholder image. (src) can be used to bring in the src property",
+ "value": "",
+ "type": "string"
}
}
}
\ No newline at end of file
diff --git a/packages/library/components/image/story.js b/packages/library/components/image/story.js
index 4b397071..fcdb7f90 100644
--- a/packages/library/components/image/story.js
+++ b/packages/library/components/image/story.js
@@ -6,7 +6,7 @@ const details = setup('image', Image);
export default details.defaultValues;
export const Standard = (args) => details.template(args, (args) => args.text);
-Standard.args = { src: 'https://www.britishgas.co.uk/aem6/content/dam/britishgas/images/ns/homepage/engineer-van-homepage.jpg' };
+Standard.args = { src: 'https://www.britishgas.co.uk/aem6/content/dam/britishgas/images/smart-meters/Technology/Lockup%202.png', placeholder: '(src).thumb.48.48.png' };
export const Background = (args) => details.template(args, (args) => args.text);
-Background.args = { src: 'https://www.britishgas.co.uk/aem6/content/dam/britishgas/images/ns/homepage/engineer-van-homepage.jpg', background: true };
+Background.args = { src: 'https://www.britishgas.co.uk/aem6/content/dam/britishgas/images/ns/homepage/engineer-van-homepage.jpg', placeholder: '(src).thumb.48.48.png', background: true };
diff --git a/packages/library/directives/image-loader.js b/packages/library/directives/image-loader.js
new file mode 100644
index 00000000..e078e955
--- /dev/null
+++ b/packages/library/directives/image-loader.js
@@ -0,0 +1,114 @@
+import { AsyncDirective, directive, html, until, styleMap, ifDefined } from '@muon/library';
+export class ImageLoaderDirective extends AsyncDirective {
+ constructor(partInfo) {
+ super(partInfo);
+
+ this.src = '';
+ this.alt = '';
+ this.placeholder = '';
+ this.loading = '';
+ this.image = undefined;
+ }
+
+ async fetchImage() {
+ return new Promise((resolve, reject) => {
+ this.image = new Image();
+
+ this.image.src = this.src;
+ this.image.alt = this.alt;
+ this.image.classList.add('blur-out', 'image-lazy');
+ this.image.onload = () => resolve(this.image);
+ this.image.onerror = () => reject();
+ });
+ }
+
+ observer(parts, attributes) {
+ return new Promise((resolve) => {
+ const options = {
+ threshold: 0.01,
+ rootMargin: '150px'
+ };
+
+ const io = new IntersectionObserver((entries) => {
+ /* eslint-disable consistent-return */
+ return entries.forEach((entry) => {
+ if (!this.image && entry.intersectionRatio > 0) {
+ return resolve(this.render(attributes[0]));
+ }
+ });
+ }, options);
+
+ const observe = parts.parentNode;
+ io.observe(observe);
+ });
+ }
+
+ update(parts, attributes) {
+ if (attributes?.[0]?.loading === 'lazy') {
+ return html`${until(this.observer(parts, attributes), undefined)}`;
+ }
+
+ return this.render(attributes[0]);
+ }
+}
+
+export class ImageInlineLoaderDirective extends ImageLoaderDirective {
+ render({ src, alt, placeholder, loading = 'lazy' }) {
+ const loadingAttribute = window.chrome ? loading : undefined;
+
+ this.src = src;
+ this.alt = alt;
+ this.placeholder = placeholder;
+ this.loading = loading;
+
+ if (this.placeholder) {
+ this.setValue(html`

`);
+ }
+
+ Promise.resolve(this.fetchImage()).then((image) => {
+ if (image) {
+ dispatchEvent(new CustomEvent('image-loaded', { bubbles: true }));
+ this.setValue(image);
+ }
+ }).catch(() => {
+ console.error(`Image (${this.src}) failed to load`);
+ });
+
+ return undefined;
+ }
+}
+
+export class ImageBackgroundLoaderDirective extends ImageLoaderDirective {
+ render({ src, alt, placeholder, loading = 'lazy' }) {
+ this.src = src;
+ this.alt = alt;
+ this.placeholder = placeholder;
+ this.loading = loading;
+
+ const styles = {
+ '--background-image': `url("${this.placeholder}")`
+ };
+
+ if (this.placeholder) {
+ this.setValue(html`
`);
+ }
+
+ Promise.resolve(this.fetchImage()).then((image) => {
+ if (image) {
+ const styles = {
+ '--background-image': `url("${this.src}")`
+ };
+
+ dispatchEvent(new CustomEvent('image-loaded'));
+ this.setValue(html`
`);
+ }
+ }).catch(() => {
+ console.error(`Image (${this.src}) failed to load`);
+ });
+
+ return undefined;
+ }
+}
+
+export const imageInlineLoader = directive(ImageInlineLoaderDirective);
+export const imageBackgroundLoader = directive(ImageBackgroundLoaderDirective);
diff --git a/packages/library/tests/components/image/__snapshots__/image.test.snap.js b/packages/library/tests/components/image/__snapshots__/image.test.snap.js
new file mode 100644
index 00000000..14975adf
--- /dev/null
+++ b/packages/library/tests/components/image/__snapshots__/image.test.snap.js
@@ -0,0 +1,195 @@
+/* @web/test-runner snapshot v1 */
+export const snapshots = {};
+
+snapshots["icon implements standard self"] =
+`
+
+`;
+/* end snapshot icon implements standard self */
+
+snapshots["image implements standard self"] =
+`
+
+`;
+/* end snapshot image implements standard self */
+
+snapshots["image implements src image"] =
+`
+

+
+`;
+/* end snapshot image implements src image */
+
+snapshots["image implements ratio"] =
+`
+

+
+`;
+/* end snapshot image implements ratio */
+
+snapshots["image implements background"] =
+`
+`;
+/* end snapshot image implements background */
+
+snapshots["image implements placeholder image"] =
+`
+
+`;
+/* end snapshot image implements placeholder image */
+
+snapshots["image implements alt"] =
+`
+

+
+`;
+/* end snapshot image implements alt */
+
+snapshots["image fallsback on ratio if not correct"] =
+`
+

+
+`;
+/* end snapshot image fallsback on ratio if not correct */
+
+snapshots["image fallsback on image padding if aspect-ratio not available"] =
+`
+

+
+`;
+/* end snapshot image fallsback on image padding if aspect-ratio not available */
+
+snapshots["image image fails to loads"] =
+`
+
+`;
+/* end snapshot image image fails to loads */
+
+snapshots["image image fails to load"] =
+`
+
+`;
+/* end snapshot image image fails to load */
+
+snapshots["image image is eager"] =
+`
+

+
+`;
+/* end snapshot image image is eager */
+
+snapshots["image image fails to load for background"] =
+`
+
+`;
+/* end snapshot image image fails to load for background */
+
+snapshots["image implements placeholder image for background"] =
+`
+
+`;
+/* end snapshot image implements placeholder image for background */
+
+snapshots["image implements placeholder image for chrome"] =
+`
+
+`;
+/* end snapshot image implements placeholder image for chrome */
+
+snapshots["image fallsback for ratios"] =
+`
+

+
+`;
+/* end snapshot image fallsback for ratios */
+
+snapshots["image fallback for ratio"] =
+`
+`;
+/* end snapshot image fallback for ratio */
+
diff --git a/packages/library/tests/components/image/image.test.js b/packages/library/tests/components/image/image.test.js
new file mode 100644
index 00000000..baaa4a68
--- /dev/null
+++ b/packages/library/tests/components/image/image.test.js
@@ -0,0 +1,279 @@
+/* eslint-disable no-undef */
+import { expect, fixture, html, defineCE, unsafeStatic, nextFrame } from '@open-wc/testing';
+import sinon from 'sinon';
+import { defaultChecks } from '../../helpers';
+import { Image } from '@muon/library/components/image';
+
+const tagName = defineCE(Image);
+const tag = unsafeStatic(tagName);
+
+const awaitLoading = () => {
+ return new Promise((resolve) => {
+ window.addEventListener('image-loaded', resolve);
+ });
+};
+
+describe('image', () => {
+
+ afterEach(() => {
+ sinon.restore();
+ });
+
+ it('implements standard self', async () => {
+ const el = await fixture(html`<${tag}>${tag}>`);
+
+ await defaultChecks(el);
+
+ const shadowRoot = el.shadowRoot;
+ const elementImage = shadowRoot.querySelector('.image');
+
+ expect(elementImage.innerHTML).to.equal('', 'has empty image div');
+ expect(el.src).to.equal(undefined, '`src` has no default property');
+ expect(el.type).to.equal('standard', '`type` property has default value `standard`');
+ expect(el._ratios).to.deep.equal(['1 / 1', '4 / 3', '16 / 9'], '`ratios` property has default token value');
+ expect(el.placeholder).to.equal('', '`placeholder` has default token value');
+ expect(el.ratio).to.equal('16 / 9', '`ratio` has default token value');
+ expect(el.loading).to.equal('lazy', '`loading` has default value');
+ expect(el.alt).to.equal('', 'alt has no value');
+
+ });
+
+ it('implements src image', async () => {
+ const el = await fixture(html`<${tag} src="https://via.placeholder.com/150">${tag}>`);
+
+ await awaitLoading(el);
+ await defaultChecks(el);
+
+ const shadowRoot = el.shadowRoot;
+ const elementImage = shadowRoot.querySelector('.image');
+ const img = elementImage.querySelector('img');
+
+ expect(elementImage).to.not.be.null; // eslint-disable-line no-unused-expressions
+ expect(el.type).to.equal('standard', '`type` property has default value `standard`');
+ expect(el.ratio).to.equal('16 / 9', '`ratio` has default token value');
+ expect(img.src).to.equal('https://via.placeholder.com/150', 'has `src` value from el');
+ expect(img.alt).to.equal('', 'alt has not value');
+ expect(Array.from(img.classList)).to.deep.equal(['blur-out', 'image-lazy']);
+ expect(elementImage.style.getPropertyValue('--image-ratio')).to.equal('16 / 9', 'ratio passed as custom css variable');
+ expect(getComputedStyle(elementImage).aspectRatio).to.equal('16 / 9', 'computed style value added for aspect-ratio');
+
+ });
+
+ it('implements ratio', async () => {
+ const el = await fixture(html`<${tag} src="https://via.placeholder.com/150" ratio="1 / 1">${tag}>`);
+
+ await awaitLoading(el);
+ await defaultChecks(el);
+
+ const shadowRoot = el.shadowRoot;
+ const elementImage = shadowRoot.querySelector('.image');
+ const img = elementImage.querySelector('img');
+
+ expect(elementImage).to.not.be.null; // eslint-disable-line no-unused-expressions
+ expect(el.type).to.equal('standard', '`type` property has default value `standard`');
+ expect(el.ratio).to.equal('1 / 1', '`ratio` has default token value');
+ expect(img.src).to.equal('https://via.placeholder.com/150', 'has `src` value from el');
+ expect(img.alt).to.equal('', 'alt has not value');
+ expect(elementImage.style.getPropertyValue('--image-ratio')).to.equal('1 / 1', 'ratio passed as custom css variable');
+ expect(getComputedStyle(elementImage).aspectRatio).to.equal('1 / 1', 'computed style value added for aspect-ratio');
+ expect(Array.from(img.classList)).to.deep.equal(['blur-out', 'image-lazy']);
+ });
+
+ it('implements background', async () => {
+ const el = await fixture(html`<${tag} src="https://via.placeholder.com/150" background>${tag}>`);
+
+ await awaitLoading(el);
+ await defaultChecks(el);
+
+ const shadowRoot = el.shadowRoot;
+ const elementImage = shadowRoot.querySelector('.image');
+ const img = elementImage.querySelector('img');
+ const holder = elementImage.querySelector('.image-holder');
+
+ expect(elementImage).to.not.be.null; // eslint-disable-line no-unused-expressions
+ expect(img).to.be.null; // eslint-disable-line no-unused-expressions
+ expect(el.type).to.equal('standard', '`type` property has default value `standard`');
+ expect(Array.from(holder.classList)).to.deep.equal(['image-holder', 'blur-out']);
+ expect(getComputedStyle(holder).backgroundImage).to.equal('url("https://via.placeholder.com/150")', 'computed style value added for background image');
+ expect(holder.style.getPropertyValue('--background-image')).to.equal('url("https://via.placeholder.com/150")', 'image passed as custom css variable');
+ });
+
+ it('implements placeholder image', async () => {
+ const el = await fixture(html`<${tag} src="https://via.placeholder.com/35000" placeholder="https://via.placeholder.com/15">${tag}>`);
+
+ await nextFrame();
+ await defaultChecks(el);
+
+ const shadowRoot = el.shadowRoot;
+ const elementImage = shadowRoot.querySelector('.image');
+ const img = elementImage.querySelector('img');
+
+ expect(elementImage).to.not.be.null; // eslint-disable-line no-unused-expressions
+ expect(el.type).to.equal('standard', '`type` property has default value `standard`');
+ expect(el.ratio).to.equal('16 / 9', '`ratio` has default token value');
+ expect(img.src).to.equal('https://via.placeholder.com/15', 'has `src` value from el');
+ expect(img.alt).to.equal('', 'alt has not value');
+ expect(Array.from(img.classList)).to.deep.equal(['image-lazy', 'blur']);
+ expect(elementImage.style.getPropertyValue('--image-ratio')).to.equal('16 / 9', 'ratio passed as custom css variable');
+ expect(getComputedStyle(elementImage).aspectRatio).to.equal('16 / 9', 'computed style value added for aspect-ratio');
+ });
+
+ it('implements placeholder image for background', async () => {
+ const el = await fixture(html`<${tag} src="https://via.placeholder.com/35000" placeholder="https://via.placeholder.com/15" background>${tag}>`);
+
+ await nextFrame();
+ await defaultChecks(el);
+
+ const shadowRoot = el.shadowRoot;
+ const elementImage = shadowRoot.querySelector('.image');
+ const holder = elementImage.querySelector('.image-holder');
+
+ expect(elementImage).to.not.be.null; // eslint-disable-line no-unused-expressions
+ expect(el.type).to.equal('standard', '`type` property has default value `standard`');
+ expect(getComputedStyle(holder).backgroundImage).to.equal('url("https://via.placeholder.com/15")', 'computed style value added for background image');
+ expect(holder.style.getPropertyValue('--background-image')).to.equal('url("https://via.placeholder.com/15")', 'image passed as custom css variable');
+ expect(el.ratio).to.equal('16 / 9', '`ratio` has default token value');
+ expect(elementImage.style.getPropertyValue('--image-ratio')).to.equal('16 / 9', 'ratio passed as custom css variable');
+ expect(getComputedStyle(elementImage).aspectRatio).to.equal('16 / 9', 'computed style value added for aspect-ratio');
+ });
+
+ it('implements alt', async () => {
+ const el = await fixture(html`<${tag} src="https://via.placeholder.com/150" alt="alternative text for the image">${tag}>`);
+
+ await awaitLoading(el);
+ await defaultChecks(el);
+
+ const shadowRoot = el.shadowRoot;
+ const elementImage = shadowRoot.querySelector('.image');
+ const img = elementImage.querySelector('img');
+
+ expect(elementImage).to.not.be.null; // eslint-disable-line no-unused-expressions
+ expect(el.type).to.equal('standard', '`type` property has default value `standard`');
+ expect(el.ratio).to.equal('16 / 9', '`ratio` has default token value');
+ expect(img.src).to.equal('https://via.placeholder.com/150', 'has `src` value from el');
+ expect(img.alt).to.equal('alternative text for the image', 'alt has a value');
+ expect(Array.from(img.classList)).to.deep.equal(['blur-out', 'image-lazy']);
+ expect(elementImage.style.getPropertyValue('--image-ratio')).to.equal('16 / 9', 'ratio passed as custom css variable');
+ expect(getComputedStyle(elementImage).aspectRatio).to.equal('16 / 9', 'computed style value added for aspect-ratio');
+ });
+
+ it('fallsback on ratio if not correct', async () => {
+ const el = await fixture(html`<${tag} src="https://via.placeholder.com/150" ratio="not-a-ratio">${tag}>`);
+
+ await awaitLoading(el);
+ await defaultChecks(el);
+
+ const shadowRoot = el.shadowRoot;
+ const elementImage = shadowRoot.querySelector('.image');
+ const img = elementImage.querySelector('img');
+
+ expect(elementImage).to.not.be.null; // eslint-disable-line no-unused-expressions
+ expect(el.type).to.equal('standard', '`type` property has default value `standard`');
+ expect(el.ratio).to.equal('16 / 9', '`ratio` has default token value');
+ expect(img.src).to.equal('https://via.placeholder.com/150', 'has `src` value from el');
+ expect(img.alt).to.equal('', 'alt has not value');
+ expect(elementImage.style.getPropertyValue('--image-ratio')).to.equal('16 / 9', 'ratio passed as custom css variable');
+ expect(getComputedStyle(elementImage).aspectRatio).to.equal('16 / 9', 'computed style value added for aspect-ratio');
+ expect(Array.from(img.classList)).to.deep.equal(['blur-out', 'image-lazy']);
+ });
+
+ it('image is eager', async () => {
+ const el = await fixture(html`<${tag} src="https://via.placeholder.com/150" loading="eager">${tag}>`);
+
+ await awaitLoading(el);
+ await defaultChecks(el);
+
+ const shadowRoot = el.shadowRoot;
+ const elementImage = shadowRoot.querySelector('.image');
+ const img = elementImage.querySelector('img');
+
+ expect(elementImage).to.not.be.null; // eslint-disable-line no-unused-expressions
+ expect(el.type).to.equal('standard', '`type` property has default value `standard`');
+ expect(el.ratio).to.equal('16 / 9', '`ratio` has default token value');
+ expect(img.src).to.equal('https://via.placeholder.com/150', 'has `src` value from el');
+ expect(img.alt).to.equal('', 'alt has not value');
+ expect(elementImage.style.getPropertyValue('--image-ratio')).to.equal('16 / 9', 'ratio passed as custom css variable');
+ expect(getComputedStyle(elementImage).aspectRatio).to.equal('16 / 9', 'computed style value added for aspect-ratio');
+ expect(Array.from(img.classList)).to.deep.equal(['blur-out', 'image-lazy']);
+ });
+
+ it('fallsback on image padding if aspect-ratio not available', async () => {
+ sinon.replace(
+ CSS,
+ 'supports',
+ sinon.fake.returns(false)
+ );
+
+ const el = await fixture(html`<${tag} src="https://via.placeholder.com/150">${tag}>`);
+
+ await awaitLoading(el);
+ await defaultChecks(el);
+
+ const shadowRoot = el.shadowRoot;
+ const elementImage = shadowRoot.querySelector('.image');
+ const img = elementImage.querySelector('img');
+
+ expect(elementImage).to.not.be.null; // eslint-disable-line no-unused-expressions
+ expect(el.type).to.equal('standard', '`type` property has default value `standard`');
+ expect(el.ratio).to.equal('16 / 9', '`ratio` has default token value');
+ expect(img.src).to.equal('https://via.placeholder.com/150', 'has `src` value from el');
+ expect(img.alt).to.equal('', 'alt has not value');
+ expect(elementImage.style.getPropertyValue('--image-padding')).to.equal('56.25%', 'ratio percentage passed as custom css variable');
+ // expect(getComputedStyle(elementImage).paddingTop).to.equal('56.25%', 'computed style value added padding top');
+ // @TODO: work out how to fake @supports in CSS
+ expect(Array.from(img.classList)).to.deep.equal(['blur-out', 'image-lazy']);
+ });
+
+ it('image fails to load', async () => {
+ const consoleError = sinon.stub(console, 'error');
+ const el = await fixture(html`<${tag} src="this-is-not-an-image">${tag}>`);
+
+ await nextFrame();
+ await defaultChecks(el);
+
+ const shadowRoot = el.shadowRoot;
+ const elementImage = shadowRoot.querySelector('.image');
+ const img = elementImage.querySelector('img');
+
+ expect(elementImage).to.not.be.null; // eslint-disable-line no-unused-expressions
+ expect(img).to.be.null; // eslint-disable-line no-unused-expressions
+ expect(el.type).to.equal('standard', '`type` property has default value `standard`');
+ expect(el.ratio).to.equal('16 / 9', '`ratio` has default token value');
+ expect(elementImage.style.getPropertyValue('--image-ratio')).to.equal('16 / 9', 'ratio passed as custom css variable');
+ expect(getComputedStyle(elementImage).aspectRatio).to.equal('16 / 9', 'computed style value added for aspect-ratio');
+ expect(consoleError.args[0]).to.deep.equal(['Image (this-is-not-an-image) failed to load']);
+ });
+
+ it('image fails to load for background', async () => {
+ const consoleError = sinon.stub(console, 'error');
+ const el = await fixture(html`<${tag} src="this-is-not-an-image">${tag}>`);
+
+ await nextFrame();
+ await defaultChecks(el);
+
+ expect(consoleError.args[0]).to.deep.equal(['Image (this-is-not-an-image) failed to load']);
+ });
+
+ it('implements placeholder image for chrome', async () => {
+ window.chrome = true;
+ const el = await fixture(html`<${tag} src="https://via.placeholder.com/35000" placeholder="https://via.placeholder.com/15">${tag}>`);
+
+ await nextFrame();
+ await defaultChecks(el);
+
+ const shadowRoot = el.shadowRoot;
+ const elementImage = shadowRoot.querySelector('.image');
+ const img = elementImage.querySelector('img');
+
+ expect(elementImage).to.not.be.null; // eslint-disable-line no-unused-expressions
+ expect(el.type).to.equal('standard', '`type` property has default value `standard`');
+ expect(el.ratio).to.equal('16 / 9', '`ratio` has default token value');
+ expect(img.src).to.equal('https://via.placeholder.com/15', 'has `src` value from el');
+ expect(img.alt).to.equal('', 'alt has not value');
+ expect(img.loading).to.equal('lazy', 'loading has a value');
+ expect(Array.from(img.classList)).to.deep.equal(['image-lazy', 'blur']);
+ expect(elementImage.style.getPropertyValue('--image-ratio')).to.equal('16 / 9', 'ratio passed as custom css variable');
+ expect(getComputedStyle(elementImage).aspectRatio).to.equal('16 / 9', 'computed style value added for aspect-ratio');
+ });
+
+});