Skip to content

Commit

Permalink
Avatar: support lazy loading of image (#3727)
Browse files Browse the repository at this point in the history
  • Loading branch information
jakobe authored Nov 29, 2024
1 parent a8a60e1 commit d77d5d5
Show file tree
Hide file tree
Showing 10 changed files with 141 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,9 @@ <h2>Avatar with badge</h2>

<h2>Avatar with image</h2>
<cookbook-avatar-example-image></cookbook-avatar-example-image>
<h3>Sizes</h3>
<cookbook-avatar-example-image-size></cookbook-avatar-example-image-size>
<h3>Lazy loaded image</h3>
<cookbook-avatar-example-image-loazy-loading></cookbook-avatar-example-image-loazy-loading>
<h3>Fallback image</h3>
<cookbook-avatar-example-image-error></cookbook-avatar-example-image-error>
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { AvatarExampleBadgeComponent } from './examples/badge';
import { AvatarExampleImageComponent } from './examples/image';
import { AvatarExampleImageSizeComponent } from './examples/image-sizes';
import { AvatarExampleImageErrorComponent } from './examples/image-error';
import { AvatarExampleImageLazyLoadingComponent } from './examples/image-lazy-loading';

const COMPONENT_DECLARATIONS = [
AvatarExampleComponent,
Expand All @@ -25,6 +26,7 @@ const COMPONENT_DECLARATIONS = [
AvatarExampleImageComponent,
AvatarExampleImageSizeComponent,
AvatarExampleImageErrorComponent,
AvatarExampleImageLazyLoadingComponent,
];

@NgModule({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Component } from '@angular/core';

const config = {
selector: 'cookbook-avatar-example-image-loazy-loading',
template: `<kirby-avatar imageSrc="/assets/images/woman.png" imageLoading="lazy" size="lg"></kirby-avatar>`,
};

@Component({
selector: config.selector,
template: config.template,
styleUrls: ['./avatar-examples.shared.scss'],
})
export class AvatarExampleImageLazyLoadingComponent {
template: string = config.template;
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,33 @@ <h2>Avatar with image</h2>
<cookbook-example-viewer [html]="imageExample.template">
<cookbook-avatar-example-image #imageExample></cookbook-avatar-example-image>
</cookbook-example-viewer>
<h3>Sizes</h3>
<cookbook-example-viewer [html]="imageSizeExample.template">
<cookbook-avatar-example-image-size #imageSizeExample></cookbook-avatar-example-image-size>
</cookbook-example-viewer>

<h2>Avatar with fallback image</h2>
<h3>Lazy loaded image</h3>
<p>
If you want to defer the loading of the avatar image set
<code>imageLoading="lazy"</code>
.
</p>
<p>
<a
class="kirby-external-icon"
target="_blank"
href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#loading"
>
Read more about lazy loading images on MDN
</a>
</p>
<cookbook-example-viewer [html]="imageLazyLoadingExample.template">
<cookbook-avatar-example-image-loazy-loading
#imageLazyLoadingExample
></cookbook-avatar-example-image-loazy-loading>
</cookbook-example-viewer>

<h3>Fallback image</h3>
<p>
The image avatar will emit an
<code>ErrorEvent</code>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,32 @@ import { ApiDescriptionProperty } from '~/app/shared/api-description/api-descrip
})
export class AvatarShowcaseComponent {
properties: ApiDescriptionProperty[] = [
{
name: 'imageSrc',
description: 'Points to the src of the image location',
defaultValue: 'null',
type: ['string'],
},
{
name: 'size',
description: 'Sets the size of the avatar.',
defaultValue: AvatarSize.SM,
type: Object.values(AvatarSize),
},
{
name: 'imageSrc',
description: 'The path to the image you want to embed in the avatar.',
defaultValue: 'undefined',
type: ['string'],
},
{
name: 'altText',
description:
'Must be filled out - its the alt text attribute that screenreaders use when "viewing" the image.',
defaultValue: 'null',
'The alt text attribute that screenreaders use when "viewing" the image. Mandatory when using the avatar with an image.',
defaultValue: 'undefined',
type: ['string'],
},
{
name: 'imageLoading',
description:
'Sets the loading attribute of the image.\n\nSee: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#loading',
defaultValue: 'undefined',
type: ['eager', 'lazy'],
},
{
name: 'overlay',
description:
Expand Down
8 changes: 7 additions & 1 deletion libs/designsystem/avatar/src/avatar.component.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
<div class="avatar" [ngClass]="{ overlay: overlay, stroke: stroke }">
<img *ngIf="imageSrc" [src]="imageSrc" [attr.alt]="altText" (error)="onImageError($event)" />
<img
*ngIf="imageSrc"
[src]="imageSrc"
[attr.alt]="altText"
[attr.loading]="imageLoading"
(error)="onImageError($event)"
/>
<ng-content *ngIf="!text" select="kirby-icon"></ng-content>
<span class="avatar-text" *ngIf="text">{{ text }}</span>
</div>
Expand Down
77 changes: 62 additions & 15 deletions libs/designsystem/avatar/src/avatar.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ describe('AvatarComponent', () => {
expect(spectator.component).toBeTruthy();
});

it('should render with correct default colors', async () => {
it('should render with correct default colors', () => {
spectator = createHost(`
<kirby-avatar>
<kirby-icon name="qr"></kirby-icon>
Expand All @@ -47,23 +47,70 @@ describe('AvatarComponent', () => {
});
});

it('should emit error when rendering avatar with invalid image source', async () => {
spectator = createHost(`
<kirby-avatar imageSrc="failingSrc.png"></kirby-avatar>
`);
spyOn(spectator.component.imageError, 'emit');

let errorEvent: ErrorEvent;
const img = spectator.query<HTMLImageElement>('img');
const waitForImageError = new Promise<HTMLImageElement>((resolve) => {
img.addEventListener('error', (ev) => {
errorEvent = ev;
resolve(img);
describe('when rendering Avatar with imageSrc set', () => {
describe('by default', () => {
beforeEach(() => {
spectator = createHost(`
<kirby-avatar imageSrc="/assets/images/woman.png"></kirby-avatar>`);
});

it('should render image', () => {
const img = spectator.query<HTMLImageElement>('img');
expect(img).toExist();
});

it('should render image with configured imageSrc', () => {
const img = spectator.query<HTMLImageElement>('img');
expect(img.src.endsWith('/assets/images/woman.png')).toBeTrue();
});

it('should not set alt attribute on img when altText is not set', () => {
const img = spectator.query<HTMLImageElement>('img');
expect(img).not.toHaveAttribute('alt');
});

it('should not set loading attribute on img when imageLoading is not set', () => {
const img = spectator.query<HTMLImageElement>('img');
expect(img).not.toHaveAttribute('loading');
});
});
await waitForImageError;

expect(spectator.component.imageError.emit).toHaveBeenCalledOnceWith(errorEvent);
it('should set alt attribute on image when altText is set', () => {
spectator = createHost(`
<kirby-avatar imageSrc="/assets/images/woman.png" altText="Test"></kirby-avatar>
`);

const img = spectator.query<HTMLImageElement>('img');
expect(img).toHaveAttribute('alt', 'Test');
});

it('should defer loading of image when imageLoading="lazy"', () => {
spectator = createHost(`
<kirby-avatar imageSrc="/assets/images/woman.png" imageLoading="lazy"></kirby-avatar>
`);

const img = spectator.query<HTMLImageElement>('img');
expect(img).toHaveAttribute('loading', 'lazy');
});

it('should emit error when rendering avatar with invalid image source', async () => {
spectator = createHost(`
<kirby-avatar imageSrc="failingSrc.png"></kirby-avatar>
`);
spyOn(spectator.component.imageError, 'emit');

let errorEvent: ErrorEvent;
const img = spectator.query<HTMLImageElement>('img');
const waitForImageError = new Promise<HTMLImageElement>((resolve) => {
img.addEventListener('error', (ev) => {
errorEvent = ev;
resolve(img);
});
});
await waitForImageError;

expect(spectator.component.imageError.emit).toHaveBeenCalledOnceWith(errorEvent);
});
});

describe('when rendering Avatar within Progress Circle', () => {
Expand Down
1 change: 1 addition & 0 deletions libs/designsystem/avatar/src/avatar.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export enum AvatarSize {
})
export class AvatarComponent {
@Input() imageSrc: string;
@Input() imageLoading: 'eager' | 'lazy' | undefined;
@Input() altText: string;
@Input() stroke: boolean;
@Input() text: string;
Expand Down
15 changes: 11 additions & 4 deletions libs/designsystem/karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,23 @@ module.exports = function (config) {
reporters: ['kjhtml', 'junit', 'spec'],
files: [
{
pattern: './src/lib/icons/svg/*.svg',
type: 'dom',
pattern: './icon/src/icons/svg/*.svg',
watched: false,
include: false,
included: false,
served: true,
nocache: false,
},
{
pattern: './testing/images/*.png',
watched: false,
included: false,
served: true,
nocache: false,
},
],
proxies: {
'/assets/kirby/icons/svg/': '/base/src/lib/icons/svg/',
'/assets/images/': '/base/testing/images/',
'/assets/kirby/icons/svg/': '/base/icon/src/icons/svg/',
'/svg/': '/base/src/lib/icons/svg/',
},
});
Expand Down
Binary file added libs/designsystem/testing/images/woman.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit d77d5d5

Please sign in to comment.