Skip to content

Commit 198ce60

Browse files
committed
fix: improve accessibility and add tests for OtherVersions changes
- Add visible focus states to expand/collapse buttons - Add aria-hidden to decorative icons - Add aria-label to Other versions and major group expand buttons - Add i18n keys for expand/collapse other versions and major groups - Add tests for DateTime and ProvenanceBadge in OtherVersions section - Add accessibility tests for focus states and aria attributes
1 parent 9f255bd commit 198ce60

3 files changed

Lines changed: 208 additions & 6 deletions

File tree

app/components/PackageVersions.vue

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ function getTagVersions(tag: string): VersionDisplay[] {
306306
<button
307307
v-if="getTagVersions(row.tag).length > 1 || !hasLoadedAll"
308308
type="button"
309-
class="w-4 h-4 flex items-center justify-center text-fg-subtle hover:text-fg transition-colors"
309+
class="w-4 h-4 flex items-center justify-center text-fg-subtle hover:text-fg transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg-muted focus-visible:ring-offset-1 focus-visible:ring-offset-bg rounded-sm"
310310
:aria-expanded="expandedTags.has(row.tag)"
311311
:aria-label="
312312
expandedTags.has(row.tag)
@@ -318,13 +318,15 @@ function getTagVersions(tag: string): VersionDisplay[] {
318318
<span
319319
v-if="loadingTags.has(row.tag)"
320320
class="i-carbon-rotate-180 w-3 h-3 animate-spin"
321+
aria-hidden="true"
321322
/>
322323
<span
323324
v-else
324325
class="w-3 h-3 transition-transform duration-200"
325326
:class="
326327
expandedTags.has(row.tag) ? 'i-carbon-chevron-down' : 'i-carbon-chevron-right'
327328
"
329+
aria-hidden="true"
328330
/>
329331
</button>
330332
<span v-else class="w-4" />
@@ -441,18 +443,28 @@ function getTagVersions(tag: string): VersionDisplay[] {
441443
<div class="pt-1">
442444
<button
443445
type="button"
444-
class="flex items-center gap-2 text-left"
446+
class="flex items-center gap-2 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg-muted focus-visible:ring-offset-1 focus-visible:ring-offset-bg rounded-sm"
445447
:aria-expanded="otherVersionsExpanded"
448+
:aria-label="
449+
otherVersionsExpanded
450+
? $t('package.versions.collapse_other')
451+
: $t('package.versions.expand_other')
452+
"
446453
@click="expandOtherVersions"
447454
>
448455
<span
449456
class="w-4 h-4 flex items-center justify-center text-fg-subtle hover:text-fg transition-colors"
450457
>
451-
<span v-if="otherVersionsLoading" class="i-carbon-rotate-180 w-3 h-3 animate-spin" />
458+
<span
459+
v-if="otherVersionsLoading"
460+
class="i-carbon-rotate-180 w-3 h-3 animate-spin"
461+
aria-hidden="true"
462+
/>
452463
<span
453464
v-else
454465
class="w-3 h-3 transition-transform duration-200"
455466
:class="otherVersionsExpanded ? 'i-carbon-chevron-down' : 'i-carbon-chevron-right'"
467+
aria-hidden="true"
456468
/>
457469
</span>
458470
<span class="text-xs text-fg-muted py-1.5">
@@ -518,12 +530,12 @@ function getTagVersions(tag: string): VersionDisplay[] {
518530
<div class="flex items-center gap-2 min-w-0">
519531
<button
520532
type="button"
521-
class="w-4 h-4 flex items-center justify-center text-fg-subtle hover:text-fg transition-colors shrink-0"
533+
class="w-4 h-4 flex items-center justify-center text-fg-subtle hover:text-fg transition-colors shrink-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg-muted focus-visible:ring-offset-1 focus-visible:ring-offset-bg rounded-sm"
522534
:aria-expanded="expandedMajorGroups.has(group.major)"
523535
:aria-label="
524536
expandedMajorGroups.has(group.major)
525-
? `Collapse major ${group.major}`
526-
: `Expand major ${group.major}`
537+
? $t('package.versions.collapse_major', { major: group.major })
538+
: $t('package.versions.expand_major', { major: group.major })
527539
"
528540
@click="toggleMajorGroup(group.major)"
529541
>
@@ -534,6 +546,7 @@ function getTagVersions(tag: string): VersionDisplay[] {
534546
? 'i-carbon-chevron-down'
535547
: 'i-carbon-chevron-right'
536548
"
549+
aria-hidden="true"
537550
/>
538551
</button>
539552
<NuxtLink

i18n/locales/en.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,10 @@
116116
"title": "Versions",
117117
"collapse": "Collapse {tag}",
118118
"expand": "Expand {tag}",
119+
"collapse_other": "Collapse other versions",
120+
"expand_other": "Expand other versions",
121+
"collapse_major": "Collapse major {major}",
122+
"expand_major": "Expand major {major}",
119123
"other_versions": "Other versions",
120124
"more_tagged": "{count} more tagged",
121125
"all_covered": "All versions are covered by tags above",

test/nuxt/components/PackageVersions.spec.ts

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -624,6 +624,136 @@ describe('PackageVersions', () => {
624624
expect(text.includes('1.1.0') || component.findAll('button').length > 2).toBe(true)
625625
})
626626
})
627+
628+
it('shows DateTime for major group versions', async () => {
629+
mockFetchAllPackageVersions.mockResolvedValue([
630+
{ version: '2.0.0', time: '2024-01-15T12:00:00.000Z', hasProvenance: false },
631+
{ version: '1.1.0', time: '2024-01-10T12:00:00.000Z', hasProvenance: false },
632+
])
633+
634+
const component = await mountSuspended(PackageVersions, {
635+
props: {
636+
packageName: 'test-package',
637+
versions: {
638+
'2.0.0': createVersion('2.0.0'),
639+
},
640+
distTags: { latest: '2.0.0' },
641+
time: { '2.0.0': '2024-01-15T12:00:00.000Z' },
642+
},
643+
})
644+
645+
// Expand "Other versions"
646+
const otherVersionsButton = component
647+
.findAll('button')
648+
.find(btn => btn.text().includes('Other versions'))
649+
650+
await otherVersionsButton!.trigger('click')
651+
652+
await vi.waitFor(() => {
653+
// Should have DateTime components for both the main version and other versions
654+
const dateTimeComponents = component.findAllComponents({ name: 'DateTime' })
655+
expect(dateTimeComponents.length).toBeGreaterThan(1)
656+
})
657+
})
658+
659+
it('shows ProvenanceBadge for major group versions with provenance', async () => {
660+
mockFetchAllPackageVersions.mockResolvedValue([
661+
{ version: '2.0.0', time: '2024-01-15T12:00:00.000Z', hasProvenance: true },
662+
{ version: '1.1.0', time: '2024-01-10T12:00:00.000Z', hasProvenance: true },
663+
])
664+
665+
const component = await mountSuspended(PackageVersions, {
666+
props: {
667+
packageName: 'test-package',
668+
versions: {
669+
'2.0.0': createVersion('2.0.0', { hasProvenance: true }),
670+
},
671+
distTags: { latest: '2.0.0' },
672+
time: { '2.0.0': '2024-01-15T12:00:00.000Z' },
673+
},
674+
})
675+
676+
// Expand "Other versions"
677+
const otherVersionsButton = component
678+
.findAll('button')
679+
.find(btn => btn.text().includes('Other versions'))
680+
681+
await otherVersionsButton!.trigger('click')
682+
683+
await vi.waitFor(() => {
684+
// Should have ProvenanceBadge components for versions with provenance
685+
const provenanceBadges = component.findAllComponents({ name: 'ProvenanceBadge' })
686+
expect(provenanceBadges.length).toBeGreaterThan(1)
687+
})
688+
})
689+
690+
it('renders major group header as clickable link', async () => {
691+
mockFetchAllPackageVersions.mockResolvedValue([
692+
{ version: '2.0.0', time: '2024-01-15T12:00:00.000Z', hasProvenance: false },
693+
{ version: '1.1.0', time: '2024-01-10T12:00:00.000Z', hasProvenance: false },
694+
{ version: '1.0.0', time: '2024-01-05T12:00:00.000Z', hasProvenance: false },
695+
])
696+
697+
const component = await mountSuspended(PackageVersions, {
698+
props: {
699+
packageName: 'test-package',
700+
versions: {
701+
'2.0.0': createVersion('2.0.0'),
702+
},
703+
distTags: { latest: '2.0.0' },
704+
time: { '2.0.0': '2024-01-15T12:00:00.000Z' },
705+
},
706+
})
707+
708+
// Expand "Other versions"
709+
const otherVersionsButton = component
710+
.findAll('button')
711+
.find(btn => btn.text().includes('Other versions'))
712+
713+
await otherVersionsButton!.trigger('click')
714+
715+
await vi.waitFor(() => {
716+
// Find the major group header - should be a link (NuxtLink renders as <a>)
717+
const links = component.findAll('a')
718+
const majorGroupLink = links.find(l => l.text() === '1.1.0')
719+
expect(majorGroupLink?.exists()).toBe(true)
720+
})
721+
})
722+
723+
it('shows DateTime and ProvenanceBadge for single version in major group', async () => {
724+
mockFetchAllPackageVersions.mockResolvedValue([
725+
{ version: '2.0.0', time: '2024-01-15T12:00:00.000Z', hasProvenance: false },
726+
{ version: '1.0.0', time: '2024-01-05T12:00:00.000Z', hasProvenance: true },
727+
])
728+
729+
const component = await mountSuspended(PackageVersions, {
730+
props: {
731+
packageName: 'test-package',
732+
versions: {
733+
'2.0.0': createVersion('2.0.0'),
734+
},
735+
distTags: { latest: '2.0.0' },
736+
time: { '2.0.0': '2024-01-15T12:00:00.000Z' },
737+
},
738+
})
739+
740+
// Expand "Other versions"
741+
const otherVersionsButton = component
742+
.findAll('button')
743+
.find(btn => btn.text().includes('Other versions'))
744+
745+
await otherVersionsButton!.trigger('click')
746+
747+
await vi.waitFor(() => {
748+
// Single version group (1.0.0) should still have DateTime
749+
const dateTimeComponents = component.findAllComponents({ name: 'DateTime' })
750+
expect(dateTimeComponents.length).toBeGreaterThan(1)
751+
752+
// And ProvenanceBadge for the version with provenance
753+
const provenanceBadges = component.findAllComponents({ name: 'ProvenanceBadge' })
754+
expect(provenanceBadges.length).toBeGreaterThan(0)
755+
})
756+
})
627757
})
628758

629759
describe('loading states', () => {
@@ -746,6 +876,61 @@ describe('PackageVersions', () => {
746876
expect(expandButton.exists()).toBe(true)
747877
expect(expandButton.attributes('aria-label')).toMatch(/Expand|Collapse/)
748878
})
879+
880+
it('other versions button has aria-label', async () => {
881+
const component = await mountSuspended(PackageVersions, {
882+
props: {
883+
packageName: 'test-package',
884+
versions: {
885+
'1.0.0': createVersion('1.0.0'),
886+
},
887+
distTags: { latest: '1.0.0' },
888+
time: { '1.0.0': '2024-01-15T12:00:00.000Z' },
889+
},
890+
})
891+
892+
const otherVersionsButton = component
893+
.findAll('button')
894+
.find(btn => btn.text().includes('Other versions'))
895+
896+
expect(otherVersionsButton?.attributes('aria-label')).toMatch(/Expand other versions/)
897+
})
898+
899+
it('expand buttons have visible focus states', async () => {
900+
const component = await mountSuspended(PackageVersions, {
901+
props: {
902+
packageName: 'test-package',
903+
versions: {
904+
'1.0.0': createVersion('1.0.0'),
905+
},
906+
distTags: { latest: '1.0.0' },
907+
time: { '1.0.0': '2024-01-15T12:00:00.000Z' },
908+
},
909+
})
910+
911+
const expandButton = component.find('button[aria-expanded]')
912+
expect(expandButton.classes().some(c => c.includes('focus-visible'))).toBe(true)
913+
})
914+
915+
it('icons have aria-hidden attribute', async () => {
916+
const component = await mountSuspended(PackageVersions, {
917+
props: {
918+
packageName: 'test-package',
919+
versions: {
920+
'1.0.0': createVersion('1.0.0'),
921+
},
922+
distTags: { latest: '1.0.0' },
923+
time: { '1.0.0': '2024-01-15T12:00:00.000Z' },
924+
},
925+
})
926+
927+
// Find chevron icons inside buttons
928+
const chevronIcons = component.findAll('button span.i-carbon-chevron-right')
929+
expect(chevronIcons.length).toBeGreaterThan(0)
930+
for (const icon of chevronIcons) {
931+
expect(icon.attributes('aria-hidden')).toBe('true')
932+
}
933+
})
749934
})
750935

751936
describe('error handling', () => {

0 commit comments

Comments
 (0)