Skip to content

fix: uui-avatar does not support non-latin characters #1067

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 20 additions & 19 deletions packages/uui-avatar/lib/uui-avatar.element.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { defineElement } from '@umbraco-ui/uui-base/lib/registration';
import { css, html, LitElement } from 'lit';
import { property, state } from 'lit/decorators.js';
import { property } from 'lit/decorators.js';

/**
* Avatar for displaying users
Expand Down Expand Up @@ -43,19 +43,20 @@ export class UUIAvatarElement extends LitElement {
* @default ''
*/
@property({ type: String, reflect: true })
get name() {
return this._name;
}
set name(newVal) {
const oldValue = this._name;
this._name = newVal;
this.initials = this.createInitials(this._name);
this.requestUpdate('title', oldValue);
}
private _name = '';
name = '';

@state()
private initials = '';
/**
* Use this to override the initials generated from the name.
* @type {string}
* @attr
* @default undefined
*/
@property({ type: String })
initials?: string;

private get _initials() {
return this.initials?.substring(0, 3) || this.createInitials(this.name);
}

connectedCallback() {
super.connectedCallback();
Expand All @@ -71,16 +72,16 @@ export class UUIAvatarElement extends LitElement {
return initials;
}

const words = name.match(/(\w+)/g);

const matches = [...name.matchAll(/(?:^|\s)(.)/g)];
const words = matches.map(m => m[1]).join('');
if (!words?.length) {
return initials;
}

initials = words[0].substring(0, 1);
initials = words[0].charAt(0);

if (words.length > 1) {
initials += words[words.length - 1].substring(0, 1);
initials += words[words.length - 1].charAt(0);
}

return initials.toUpperCase();
Expand All @@ -90,14 +91,14 @@ export class UUIAvatarElement extends LitElement {
return html` <img
src="${this.imgSrc}"
srcset="${this.imgSrcset}"
alt="${this.initials}"
alt="${this._initials}"
title="${this.name}" />`;
}

render() {
return html`
${this.imgSrc ? this.renderImage() : ''}
${!this.imgSrc ? this.initials : ''}
${!this.imgSrc ? this._initials : ''}
<slot></slot>
`;
}
Expand Down
7 changes: 7 additions & 0 deletions packages/uui-avatar/lib/uui-avatar.story.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@ export const Colors: Story = {
},
};

export const Initials: Story = {
args: {
name: 'Umbraco HQ',
initials: 'AB',
},
};

/**
* Slotted content might overflow, use the `overflow` attribute to hide overflow.
*/
Expand Down
93 changes: 68 additions & 25 deletions packages/uui-avatar/lib/uui-avatar.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,34 +41,77 @@ describe('UuiAvatar', () => {
});
});

it('renders an image when imgSrc is set', async () => {
const avatar = await fixture(
html`<uui-avatar img-src="${avatarSrc}" name="My Avatar"></uui-avatar>`,
);
expect(avatar).shadowDom.to.equal(
`<img alt="MA" src="${avatarSrc}" srcset="" title="My Avatar" /><slot></<slot>`,
);
});
describe('initials', () => {
it('renders an image when imgSrc is set', async () => {
const avatar = await fixture(
html`<uui-avatar img-src="${avatarSrc}" name="My Avatar"></uui-avatar>`,
);
expect(avatar).shadowDom.to.equal(
`<img alt="MA" src="${avatarSrc}" srcset="" title="My Avatar" /><slot></<slot>`,
);
});

it('renders an image with alt text when imgSrc and text is set', async () => {
const avatar = await fixture(
html`<uui-avatar img-src="${avatarSrc}" name="alt text"></uui-avatar>`,
);
expect(avatar).shadowDom.to.equal(
`<img alt="AT" src="${avatarSrc}" srcset="" title="alt text" /><slot></<slot>`,
);
});
it('renders an image with alt text when imgSrc and text is set', async () => {
const avatar = await fixture(
html`<uui-avatar img-src="${avatarSrc}" name="alt text"></uui-avatar>`,
);
expect(avatar).shadowDom.to.equal(
`<img alt="AT" src="${avatarSrc}" srcset="" title="alt text" /><slot></<slot>`,
);
});

it('shows the first initial when text is used and there is no image', async () => {
const avatar = await fixture(html`<uui-avatar name="First"></uui-avatar>`);
expect(avatar).shadowDom.to.equal('F<slot></<slot>');
});
it('shows the first initial when text is used and there is no image', async () => {
const avatar = await fixture(
html`<uui-avatar name="First"></uui-avatar>`,
);
expect(avatar).shadowDom.to.equal('F<slot></<slot>');
});

it('shows the first and last initial when text is used and there is no image', async () => {
element.name = 'First Second Last';
await element.updateComplete;
expect(element).shadowDom.to.equal('FL<slot></<slot>');
});

it('shows the first and last initial when text is used and there is no image', async () => {
const avatar = await fixture(
html`<uui-avatar name="First Second Last"></uui-avatar>`,
);
expect(avatar).shadowDom.to.equal('FL<slot></<slot>');
it('supports unicode characters', async () => {
element.name = '👩‍💻';
await element.updateComplete;
expect(element).shadowDom.to.equal('\ud83d<slot></<slot>');

element.name = '👩‍💻 👨‍💻';
await element.updateComplete;
expect(element).shadowDom.to.equal('\ud83d\ud83d<slot></<slot>');
});

it('supports non-latin characters', async () => {
element.name = 'Привет Ša';
await element.updateComplete;
expect(element).shadowDom.to.equal('ПŠ<slot></<slot>');

element.name = 'Привет';
await element.updateComplete;
expect(element).shadowDom.to.equal('П<slot></<slot>');

element.name = 'UlŠa Mya';
await element.updateComplete;
expect(element).shadowDom.to.equal('UM<slot></<slot>');

element.name = 'åse hylle';
await element.updateComplete;
expect(element).shadowDom.to.equal('ÅH<slot></<slot>');
});

it('supports overriding initials', async () => {
element.initials = 'AB';
await element.updateComplete;
expect(element).shadowDom.to.equal('AB<slot></<slot>');
});

it('shows a maximum of 3 characters', async () => {
element.initials = '1234';
await element.updateComplete;
expect(element).shadowDom.to.equal('123<slot></<slot>');
});
});

it('passes the a11y audit', async () => {
Expand Down
Loading