diff --git a/src/components/NcAvatar/NcAvatar.vue b/src/components/NcAvatar/NcAvatar.vue index b382953731..205bff5170 100644 --- a/src/components/NcAvatar/NcAvatar.vue +++ b/src/components/NcAvatar/NcAvatar.vue @@ -173,10 +173,10 @@ export default { v-bind="userStatusRole" /> - - + {{ initials }} @@ -421,7 +421,11 @@ export default { && this.userStatus.status !== 'dnd' && this.userStatus.icon }, - getUserIdentifier() { + /** + * The user identifier, either the display name if set or the user property + * If both properties are not set an empty string is returned + */ + userIdentifier() { if (this.isDisplayNameDefined) { return this.displayName } @@ -448,10 +452,14 @@ export default { } return !(this.user === getCurrentUser()?.uid || this.userDoesNotExist || this.url) }, - shouldShowPlaceholder() { - return this.allowPlaceholder && ( - this.userDoesNotExist) + + /** + * True if initials should be shown as the user icon fallback + */ + showInitials() { + return this.allowPlaceholder && this.userDoesNotExist && !(this.iconClass || this.$slots.icon) }, + avatarStyle() { const style = { '--size': this.size + 'px', @@ -461,13 +469,13 @@ export default { return style }, initialsWrapperStyle() { - const { r, g, b } = usernameToColor(this.getUserIdentifier) + const { r, g, b } = usernameToColor(this.userIdentifier) return { backgroundColor: `rgba(${r}, ${g}, ${b}, 0.1)`, } }, initialsStyle() { - const { r, g, b } = usernameToColor(this.getUserIdentifier) + const { r, g, b } = usernameToColor(this.userIdentifier) return { color: `rgb(${r}, ${g}, ${b})`, } @@ -482,21 +490,33 @@ export default { return this.displayName }, + + /** + * Get the (max. two) initials of the user as uppcase string + */ initials() { - let initials - if (this.shouldShowPlaceholder) { - const user = this.getUserIdentifier - const idx = user.indexOf(' ') + let initials = '?' + if (this.showInitials) { + const user = this.userIdentifier.trim() if (user === '') { - initials = '?' - } else { - initials = String.fromCodePoint(user.codePointAt(0)) - if (idx !== -1) { - initials = initials.concat(String.fromCodePoint(user.codePointAt(idx + 1))) - } + return '?' + } + + /** + * Filtered user name, without special characters so only letters and numbers are allowed (prevent e.g. '(' as an initial) + * \p{L}: Letters of all languages + * \p{N}: Numbers of all languages + * \s: White space for breaking the string + * @type {string} + */ + const filtered = user.match(/[\p{L}\p{N}\s]/gu).join('') + const idx = filtered.lastIndexOf(' ') + initials = String.fromCodePoint(filtered.codePointAt(0)) + if (idx !== -1) { + initials = initials.concat(String.fromCodePoint(filtered.codePointAt(idx + 1))) } } - return initials.toUpperCase() + return initials.toLocaleUpperCase() }, menu() { const actions = this.contactsMenuActions.map((item) => { @@ -783,7 +803,7 @@ export default { background-color: var(--color-main-background); border-radius: 50%; - .unknown { + .avatardiv__initials { position: absolute; top: 0; left: 0; diff --git a/tests/unit/components/NcAvatar/NcAvatar.spec.ts b/tests/unit/components/NcAvatar/NcAvatar.spec.ts index 669d7a1b32..08e0767696 100644 --- a/tests/unit/components/NcAvatar/NcAvatar.spec.ts +++ b/tests/unit/components/NcAvatar/NcAvatar.spec.ts @@ -20,7 +20,7 @@ * */ -import { mount } from '@vue/test-utils' +import { mount, shallowMount } from '@vue/test-utils' import { nextTick } from 'vue' import NcAvatar from '../../../../src/components/NcAvatar/NcAvatar.vue' @@ -87,4 +87,48 @@ describe('NcAvatar.vue', () => { expect(wrapper.find('.avatardiv__user-status').exists()).toBe(false) expect(wrapper.attributes('aria-label')).toBe('Avatar of J. Doe') }) + + it('should display initials for user id', async () => { + const wrapper = shallowMount(NcAvatar, { + propsData: { + user: 'Jane Doe', + isNoUser: true, + }, + }) + await nextTick() + expect(wrapper.text()).toBe('JD') + }) + + it('should display initials for display name property over user id', async () => { + const wrapper = shallowMount(NcAvatar, { + propsData: { + displayName: 'No User', + user: 'I am a group', + isNoUser: true, + }, + }) + await nextTick() + expect(wrapper.text()).toBe('NU') + }) + + describe('Fallback initials', () => { + it.each` + displayName | initials | case + ${''} | ${'?'} | ${'empty user'} + ${'Jane Doe'} | ${'JD'} | ${'display name property'} + ${'Jane (Doe)'} | ${'JD'} | ${'special characters in name'} + ${'jane doe'} | ${'JD'} | ${'lower case name'} + ${'Jane Some Name Doe'} | ${'JD'} | ${'middle names'} + ${'Ümit Öçal'} | ${'ÜÖ'} | ${'non ascii characters'} + ${'ジェーン ドー'} | ${'ジド'} | ${'non latin characters'} + `('should display initials for $case ("$displayName" -> "$initials")', async ({ displayName, initials }) => { + const wrapper = shallowMount(NcAvatar, { + propsData: { + displayName, + }, + }) + await nextTick() + expect(wrapper.text()).toBe(initials) + }) + }) }) diff --git a/tsconfig.json b/tsconfig.json index fa4afcb17e..523e9cf840 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,5 +11,8 @@ "strict": true, "noImplicitAny": false, "outDir": "./dist", + }, + "vueCompilerOptions": { + "target": 2.7 } }