Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions app/components/Package/Sidebar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<script setup lang="ts">
const viewport = useWindowSize()
const scroll = useWindowScroll()
const container = useTemplateRef<HTMLDivElement>('container')
const content = useTemplateRef<HTMLDivElement>('content')
const bounds = useElementBounding(content)

const active = computed(() => {
return bounds.height.value > viewport.height.value
})

const direction = computed((previous = 'up'): string => {
if (!active.value) return 'up'
return scroll.directions.bottom ? 'down' : scroll.directions.top ? 'up' : previous
})

const offset = computed(() => {
if (!active.value) return 0
if (!container.value) return 0
if (!content.value) return 0

return direction.value === 'down'
? content.value.offsetTop
: container.value.offsetHeight - content.value.offsetTop - content.value.offsetHeight
})

const style = computed(() => {
return direction.value === 'down'
? { paddingBlockStart: `${offset.value}px` }
: { paddingBlockEnd: `${offset.value}px` }
})
</script>

<template>
<div
ref="container"
class="group relative data-[active=true]:flex"
:data-direction="direction"
:data-active="active"
:style="style"
>
<div
ref="content"
class="sticky w-full group-data-[direction=up]:(self-start top-30 xl:top-14) group-data-[direction=down]:(self-end bottom-8)"
>
<slot />
</div>
</div>
</template>
45 changes: 4 additions & 41 deletions app/pages/package/[[org]]/[name].vue
Original file line number Diff line number Diff line change
Expand Up @@ -1153,11 +1153,9 @@ onKeyStroke(
</div>
</section>
</section>
<div class="area-sidebar">
<!-- Sidebar -->
<div
class="sidebar-scroll sticky top-34 space-y-6 sm:space-y-8 min-w-0 overflow-y-auto pe-2.5 lg:(max-h-[calc(100dvh-8.5rem)] overscroll-contain) xl:(top-22 pt-2 max-h-[calc(100dvh-6rem)]) pt-1"
>

<PackageSidebar class="area-sidebar">
<div class="flex flex-col gap-4 sm:gap-6 xl:(pt-2)">
<!-- Team access controls (for scoped packages when connected) -->
<ClientOnly>
<PackageAccessControls :package-name="pkg.name" />
Expand Down Expand Up @@ -1217,7 +1215,7 @@ onKeyStroke(
<!-- Maintainers (with admin actions when connected) -->
<PackageMaintainers :package-name="pkg.name" :maintainers="pkg.maintainers" />
</div>
</div>
</PackageSidebar>
</article>

<!-- Error state -->
Expand Down Expand Up @@ -1311,41 +1309,6 @@ onKeyStroke(
grid-area: sidebar;
}

/* Sidebar scrollbar: hidden by default, shown on hover/focus */
@media (min-width: 1024px) {
.sidebar-scroll {
scrollbar-gutter: stable;
scrollbar-width: 8px;
scrollbar-color: transparent transparent;
}

.sidebar-scroll::-webkit-scrollbar {
width: 8px;
height: 8px;
}

.sidebar-scroll::-webkit-scrollbar-track,
.sidebar-scroll::-webkit-scrollbar-thumb {
background: transparent;
}

.sidebar-scroll:hover,
.sidebar-scroll:focus-within {
scrollbar-color: var(--border) transparent;
}

.sidebar-scroll:hover::-webkit-scrollbar-thumb,
.sidebar-scroll:focus-within::-webkit-scrollbar-thumb {
background-color: var(--border);
border-radius: 9999px;
}

.sidebar-scroll:hover::-webkit-scrollbar-track,
.sidebar-scroll:focus-within::-webkit-scrollbar-track {
background: transparent;
}
}

/* Improve package name wrapping for narrow screens */
.area-header h1 {
overflow-wrap: anywhere;
Expand Down
13 changes: 13 additions & 0 deletions test/nuxt/a11y.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ import {
PackageMetricsBadges,
PackagePlaygrounds,
PackageReplacement,
PackageSidebar,
PackageSkeleton,
PackageSkillsCard,
PackageTable,
Expand Down Expand Up @@ -2028,6 +2029,18 @@ describe('component accessibility audits', () => {
})
})

describe('PackageSidebar', () => {
it('should have no accessibility violations with slot content', async () => {
const component = await mountSuspended(PackageSidebar, {
slots: {
default: () => h('div', 'Sidebar content'),
},
})
const results = await runAxe(component)
expect(results.violations).toEqual([])
})
})

describe('PackageSkillsCard', () => {
it('should have no accessibility violations with skills', async () => {
const component = await mountSuspended(PackageSkillsCard, {
Expand Down
48 changes: 48 additions & 0 deletions test/nuxt/components/PackageSidebar.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { afterEach, describe, expect, it } from 'vitest'
import { mountSuspended } from '@nuxt/test-utils/runtime'
import type { VueWrapper } from '@vue/test-utils'
import Sidebar from '~/components/Package/Sidebar.vue'

const VIEWPORT_HEIGHT = window.innerHeight

function mountSidebar(contentHeight?: number) {
return mountSuspended(Sidebar, {
attachTo: document.body,
slots: contentHeight
? { default: () => h('div', { style: `height:${contentHeight}px` }) }
: { default: () => 'Sidebar Content' },
})
}

describe('PackageSidebar', () => {
let wrapper: VueWrapper

afterEach(() => {
wrapper?.unmount()
})

it('renders slot content', async () => {
wrapper = await mountSidebar()

expect(wrapper.text()).toContain('Sidebar Content')
})

it('sets active=false when content is shorter than viewport', async () => {
wrapper = await mountSidebar(100)

expect(wrapper.attributes('data-active')).toBe('false')
})

it('sets active=true when content is taller than viewport', async () => {
wrapper = await mountSidebar(VIEWPORT_HEIGHT + 500)
await nextTick()

expect(wrapper.attributes('data-active')).toBe('true')
})

it('renders with direction=up by default', async () => {
wrapper = await mountSidebar()

expect(wrapper.attributes('data-direction')).toBe('up')
})
})
Loading