Skip to content

Commit

Permalink
fix(NcAvatar): Improve initials generation to filter out special char…
Browse files Browse the repository at this point in the history
…acters

Co-authored-by: Grigorii K. Shartsev <me@shgk.me>
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
  • Loading branch information
susnux and ShGKme committed Nov 2, 2023
1 parent daaec86 commit ce3f168
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 21 deletions.
60 changes: 40 additions & 20 deletions src/components/NcAvatar/NcAvatar.vue
Expand Up @@ -173,10 +173,10 @@ export default {
v-bind="userStatusRole" />

<!-- Show the letter if no avatar nor icon class -->
<span v-if="userDoesNotExist && !(iconClass || $slots.icon)"
<span v-if="showInitials"
:style="initialsWrapperStyle"
class="avatardiv__initials-wrapper">
<span :style="initialsStyle" class="unknown">
<span :style="initialsStyle" class="avatardiv__initials">
{{ initials }}
</span>
</span>
Expand Down Expand Up @@ -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
}
Expand All @@ -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',
Expand All @@ -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})`,
}
Expand All @@ -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) => {
Expand Down Expand Up @@ -783,7 +803,7 @@ export default {
background-color: var(--color-main-background);
border-radius: 50%;
.unknown {
.avatardiv__initials {
position: absolute;
top: 0;
left: 0;
Expand Down
46 changes: 45 additions & 1 deletion tests/unit/components/NcAvatar/NcAvatar.spec.ts
Expand Up @@ -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'

Expand Down Expand Up @@ -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)
})
})
})
3 changes: 3 additions & 0 deletions tsconfig.json
Expand Up @@ -11,5 +11,8 @@
"strict": true,
"noImplicitAny": false,
"outDir": "./dist",
},
"vueCompilerOptions": {
"target": 2.7
}
}

0 comments on commit ce3f168

Please sign in to comment.