Skip to content

Commit 27f331a

Browse files
committed
fix(ui): make version selector toggle older groups for single tagged releases
1 parent c00e97c commit 27f331a

File tree

2 files changed

+120
-28
lines changed

2 files changed

+120
-28
lines changed

app/components/VersionSelector.vue

Lines changed: 78 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ const isLoadingAll = shallowRef(false)
7070
/** Cached full version list */
7171
const allVersionsCache = shallowRef<PackageVersionInfo[] | null>(null)
7272
73+
/** Whether non-tagged version groups are visible */
74+
const showAllGroups = shallowRef(false)
75+
7376
// ============================================================================
7477
// Computed
7578
// ============================================================================
@@ -78,6 +81,18 @@ const latestVersion = computed(() => props.distTags.latest)
7881
7982
const versionToTags = computed(() => buildVersionToTagsMap(props.distTags))
8083
84+
const visibleVersionGroups = computed(() => {
85+
if (!hasLoadedAll.value || showAllGroups.value) {
86+
return versionGroups.value
87+
}
88+
89+
return versionGroups.value.filter(group => group.primaryVersion.tags?.length)
90+
})
91+
92+
const hasAdditionalGroups = computed(() =>
93+
versionGroups.value.some(group => !group.primaryVersion.tags?.length),
94+
)
95+
8196
/** Get URL for a specific version */
8297
function getVersionUrl(version: string): string {
8398
return props.urlPattern.replace('{version}', version)
@@ -310,30 +325,37 @@ async function toggleGroup(groupId: string) {
310325
const group = versionGroups.value.find(g => g.id === groupId)
311326
if (!group) return
312327
313-
if (group.isExpanded) {
314-
group.isExpanded = false
328+
if (group.isLoading) return
329+
330+
if (hasLoadedAll.value) {
331+
if (hasNestedVersions(group)) {
332+
group.isExpanded = !group.isExpanded
333+
return
334+
}
335+
336+
if (controlsAdditionalGroups(group)) {
337+
showAllGroups.value = !showAllGroups.value
338+
}
339+
315340
return
316341
}
317342
318-
// Load all versions if not yet loaded
319-
if (!hasLoadedAll.value) {
320-
group.isLoading = true
321-
try {
322-
const allVersions = await loadAllVersions()
323-
processLoadedVersions(allVersions)
324-
// Find the group again after processing (it may have moved)
325-
const updatedGroup = versionGroups.value.find(g => g.id === groupId)
326-
if (updatedGroup) {
327-
updatedGroup.isExpanded = true
328-
}
329-
} catch (error) {
330-
// eslint-disable-next-line no-console
331-
console.error('Failed to load versions:', error)
332-
} finally {
333-
group.isLoading = false
343+
group.isLoading = true
344+
try {
345+
const allVersions = await loadAllVersions()
346+
processLoadedVersions(allVersions)
347+
showAllGroups.value = hasAdditionalGroups.value
348+
349+
// Find the group again after processing (it may have moved)
350+
const updatedGroup = versionGroups.value.find(g => g.id === groupId)
351+
if (updatedGroup && hasNestedVersions(updatedGroup)) {
352+
updatedGroup.isExpanded = true
334353
}
335-
} else {
336-
group.isExpanded = true
354+
} catch (error) {
355+
// eslint-disable-next-line no-console
356+
console.error('Failed to load versions:', error)
357+
} finally {
358+
group.isLoading = false
337359
}
338360
}
339361
@@ -345,7 +367,7 @@ async function toggleGroup(groupId: string) {
345367
const flatItems = computed(() => {
346368
const items: Array<{ type: 'group' | 'version'; groupId: string; version?: VersionDisplay }> = []
347369
348-
for (const group of versionGroups.value) {
370+
for (const group of visibleVersionGroups.value) {
349371
items.push({ type: 'group', groupId: group.id, version: group.primaryVersion })
350372
351373
if (group.isExpanded && group.versions.length > 1) {
@@ -401,7 +423,7 @@ function handleListboxKeydown(event: KeyboardEvent) {
401423
const item = items[focusedIndex.value]
402424
if (item?.type === 'group') {
403425
const group = versionGroups.value.find(g => g.id === item.groupId)
404-
if (group && !group.isExpanded && (group.versions.length > 1 || !hasLoadedAll.value)) {
426+
if (group && !isGroupOpen(group) && canToggleGroup(group)) {
405427
toggleGroup(item.groupId)
406428
}
407429
}
@@ -414,6 +436,8 @@ function handleListboxKeydown(event: KeyboardEvent) {
414436
const group = versionGroups.value.find(g => g.id === item.groupId)
415437
if (group?.isExpanded) {
416438
group.isExpanded = false
439+
} else if (group && controlsAdditionalGroups(group) && showAllGroups.value) {
440+
showAllGroups.value = false
417441
}
418442
} else if (item?.type === 'version') {
419443
// Jump to parent group
@@ -450,6 +474,31 @@ function navigateToVersion(version: string) {
450474
navigateTo(getVersionUrl(version))
451475
}
452476
477+
function hasNestedVersions(group: VersionGroup): boolean {
478+
return group.versions.length > 1
479+
}
480+
481+
function controlsAdditionalGroups(group: VersionGroup): boolean {
482+
return (
483+
Boolean(group.primaryVersion.tags?.length) &&
484+
!hasNestedVersions(group) &&
485+
hasAdditionalGroups.value
486+
)
487+
}
488+
489+
function isGroupOpen(group: VersionGroup): boolean {
490+
return group.isExpanded || (controlsAdditionalGroups(group) && showAllGroups.value)
491+
}
492+
493+
function canToggleGroup(group: VersionGroup): boolean {
494+
return (
495+
group.isLoading ||
496+
hasNestedVersions(group) ||
497+
!hasLoadedAll.value ||
498+
controlsAdditionalGroups(group)
499+
)
500+
}
501+
453502
// Reset focused index when dropdown opens
454503
watch(isOpen, open => {
455504
if (open) {
@@ -463,6 +512,7 @@ watch(isOpen, open => {
463512
watch(
464513
() => [props.distTags, props.versions, props.currentVersion],
465514
() => {
515+
showAllGroups.value = false
466516
if (hasLoadedAll.value && allVersionsCache.value) {
467517
processLoadedVersions(allVersionsCache.value)
468518
} else {
@@ -518,7 +568,7 @@ watch(
518568
@keydown="handleListboxKeydown"
519569
>
520570
<!-- Version groups -->
521-
<div v-for="group in versionGroups" :key="group.id">
571+
<div v-for="group in visibleVersionGroups" :key="group.id">
522572
<!-- Group header (primary version) -->
523573
<div
524574
:id="`version-${group.primaryVersion.version}`"
@@ -539,11 +589,11 @@ watch(
539589
>
540590
<!-- Expand button -->
541591
<button
542-
v-if="group.isExpanded || group.versions.length > 1 || !hasLoadedAll"
592+
v-if="canToggleGroup(group)"
543593
type="button"
544594
class="w-4 h-4 flex items-center justify-center text-fg-subtle hover:text-fg transition-colors shrink-0"
545-
:aria-expanded="group.isExpanded"
546-
:aria-label="group.isExpanded ? $t('common.collapse') : $t('common.expand')"
595+
:aria-expanded="isGroupOpen(group)"
596+
:aria-label="isGroupOpen(group) ? $t('common.collapse') : $t('common.expand')"
547597
@click.stop="toggleGroup(group.id)"
548598
>
549599
<span
@@ -554,11 +604,11 @@ watch(
554604
<span
555605
v-else
556606
class="w-3 h-3 transition-transform duration-200 rtl-flip"
557-
:class="group.isExpanded ? 'i-lucide:chevron-down' : 'i-lucide:chevron-right'"
607+
:class="isGroupOpen(group) ? 'i-lucide:chevron-down' : 'i-lucide:chevron-right'"
558608
aria-hidden="true"
559609
/>
560610
</button>
561-
<span v-else class="w-4" />
611+
<span v-else class="w-4 h-4 shrink-0" />
562612

563613
<!-- Version link -->
564614
<NuxtLink

test/nuxt/components/VersionSelector.spec.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,48 @@ describe('VersionSelector', () => {
424424
{ timeout: 2000 },
425425
)
426426
})
427+
428+
it('toggles older version groups for a single-version tagged release', async () => {
429+
mockFetchAllPackageVersions.mockResolvedValue([
430+
{ version: '1.0.0', time: '2024-01-15T12:00:00.000Z', hasProvenance: false },
431+
{ version: '0.9.0', time: '2024-01-10T12:00:00.000Z', hasProvenance: false },
432+
])
433+
434+
const component = await mountSuspended(VersionSelector, {
435+
props: {
436+
packageName: 'test-package',
437+
currentVersion: '1.0.0',
438+
versions: { '1.0.0': {}, '0.9.0': {} },
439+
distTags: { latest: '1.0.0' },
440+
urlPattern: '/package-docs/test-package/v/{version}',
441+
},
442+
})
443+
444+
const button = component.find('button[aria-haspopup="listbox"]')
445+
await button.trigger('click')
446+
447+
const expandButton = component.find('[role="listbox"] button[aria-expanded="false"]')
448+
await expandButton.trigger('click')
449+
450+
await vi.waitFor(() => {
451+
expect(mockFetchAllPackageVersions).toHaveBeenCalledWith('test-package')
452+
})
453+
454+
await vi.waitFor(() => {
455+
expect(component.find('[role="listbox"]').text()).toContain('0.9')
456+
const expandedButton = component.find('[role="listbox"] button[aria-expanded="true"]')
457+
expect(expandedButton.exists()).toBe(true)
458+
})
459+
460+
const expandedButton = component.find('[role="listbox"] button[aria-expanded="true"]')
461+
await expandedButton.trigger('click')
462+
463+
await vi.waitFor(() => {
464+
expect(component.find('[role="listbox"]').text()).not.toContain('0.9')
465+
const collapsedButton = component.find('[role="listbox"] button[aria-expanded="false"]')
466+
expect(collapsedButton.exists()).toBe(true)
467+
})
468+
})
427469
})
428470

429471
describe('0.x version grouping', () => {

0 commit comments

Comments
 (0)