fix(dashboard): fix ContributorStack each-block key and add accessible avatar labels
- Replace (actor.name ?? actor.initials + i) with (actor.initials + '-' + actor.color)
to fix operator-precedence bug that made keys order-dependent when name is null
- Add role="img" + aria-label={actor.name ?? actor.initials} so screen readers
and touch users can access contributor names
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -20,11 +20,13 @@ const safeContributors = $derived(contributors ?? []);
|
|||||||
></span>
|
></span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="inline-flex items-center">
|
<span class="inline-flex items-center">
|
||||||
{#each safeContributors as actor, i (actor.name ?? actor.initials + i)}
|
{#each safeContributors as actor, i (actor.initials + '-' + actor.color)}
|
||||||
<span
|
<span
|
||||||
|
role="img"
|
||||||
|
aria-label={actor.name ?? actor.initials}
|
||||||
class="inline-flex h-[22px] w-[22px] flex-shrink-0 items-center justify-center rounded-full font-sans text-[9px] font-bold text-white ring-2 ring-white {i > 0 ? '-ml-1.5' : ''}"
|
class="inline-flex h-[22px] w-[22px] flex-shrink-0 items-center justify-center rounded-full font-sans text-[9px] font-bold text-white ring-2 ring-white {i > 0 ? '-ml-1.5' : ''}"
|
||||||
style="background-color: {actor.color || '#8c9aa3'};"
|
style="background-color: {actor.color || '#8c9aa3'};"
|
||||||
title={actor.name ?? ''}
|
title={actor.name ?? actor.initials}
|
||||||
>
|
>
|
||||||
{actor.initials}
|
{actor.initials}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
50
frontend/src/lib/components/ContributorStack.svelte.spec.ts
Normal file
50
frontend/src/lib/components/ContributorStack.svelte.spec.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { describe, it, expect, afterEach } from 'vitest';
|
||||||
|
import { cleanup, render } from 'vitest-browser-svelte';
|
||||||
|
import { page } from 'vitest/browser';
|
||||||
|
|
||||||
|
import ContributorStack from './ContributorStack.svelte';
|
||||||
|
import type { components } from '$lib/generated/api';
|
||||||
|
|
||||||
|
type ActivityActorDTO = components['schemas']['ActivityActorDTO'];
|
||||||
|
|
||||||
|
afterEach(() => cleanup());
|
||||||
|
|
||||||
|
const makeActor = (overrides: Partial<ActivityActorDTO> = {}): ActivityActorDTO => ({
|
||||||
|
initials: 'MR',
|
||||||
|
color: '#7a4f9a',
|
||||||
|
name: 'Max Raddatz',
|
||||||
|
...overrides
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ContributorStack', () => {
|
||||||
|
it('contributor avatar is announced by screen readers with actor name', async () => {
|
||||||
|
const actor = makeActor({ name: 'Anna Meier', initials: 'AM' });
|
||||||
|
render(ContributorStack, { contributors: [actor], hasMore: false });
|
||||||
|
await expect.element(page.getByRole('img', { name: 'Anna Meier' })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to initials as accessible name when actor name is null', async () => {
|
||||||
|
const actor = makeActor({ name: undefined, initials: 'AM' });
|
||||||
|
render(ContributorStack, { contributors: [actor], hasMore: false });
|
||||||
|
await expect.element(page.getByRole('img', { name: 'AM' })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders two avatars without crashing when actors have identical initials', async () => {
|
||||||
|
const actors = [
|
||||||
|
makeActor({ name: undefined, initials: 'AM', color: '#aa0000' }),
|
||||||
|
makeActor({ name: undefined, initials: 'AM', color: '#0000bb' })
|
||||||
|
];
|
||||||
|
render(ContributorStack, { contributors: actors, hasMore: false });
|
||||||
|
await expect.element(page.getByText('AM').first()).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders overflow indicator when hasMore is true', async () => {
|
||||||
|
render(ContributorStack, { contributors: [makeActor()], hasMore: true });
|
||||||
|
await expect.element(page.getByText('…')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders empty placeholder when no contributors', async () => {
|
||||||
|
render(ContributorStack, { contributors: [], hasMore: false });
|
||||||
|
await expect.element(page.getByTitle('Noch niemand angefangen')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user