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

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
  • Loading branch information
susnux committed Nov 1, 2023
1 parent daaec86 commit 8392347
Show file tree
Hide file tree
Showing 3 changed files with 135 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
93 changes: 92 additions & 1 deletion tests/unit/components/NcAvatar/NcAvatar.spec.ts
Expand Up @@ -20,11 +20,102 @@
*
*/

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'

interface ITestCase {
name: string
expected: string
props: Record<string, unknown>
}

describe('NcAvatar.vue', () => {
describe('Initials', () => {
const testCases: ITestCase[] = [
{
name: 'empty user',
expected: '?',
props: {},
},
{
name: 'user property',
expected: 'JD',
props: {
user: 'Jane Doe',
isNoUser: true,
},
},
{
name: 'display name property',
expected: 'JD',
props: {
displayName: 'Jane Doe',
},
},
{
name: 'display name property over user property',
expected: 'NU',
props: {
displayName: 'No User',
user: 'I am a group',
isNoUser: true,
},
},
{
name: 'special characters in name',
expected: 'JD',
props: {
displayName: 'Jane (Doe)',
},
},
{
name: 'lower case name',
expected: 'JD',
props: {
displayName: 'jane doe',
},
},
{
name: 'middle names',
expected: 'JD',
props: {
displayName: 'Jane Some Name Doe',
},
},
{
name: 'non ascii characters',
expected: 'ÜÖ',
props: {
displayName: 'Ümit Öçal',
},
},
{
name: 'non latin characters',
expected: 'ジド',
props: {
displayName: 'ジェーン ドー',
},
},
]

for (const { name, props, expected } of testCases) {
it(`can handle ${name}`, async () => {
const wrapper = shallowMount(NcAvatar, {
propsData: {
...props,
},
})
expect(wrapper.exists()).toBe(true)

await nextTick()
const initials = wrapper.find('.avatardiv__initials')
expect(initials.exists()).toBe(true)
expect(initials.text()).toBe(expected)
})
}
})

it('aria label is set to include status if status is shown visually', async () => {
const status = {
icon: '',
Expand Down
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 8392347

Please sign in to comment.