[READ-ONLY] a fast, modern browser for the npm registry
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: add links to major versions within 'other versions' dropdown (#170)

authored by

azaleta and committed by
GitHub
f63b7b7a 04f94fed

+305 -49
+116 -49
app/components/PackageVersions.vue
··· 306 306 <button 307 307 v-if="getTagVersions(row.tag).length > 1 || !hasLoadedAll" 308 308 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" 310 310 :aria-expanded="expandedTags.has(row.tag)" 311 311 :aria-label=" 312 312 expandedTags.has(row.tag) ··· 318 318 <span 319 319 v-if="loadingTags.has(row.tag)" 320 320 class="i-carbon-rotate-180 w-3 h-3 animate-spin" 321 + aria-hidden="true" 321 322 /> 322 323 <span 323 324 v-else ··· 325 326 :class=" 326 327 expandedTags.has(row.tag) ? 'i-carbon-chevron-down' : 'i-carbon-chevron-right' 327 328 " 329 + aria-hidden="true" 328 330 /> 329 331 </button> 330 332 <span v-else class="w-4" /> ··· 441 443 <div class="pt-1"> 442 444 <button 443 445 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" 445 447 :aria-expanded="otherVersionsExpanded" 448 + :aria-label=" 449 + otherVersionsExpanded 450 + ? $t('package.versions.collapse_other') 451 + : $t('package.versions.expand_other') 452 + " 446 453 @click="expandOtherVersions" 447 454 > 448 455 <span 449 456 class="w-4 h-4 flex items-center justify-center text-fg-subtle hover:text-fg transition-colors" 450 457 > 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 + /> 452 463 <span 453 464 v-else 454 465 class="w-3 h-3 transition-transform duration-200" 455 466 :class="otherVersionsExpanded ? 'i-carbon-chevron-down' : 'i-carbon-chevron-right'" 467 + aria-hidden="true" 456 468 /> 457 469 </span> 458 470 <span class="text-xs text-fg-muted py-1.5"> ··· 513 525 <template v-if="otherMajorGroups.length > 0"> 514 526 <div v-for="group in otherMajorGroups" :key="group.major"> 515 527 <!-- Major group header --> 516 - <button 517 - v-if="group.versions.length > 1" 518 - type="button" 519 - class="w-full text-left py-1" 520 - :aria-expanded="expandedMajorGroups.has(group.major)" 521 - :title="group.versions[0]?.version" 522 - @click="toggleMajorGroup(group.major)" 523 - > 524 - <div class="flex items-center gap-2"> 525 - <span 526 - class="w-3 h-3 transition-transform duration-200 text-fg-subtle" 527 - :class=" 528 - expandedMajorGroups.has(group.major) 529 - ? 'i-carbon-chevron-down' 530 - : 'i-carbon-chevron-right' 531 - " 532 - /> 533 - <span 534 - class="font-mono text-xs truncate" 535 - :class="group.versions[0]?.deprecated ? 'text-red-400' : 'text-fg-muted'" 536 - > 537 - {{ group.versions[0]?.version }} 538 - </span> 528 + <div v-if="group.versions.length > 1" class="py-1"> 529 + <div class="flex items-center justify-between gap-2"> 530 + <div class="flex items-center gap-2 min-w-0"> 531 + <button 532 + type="button" 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" 534 + :aria-expanded="expandedMajorGroups.has(group.major)" 535 + :aria-label=" 536 + expandedMajorGroups.has(group.major) 537 + ? $t('package.versions.collapse_major', { major: group.major }) 538 + : $t('package.versions.expand_major', { major: group.major }) 539 + " 540 + @click="toggleMajorGroup(group.major)" 541 + > 542 + <span 543 + class="w-3 h-3 transition-transform duration-200" 544 + :class=" 545 + expandedMajorGroups.has(group.major) 546 + ? 'i-carbon-chevron-down' 547 + : 'i-carbon-chevron-right' 548 + " 549 + aria-hidden="true" 550 + /> 551 + </button> 552 + <NuxtLink 553 + v-if="group.versions[0]?.version" 554 + :to="versionRoute(group.versions[0]?.version)" 555 + class="font-mono text-xs transition-colors duration-200 truncate" 556 + :class=" 557 + group.versions[0]?.deprecated 558 + ? 'text-red-400 hover:text-red-300' 559 + : 'text-fg-muted hover:text-fg' 560 + " 561 + :title=" 562 + group.versions[0]?.deprecated 563 + ? t('package.versions.deprecated_title', { 564 + version: group.versions[0]?.version, 565 + }) 566 + : group.versions[0]?.version 567 + " 568 + > 569 + {{ group.versions[0]?.version }} 570 + </NuxtLink> 571 + </div> 572 + <div class="flex items-center gap-2 shrink-0"> 573 + <DateTime 574 + v-if="group.versions[0]?.time" 575 + :datetime="group.versions[0]?.time" 576 + class="text-[10px] text-fg-subtle" 577 + year="numeric" 578 + month="short" 579 + day="numeric" 580 + /> 581 + <ProvenanceBadge 582 + v-if="group.versions[0]?.hasProvenance" 583 + :package-name="packageName" 584 + :version="group.versions[0]?.version" 585 + compact 586 + /> 587 + </div> 539 588 </div> 540 589 <div 541 590 v-if="group.versions[0]?.tags?.length" ··· 550 599 {{ tag }} 551 600 </span> 552 601 </div> 553 - </button> 602 + </div> 554 603 <!-- Single version (no expand needed) --> 555 604 <div v-else class="py-1"> 556 - <div class="flex items-center gap-2"> 557 - <span class="w-3" /> 558 - <NuxtLink 559 - v-if="group.versions[0]" 560 - :to="versionRoute(group.versions[0].version)" 561 - class="font-mono text-xs transition-colors duration-200 truncate" 562 - :class=" 563 - group.versions[0].deprecated 564 - ? 'text-red-400 hover:text-red-300' 565 - : 'text-fg-muted hover:text-fg' 566 - " 567 - :title=" 568 - group.versions[0].deprecated 569 - ? t('package.versions.deprecated_title', { 570 - version: group.versions[0].version, 571 - }) 572 - : group.versions[0].version 573 - " 574 - > 575 - {{ group.versions[0].version }} 576 - </NuxtLink> 605 + <div class="flex items-center justify-between gap-2"> 606 + <div class="flex items-center gap-2 min-w-0"> 607 + <span class="w-4 shrink-0" /> 608 + <NuxtLink 609 + v-if="group.versions[0]?.version" 610 + :to="versionRoute(group.versions[0]?.version)" 611 + class="font-mono text-xs transition-colors duration-200 truncate" 612 + :class=" 613 + group.versions[0]?.deprecated 614 + ? 'text-red-400 hover:text-red-300' 615 + : 'text-fg-muted hover:text-fg' 616 + " 617 + :title=" 618 + group.versions[0]?.deprecated 619 + ? t('package.versions.deprecated_title', { 620 + version: group.versions[0]?.version, 621 + }) 622 + : group.versions[0]?.version 623 + " 624 + > 625 + {{ group.versions[0]?.version }} 626 + </NuxtLink> 627 + </div> 628 + <div class="flex items-center gap-2 shrink-0"> 629 + <DateTime 630 + v-if="group.versions[0]?.time" 631 + :datetime="group.versions[0]?.time" 632 + class="text-[10px] text-fg-subtle" 633 + year="numeric" 634 + month="short" 635 + day="numeric" 636 + /> 637 + <ProvenanceBadge 638 + v-if="group.versions[0]?.hasProvenance" 639 + :package-name="packageName" 640 + :version="group.versions[0]?.version" 641 + compact 642 + /> 643 + </div> 577 644 </div> 578 645 <div v-if="group.versions[0]?.tags?.length" class="flex items-center gap-1 ml-5"> 579 646 <span ··· 589 656 <!-- Major group versions --> 590 657 <div 591 658 v-if="expandedMajorGroups.has(group.major) && group.versions.length > 1" 592 - class="ml-5 space-y-0.5" 659 + class="ml-6 space-y-0.5" 593 660 > 594 661 <div v-for="v in group.versions.slice(1)" :key="v.version" class="py-1"> 595 662 <div class="flex items-center justify-between gap-2">
+4
i18n/locales/en.json
··· 116 116 "title": "Versions", 117 117 "collapse": "Collapse {tag}", 118 118 "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}", 119 123 "other_versions": "Other versions", 120 124 "more_tagged": "{count} more tagged", 121 125 "all_covered": "All versions are covered by tags above",
+185
test/nuxt/components/PackageVersions.spec.ts
··· 624 624 expect(text.includes('1.1.0') || component.findAll('button').length > 2).toBe(true) 625 625 }) 626 626 }) 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 + }) 627 757 }) 628 758 629 759 describe('loading states', () => { ··· 745 875 const expandButton = component.find('button[aria-label]') 746 876 expect(expandButton.exists()).toBe(true) 747 877 expect(expandButton.attributes('aria-label')).toMatch(/Expand|Collapse/) 878 + }) 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 + } 748 933 }) 749 934 }) 750 935