Skip to content
Open
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
3 changes: 2 additions & 1 deletion app/components/Package/Maintainers.vue
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,8 @@ watch(
class="link-subtle text-sm shrink-0"
dir="ltr"
>
~{{ maintainer.name }}
<UserAvatar :username="maintainer.name" size="xs" aria-hidden="true" />
<span>{{ maintainer.name }}</span>
</LinkBase>
<span v-else class="font-mono text-sm text-fg-muted" dir="ltr">{{
maintainer.email
Expand Down
52 changes: 45 additions & 7 deletions app/components/User/Avatar.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,36 @@
<script setup lang="ts">
const props = defineProps<{
username: string
size: 'xs' | 'lg'
}>()

const sizePixels = computed(() => {
switch (props.size) {
case 'xs':
return 24
case 'lg':
return 64
}
})

const sizeClass = computed(() => {
switch (props.size) {
case 'xs':
return 'size-6'
case 'lg':
return 'size-16'
}
})

const textClass = computed(() => {
switch (props.size) {
case 'xs':
return 'text-xs'
case 'lg':
return 'text-2xl'
}
})

const { data: gravatarUrl } = useLazyFetch(() => `/api/gravatar/${props.username}`, {
transform: res => (res.hash ? `/_avatar/${res.hash}?s=128&d=404` : null),
getCachedData(key, nuxtApp) {
Expand All @@ -14,7 +42,8 @@ const { data: gravatarUrl } = useLazyFetch(() => `/api/gravatar/${props.username
<template>
<!-- Avatar -->
<div
class="size-16 shrink-0 rounded-full bg-bg-muted border border-border flex items-center justify-center overflow-hidden"
class="shrink-0 rounded-full bg-bg-muted border border-border flex items-center justify-center overflow-hidden"
:class="sizeClass"
role="img"
:aria-label="`Avatar for ${username}`"
>
Expand All @@ -23,13 +52,22 @@ const { data: gravatarUrl } = useLazyFetch(() => `/api/gravatar/${props.username
v-if="gravatarUrl"
:src="gravatarUrl"
alt=""
width="64"
height="64"
:width="sizePixels"
:height="sizePixels"
class="w-full h-full object-cover"
/>
<!-- Else fallback to initials -->
<span v-else class="text-2xl text-fg-subtle font-mono" aria-hidden="true">
{{ username.charAt(0).toUpperCase() }}
</span>
<!-- Else fallback to initials (use svg to avoid underline styling) -->
<svg
Comment on lines +59 to +60
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't figure out how to remove the underline style if wrapped in an anchor tag, so I used an SVG here. Seems like you need block or inline-block in order to override it, but the parent div is a flex which seems to make anything under not work.

v-else
xmlns="http://www.w3.org/2000/svg"
:width="sizePixels"
:height="sizePixels"
class="text-fg-subtle"
:class="textClass"
>
<text x="50%" y="50%" dominant-baseline="central" text-anchor="middle" fill="currentColor">
{{ username.charAt(0).toUpperCase() }}
</text>
</svg>
</div>
</template>
2 changes: 1 addition & 1 deletion app/pages/~[username]/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ defineOgImageComponent('Default', {
<!-- Header -->
<header class="mb-8 pb-8 border-b border-border">
<div class="flex flex-wrap items-center gap-4">
<UserAvatar :username="username" />
<UserAvatar :username="username" size="lg" />
<div>
<h1 class="font-mono text-2xl sm:text-3xl font-medium">~{{ username }}</h1>
<p v-if="results?.total" class="text-fg-muted text-sm mt-1">
Expand Down
2 changes: 1 addition & 1 deletion app/pages/~[username]/orgs.vue
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ defineOgImageComponent('Default', {
<!-- Header -->
<header class="mb-8 pb-8 border-b border-border">
<div class="flex flex-wrap items-center gap-4 mb-4">
<UserAvatar :username="username" />
<UserAvatar :username="username" size="lg" />
<div>
<h1 class="font-mono text-2xl sm:text-3xl font-medium">~{{ username }}</h1>
<p class="text-fg-muted text-sm mt-1">{{ $t('user.orgs_page.title') }}</p>
Expand Down
17 changes: 14 additions & 3 deletions test/nuxt/a11y.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2854,27 +2854,38 @@ describe('component accessibility audits', () => {
describe('UserAvatar', () => {
it('should have no accessibility violations', async () => {
const component = await mountSuspended(UserAvatar, {
props: { username: 'testuser' },
props: { username: 'testuser', size: 'lg' },
})
const results = await runAxe(component)
expect(results.violations).toEqual([])
})

it('should have no accessibility violations with short username', async () => {
const component = await mountSuspended(UserAvatar, {
props: { username: 'a' },
props: { username: 'a', size: 'lg' },
})
const results = await runAxe(component)
expect(results.violations).toEqual([])
})

it('should have no accessibility violations with long username', async () => {
const component = await mountSuspended(UserAvatar, {
props: { username: 'verylongusernameexample' },
props: { username: 'verylongusernameexample', size: 'lg' },
})
const results = await runAxe(component)
expect(results.violations).toEqual([])
})

it('should have no accessibility violations in all sizes', async () => {
const sizes = ['xs', 'lg'] as const
for (const size of sizes) {
const component = await mountSuspended(UserAvatar, {
props: { username: 'testuser', size },
})
const results = await runAxe(component)
expect(results.violations).toEqual([])
}
})
})

// Diff components
Expand Down
Loading