Skip to content

Commit 61c5b85

Browse files
garthdwdanielroe
andauthored
feat: update all sidebar sections to be collapsible sections (#639)
Co-authored-by: Daniel Roe <daniel@roe.dev>
1 parent 792223c commit 61c5b85

File tree

8 files changed

+121
-103
lines changed

8 files changed

+121
-103
lines changed

app/components/CollapsibleSection.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ interface Props {
66
isLoading?: boolean
77
headingLevel?: `h${number}`
88
id: string
9+
icon?: string
910
}
1011
1112
const props = withDefaults(defineProps<Props>(), {
@@ -107,6 +108,7 @@ useHead({
107108
:href="`#${id}`"
108109
class="inline-flex items-center gap-1.5 text-fg-subtle hover:text-fg-muted transition-colors duration-200 no-underline"
109110
>
111+
<span v-if="icon" :class="icon" aria-hidden="true" />
110112
{{ title }}
111113
<span
112114
class="i-carbon:link w-3 h-3 opacity-0 group-hover:opacity-100 transition-opacity duration-200"
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<script setup lang="ts">
2+
const props = defineProps<{
3+
engines?: {
4+
node?: string
5+
npm?: string
6+
}
7+
}>()
8+
</script>
9+
<template>
10+
<CollapsibleSection
11+
v-if="engines && (engines.node || engines.npm)"
12+
:title="$t('package.compatibility')"
13+
id="compatibility"
14+
>
15+
<dl class="space-y-2">
16+
<div v-if="engines.node" class="flex justify-between gap-4 py-1">
17+
<dt class="text-fg-muted text-sm shrink-0">node</dt>
18+
<dd class="font-mono text-sm text-fg text-end" :title="engines.node">
19+
{{ engines.node }}
20+
</dd>
21+
</div>
22+
<div v-if="engines.npm" class="flex justify-between gap-4 py-1">
23+
<dt class="text-fg-muted text-sm shrink-0">npm</dt>
24+
<dd class="font-mono text-sm text-fg text-end" :title="engines.npm">
25+
{{ engines.npm }}
26+
</dd>
27+
</div>
28+
</dl>
29+
</CollapsibleSection>
30+
</template>

app/components/Package/InstallScripts.vue

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,11 @@ const isExpanded = shallowRef(false)
1818
</script>
1919

2020
<template>
21-
<section>
22-
<h2
23-
id="install-scripts-heading"
24-
class="text-xs text-fg-subtle uppercase tracking-wider mb-3 flex items-center gap-2"
25-
>
26-
<span class="i-carbon:warning-alt w-3 h-3 text-yellow-500" aria-hidden="true" />
27-
{{ $t('package.install_scripts.title') }}
28-
</h2>
29-
21+
<CollapsibleSection
22+
:title="$t('package.install_scripts.title')"
23+
id="installScripts"
24+
icon="i-carbon:warning-alt w-3 h-3 text-yellow-500"
25+
>
3026
<!-- Script list: name as label, content below -->
3127
<dl class="space-y-2 m-0">
3228
<div v-for="scriptName in installScripts.scripts" :key="scriptName">
@@ -112,5 +108,5 @@ const isExpanded = shallowRef(false)
112108
</li>
113109
</ul>
114110
</div>
115-
</section>
111+
</CollapsibleSection>
116112
</template>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<script setup lang="ts">
2+
import { NuxtLink } from '#components'
3+
4+
defineProps<{
5+
keywords?: string[]
6+
}>()
7+
</script>
8+
<template>
9+
<CollapsibleSection v-if="keywords?.length" :title="$t('package.keywords_title')" id="keywords">
10+
<ul class="flex flex-wrap gap-1.5 list-none m-0 p-0">
11+
<li v-for="keyword in keywords.slice(0, 15)" :key="keyword">
12+
<TagClickable :as="NuxtLink" :to="{ name: 'search', query: { q: `keywords:${keyword}` } }">
13+
{{ keyword }}
14+
</TagClickable>
15+
</li>
16+
</ul>
17+
</CollapsibleSection>
18+
</template>

app/components/Package/Maintainers.vue

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -168,19 +168,11 @@ watch(
168168
</script>
169169

170170
<template>
171-
<section id="maintainers" v-if="maintainers?.length" class="scroll-mt-20">
172-
<h2 id="maintainers-heading" class="group text-xs text-fg-subtle uppercase tracking-wider mb-3">
173-
<a
174-
href="#maintainers"
175-
class="inline-flex items-center gap-1.5 text-fg-subtle hover:text-fg-muted transition-colors duration-200 no-underline"
176-
>
177-
{{ $t('package.maintainers.title') }}
178-
<span
179-
class="i-carbon-link w-3 h-3 opacity-0 group-hover:opacity-100 transition-opacity duration-200"
180-
aria-hidden="true"
181-
/>
182-
</a>
183-
</h2>
171+
<CollapsibleSection
172+
v-if="maintainers?.length"
173+
id="maintainers"
174+
:title="$t('package.maintainers.title')"
175+
>
184176
<ul class="space-y-2 list-none m-0 p-0" :aria-label="$t('package.maintainers.list_label')">
185177
<li
186178
v-for="maintainer in visibleMaintainers"
@@ -293,5 +285,5 @@ watch(
293285
{{ $t('package.maintainers.add_owner') }}
294286
</button>
295287
</div>
296-
</section>
288+
</CollapsibleSection>
297289
</template>

app/components/Package/SkillsCard.vue

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,7 @@ const skillsModal = useModal('skills-modal')
1111
</script>
1212

1313
<template>
14-
<section v-if="skills.length" id="skills" class="scroll-mt-20">
15-
<h2 class="text-xs text-fg-subtle uppercase tracking-wider mb-3">
16-
{{ $t('package.skills.title') }}
17-
</h2>
14+
<CollapsibleSection v-if="skills.length" :title="$t('package.skills.title')" id="skills">
1815
<button
1916
type="button"
2017
class="w-full flex items-center gap-2 px-3 py-2 text-sm font-mono bg-bg-muted border border-border rounded-md hover:border-border-hover hover:bg-bg-elevated focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-hover transition-colors duration-200"
@@ -25,5 +22,5 @@ const skillsModal = useModal('skills-modal')
2522
$t('package.skills.skills_available', { count: skills.length }, skills.length)
2623
}}</span>
2724
</button>
28-
</section>
25+
</CollapsibleSection>
2926
</template>

app/pages/package/[...package].vue

Lines changed: 2 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1005,33 +1005,7 @@ defineOgImageComponent('Package', {
10051005
</ClientOnly>
10061006

10071007
<!-- Keywords -->
1008-
<section id="keywords" v-if="displayVersion?.keywords?.length" class="scroll-mt-20">
1009-
<h2
1010-
id="keywords-heading"
1011-
class="group text-xs text-fg-subtle uppercase tracking-wider mb-3"
1012-
>
1013-
<a
1014-
href="#keywords"
1015-
class="inline-flex items-center gap-1.5 text-fg-subtle hover:text-fg-muted transition-colors duration-200 no-underline"
1016-
>
1017-
{{ $t('package.keywords_title') }}
1018-
<span
1019-
class="i-carbon:link w-3 h-3 opacity-0 group-hover:opacity-100 transition-opacity duration-200"
1020-
aria-hidden="true"
1021-
/>
1022-
</a>
1023-
</h2>
1024-
<ul class="flex flex-wrap gap-1.5 list-none m-0 p-0">
1025-
<li v-for="keyword in displayVersion.keywords.slice(0, 15)" :key="keyword">
1026-
<TagClickable
1027-
:as="NuxtLink"
1028-
:to="{ name: 'search', query: { q: `keywords:${keyword}` } }"
1029-
>
1030-
{{ keyword }}
1031-
</TagClickable>
1032-
</li>
1033-
</ul>
1034-
</section>
1008+
<PackageKeywords :keywords="displayVersion?.keywords" />
10351009

10361010
<!-- Agent Skills -->
10371011
<ClientOnly>
@@ -1052,43 +1026,7 @@ defineOgImageComponent('Package', {
10521026
:links="readmeData.playgroundLinks"
10531027
/>
10541028

1055-
<section
1056-
id="compatibility"
1057-
v-if="
1058-
displayVersion?.engines && (displayVersion.engines.node || displayVersion.engines.npm)
1059-
"
1060-
class="scroll-mt-20"
1061-
>
1062-
<h2
1063-
id="compatibility-heading"
1064-
class="group text-xs text-fg-subtle uppercase tracking-wider mb-3"
1065-
>
1066-
<a
1067-
href="#compatibility"
1068-
class="inline-flex items-center gap-1.5 text-fg-subtle hover:text-fg-muted transition-colors duration-200 no-underline"
1069-
>
1070-
{{ $t('package.compatibility') }}
1071-
<span
1072-
class="i-carbon:link w-3 h-3 opacity-0 group-hover:opacity-100 transition-opacity duration-200"
1073-
aria-hidden="true"
1074-
/>
1075-
</a>
1076-
</h2>
1077-
<dl class="space-y-2">
1078-
<div v-if="displayVersion.engines.node" class="flex justify-between gap-4 py-1">
1079-
<dt class="text-fg-muted text-sm shrink-0">node</dt>
1080-
<dd class="font-mono text-sm text-fg text-end" :title="displayVersion.engines.node">
1081-
{{ displayVersion.engines.node }}
1082-
</dd>
1083-
</div>
1084-
<div v-if="displayVersion.engines.npm" class="flex justify-between gap-4 py-1">
1085-
<dt class="text-fg-muted text-sm shrink-0">npm</dt>
1086-
<dd class="font-mono text-sm text-fg text-end" :title="displayVersion.engines.npm">
1087-
{{ displayVersion.engines.npm }}
1088-
</dd>
1089-
</div>
1090-
</dl>
1091-
</section>
1029+
<PackageCompatibility :engines="displayVersion?.engines" />
10921030

10931031
<!-- Versions (grouped by release channel) -->
10941032
<PackageVersions

test/nuxt/a11y.spec.ts

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import type { AxeResults, RunOptions } from 'axe-core'
2-
import type { VueWrapper } from '@vue/test-utils'
31
import type { ColumnConfig, FilterChip } from '#shared/types/preferences'
2+
import { mountSuspended } from '@nuxt/test-utils/runtime'
3+
import type { VueWrapper } from '@vue/test-utils'
44
import 'axe-core'
5+
import type { AxeResults, RunOptions } from 'axe-core'
56
import { afterEach, describe, expect, it } from 'vitest'
6-
import { mountSuspended } from '@nuxt/test-utils/runtime'
77

88
// axe-core is a UMD module that exposes itself as window.axe in the browser
99
declare const axe: {
@@ -58,7 +58,6 @@ import {
5858
AppFooter,
5959
AppHeader,
6060
BaseCard,
61-
UserAvatar,
6261
BuildEnvironment,
6362
CallToAction,
6463
CodeDirectoryListing,
@@ -67,30 +66,33 @@ import {
6766
CodeViewer,
6867
CollapsibleSection,
6968
ColumnPicker,
69+
CompareComparisonGrid,
7070
CompareFacetCard,
7171
CompareFacetRow,
7272
CompareFacetSelector,
73-
CompareComparisonGrid,
7473
ComparePackageSelector,
7574
DateTime,
7675
DependencyPathPopup,
7776
FilterChips,
7877
FilterPanel,
7978
HeaderAccountMenu,
79+
HeaderConnectorModal,
80+
HeaderSearchBox,
8081
LicenseDisplay,
8182
LoadingSpinner,
82-
PackageChartModal,
83-
PackageClaimPackageModal,
84-
HeaderConnectorModal,
8583
OrgMembersPanel,
8684
OrgOperationsQueue,
8785
OrgTeamsPanel,
8886
PackageAccessControls,
8987
PackageCard,
88+
PackageChartModal,
89+
PackageClaimPackageModal,
90+
PackageCompatibility,
9091
PackageDependencies,
9192
PackageDeprecatedTree,
9293
PackageDownloadAnalytics,
9394
PackageInstallScripts,
95+
PackageKeywords,
9496
PackageList,
9597
PackageListControls,
9698
PackageListToolbar,
@@ -108,6 +110,7 @@ import {
108110
PaginationControls,
109111
ProvenanceBadge,
110112
Readme,
113+
SearchSuggestionCard,
111114
SettingsAccentColorPicker,
112115
SettingsBgThemePicker,
113116
SettingsToggle,
@@ -118,8 +121,7 @@ import {
118121
TooltipAnnounce,
119122
TooltipApp,
120123
TooltipBase,
121-
HeaderSearchBox,
122-
SearchSuggestionCard,
124+
UserAvatar,
123125
VersionSelector,
124126
ViewModeToggle,
125127
} from '#components'
@@ -650,6 +652,49 @@ describe('component accessibility audits', () => {
650652
})
651653
})
652654

655+
describe('PackageCompatibility', () => {
656+
it('should have no accessibility violations without engines', async () => {
657+
const component = await mountSuspended(PackageCompatibility, {
658+
props: {},
659+
})
660+
const results = await runAxe(component)
661+
expect(results.violations).toEqual([])
662+
})
663+
664+
it('should have no accessibility violations with engines', async () => {
665+
const component = await mountSuspended(PackageCompatibility, {
666+
props: {
667+
engines: {
668+
node: '>=14',
669+
npm: '>=10',
670+
},
671+
},
672+
})
673+
const results = await runAxe(component)
674+
expect(results.violations).toEqual([])
675+
})
676+
})
677+
678+
describe('PackageKeywords', () => {
679+
it('should have no accessibility violations without keywords', async () => {
680+
const component = await mountSuspended(PackageKeywords, {
681+
props: {},
682+
})
683+
const results = await runAxe(component)
684+
expect(results.violations).toEqual([])
685+
})
686+
687+
it('should have no accessibility violations with keywords', async () => {
688+
const component = await mountSuspended(PackageKeywords, {
689+
props: {
690+
keywords: ['keyword1', 'keyword2'],
691+
},
692+
})
693+
const results = await runAxe(component)
694+
expect(results.violations).toEqual([])
695+
})
696+
})
697+
653698
describe('CodeViewer', () => {
654699
it('should have no accessibility violations', async () => {
655700
const component = await mountSuspended(CodeViewer, {

0 commit comments

Comments
 (0)