Skip to content

Commit

Permalink
Merge pull request #4912 from nextcloud-libraries/fix/a11y/avatar-sta…
Browse files Browse the repository at this point in the history
…tus-icons

fix(NcAvatar): Increase contrast of avatar status icon
  • Loading branch information
susnux committed Dec 6, 2023
2 parents 8008811 + b93aeaa commit 86bfe29
Show file tree
Hide file tree
Showing 11 changed files with 118 additions and 51 deletions.
2 changes: 2 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ module.exports = {
transform: {
'^.+\\.(j|t)s$': 'babel-jest',
'^.+\\.vue$': '@vue/vue2-jest',
'.+\\?raw$': 'jest-raw-loader',
'.+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub',
},
transformIgnorePatterns: [
Expand All @@ -71,6 +72,7 @@ module.exports = {

moduleNameMapper: {
'\\.(css|scss)$': 'jest-transform-stub',
'\\?raw$': 'jest-raw-loader',
},

snapshotSerializers: [
Expand Down
4 changes: 4 additions & 0 deletions l10n/messages.pot
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ msgstr ""
msgid "Avatar of {displayName}, {status}"
msgstr ""

#. TRANSLATORS: User status if the user is currently away from keyboard
msgid "away"
msgstr ""

Expand Down Expand Up @@ -128,6 +129,9 @@ msgstr ""
msgid "Hide password"
msgstr ""

msgid "invisble"
msgstr ""

msgid "Load more \"{options}\""
msgstr ""

Expand Down
7 changes: 7 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@
"glob": "^10.0.0",
"jest": "^29.0.1",
"jest-environment-jsdom": "29.7.0",
"jest-raw-loader": "^1.0.1",
"jest-serializer-vue": "^3.1.0",
"jest-transform-stub": "^2.0.0",
"resolve-url-loader": "^5.0.0",
Expand Down
2 changes: 1 addition & 1 deletion src/assets/status-icons/user-status-away.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/assets/status-icons/user-status-dnd.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/assets/status-icons/user-status-online.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
48 changes: 17 additions & 31 deletions src/components/NcAvatar/NcAvatar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -167,10 +167,10 @@ export default {
<span v-if="showUserStatusIconOnAvatar" class="avatardiv__user-status avatardiv__user-status--icon">
{{ userStatus.icon }}
</span>
<span v-else-if="canDisplayUserStatus"
<NcIconSvgWrapper v-else-if="canDisplayUserStatus"
class="avatardiv__user-status"
:class="'avatardiv__user-status--' + userStatus.status"
v-bind="userStatusRole" />
:svg="userStatusIcon"
:name="userStatusIconName" />

<!-- Show the letter if no avatar nor icon class -->
<span v-if="showInitials"
Expand All @@ -188,7 +188,9 @@ import NcActions from '../NcActions/index.js'
import NcActionLink from '../NcActionLink/index.js'
import NcButton from '../NcButton/index.js'
import NcLoadingIcon from '../NcLoadingIcon/index.js'
import NcIconSvgWrapper from '../NcIconSvgWrapper/index.js'
import usernameToColor from '../../functions/usernameToColor/index.js'
import { getUserStatusIcon, getUserStatusIconName, getUserStatusText } from '../../utils/UserStatus.ts'
import { userStatus } from '../../mixins/index.js'
import { t } from '../../l10n.js'
Expand Down Expand Up @@ -236,6 +238,7 @@ export default {
NcActionLink,
NcButton,
NcLoadingIcon,
NcIconSvgWrapper,
},
mixins: [userStatus],
props: {
Expand Down Expand Up @@ -378,35 +381,25 @@ export default {
return
}
if (this.canDisplayUserStatus || this.showUserStatusIconOnAvatar) {
return t('Avatar of {displayName}, {status}', { displayName: this.displayName ?? this.user, status: this.userStatusText })
return t('Avatar of {displayName}, {status}', { displayName: this.displayName ?? this.user, status: getUserStatusText(this.userStatus.status) })
}
return t('Avatar of {displayName}', { displayName: this.displayName ?? this.user })
},
/** Translated current user status */
userStatusText() {
switch (this.userStatus.status) {
// TRANSLATORS: User status if the user is currently away from keyboard
case 'away': return t('away')
case 'dnd': return t('do not disturb')
case 'online': return t('online')
case 'offline': return t('offline')
default: return this.userStatus.status
}
userStatusIcon() {
return getUserStatusIcon(this.userStatus.status)
},
/**
* If the avatar has no menu no aria-label is assigned, but for accessibility we still need the status to be accessible
* So this sets `role=img` on the status indicator (span with background) and the required `alt` and `aria-label` attributes.
* So this sets the required accessible label for the user status icon.
*/
userStatusRole() {
userStatusIconName() {
// only needed if non-interactive, otherwise the aria-label is set
if (this.hasMenu) {
return
}
const label = t('User status: {status}', { status: this.userStatusText })
return {
role: 'img',
'aria-label': label,
}
return getUserStatusIconName(this.userStatus.status)
},
canDisplayUserStatus() {
return this.showUserStatus
Expand Down Expand Up @@ -827,9 +820,12 @@ export default {
}
.avatardiv__user-status {
box-sizing: border-box;
position: absolute;
right: -4px;
bottom: -4px;
min-height: 18px;
min-width: 18px;
max-height: 18px;
max-width: 18px;
height: 40%;
Expand All @@ -852,16 +848,6 @@ export default {
background-color: var(--color-primary-element-light);
}
&--online{
background-image: url('../../assets/status-icons/user-status-online.svg');
}
&--dnd{
background-image: url('../../assets/status-icons/user-status-dnd.svg');
background-color: #ffffff;
}
&--away{
background-image: url('../../assets/status-icons/user-status-away.svg');
}
&--icon {
border: none;
background-color: transparent;
Expand Down
40 changes: 23 additions & 17 deletions src/components/NcRichContenteditable/NcAutoCompleteResult.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,14 @@
<div :class="[icon, `autocomplete-result__icon--${avatarUrl ? 'with-avatar' : ''}`]"
:style="avatarUrl ? { backgroundImage: `url(${avatarUrl})` } : null "
class="autocomplete-result__icon">
<div v-if="haveStatus"
:class="[`autocomplete-result__status--${status && status.icon ? 'icon' : status.status}`]"
class="autocomplete-result__status">
<span v-if="status.icon"
class="autocomplete-result__status autocomplete-result__status--icon">
{{ status && status.icon || '' }}
</div>
</span>
<NcIconSvgWrapper v-else-if="status.status && status.status !== 'offline'"
class="autocomplete-result__status"
:svg="userStatusIcon"
:name="userStatusIconName" />
</div>

<!-- Title and subline -->
Expand All @@ -47,9 +50,17 @@
<script>
import { generateUrl } from '@nextcloud/router'
import NcIconSvgWrapper from '../NcIconSvgWrapper/index.js'
import { getUserStatusIcon, getUserStatusIconName } from '../../utils/UserStatus.ts'
export default {
name: 'NcAutoCompleteResult',
components: {
NcIconSvgWrapper,
},
props: {
title: {
type: String,
Expand Down Expand Up @@ -90,8 +101,11 @@ export default {
? this.getAvatarUrl(this.id, 44)
: null
},
haveStatus() {
return this.status?.icon || (this.status?.status && this.status?.status !== 'offline')
userStatusIcon() {
return getUserStatusIcon(this.status.status)
},
userStatusIconName() {
return getUserStatusIconName(this.status.status)
},
},
Expand Down Expand Up @@ -140,10 +154,12 @@ $autocomplete-padding: 10px;
}
&__status {
box-sizing: border-box;
position: absolute;
right: -4px;
bottom: -4px;
box-sizing: border-box;
min-width: 18px;
min-height: 18px;
width: 18px;
height: 18px;
border: 2px solid var(--color-main-background);
Expand All @@ -155,16 +171,6 @@ $autocomplete-padding: 10px;
background-size: 16px;
background-position: center;
&--online{
background-image: url('../../assets/status-icons/user-status-online.svg');
}
&--dnd{
background-image: url('../../assets/status-icons/user-status-dnd.svg');
background-color: #ffffff;
}
&--away{
background-image: url('../../assets/status-icons/user-status-away.svg');
}
&--icon {
border: none;
background-color: transparent;
Expand Down
5 changes: 5 additions & 0 deletions src/globals.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,8 @@ declare const SCOPE_VERSION: string

// eslint-disable-next-line @typescript-eslint/no-explicit-any
declare const TRANSLATIONS: { locale: string, translations: any }[]

declare module '*?raw' {
const content: string
export default content
}
56 changes: 56 additions & 0 deletions src/utils/UserStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* @copyright 2023 Christopher Ng <chrng8@gmail.com>
*
* @author Christopher Ng <chrng8@gmail.com>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

import { t } from '../l10n.js'

import onlineSvg from '../assets/status-icons/user-status-online.svg?raw'
import awaySvg from '../assets/status-icons/user-status-away.svg?raw'
import dndSvg from '../assets/status-icons/user-status-dnd.svg?raw'
import invisibleSvg from '../assets/status-icons/user-status-invisible.svg?raw'

type Status = 'online' | 'away' | 'dnd' | 'invisible' | 'offline'

export const getUserStatusText = (status: Status): string => {
switch (status) {
case 'away': return t('away') // TRANSLATORS: User status if the user is currently away from keyboard
case 'dnd': return t('do not disturb')
case 'online': return t('online')
case 'invisible': return t('invisble')
case 'offline': return t('offline')
default: return status
}
}

export const getUserStatusIcon = (status: Status): null | string => {
const statusIconMap = {
online: onlineSvg,
away: awaySvg,
dnd: dndSvg,
invisible: invisibleSvg,
}

return statusIconMap[status] ?? null
}

export const getUserStatusIconName = (status: Status): string => {
return t('User status: {status}', { status: getUserStatusText(status) })
}

0 comments on commit 86bfe29

Please sign in to comment.