[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: pin the sidebar only when necessary (#968)

authored by

Thomas Deinhamer and committed by
GitHub
d3800dcf 0105befd

+114 -41
+49
app/components/Package/Sidebar.vue
··· 1 + <script setup lang="ts"> 2 + const viewport = useWindowSize() 3 + const scroll = useWindowScroll() 4 + const container = useTemplateRef<HTMLDivElement>('container') 5 + const content = useTemplateRef<HTMLDivElement>('content') 6 + const bounds = useElementBounding(content) 7 + 8 + const active = computed(() => { 9 + return bounds.height.value > viewport.height.value 10 + }) 11 + 12 + const direction = computed((previous = 'up'): string => { 13 + if (!active.value) return 'up' 14 + return scroll.directions.bottom ? 'down' : scroll.directions.top ? 'up' : previous 15 + }) 16 + 17 + const offset = computed(() => { 18 + if (!active.value) return 0 19 + if (!container.value) return 0 20 + if (!content.value) return 0 21 + 22 + return direction.value === 'down' 23 + ? content.value.offsetTop 24 + : container.value.offsetHeight - content.value.offsetTop - content.value.offsetHeight 25 + }) 26 + 27 + const style = computed(() => { 28 + return direction.value === 'down' 29 + ? { paddingBlockStart: `${offset.value}px` } 30 + : { paddingBlockEnd: `${offset.value}px` } 31 + }) 32 + </script> 33 + 34 + <template> 35 + <div 36 + ref="container" 37 + class="group relative data-[active=true]:flex" 38 + :data-direction="direction" 39 + :data-active="active" 40 + :style="style" 41 + > 42 + <div 43 + ref="content" 44 + class="sticky w-full group-data-[direction=up]:(self-start top-30 xl:top-14) group-data-[direction=down]:(self-end bottom-8)" 45 + > 46 + <slot /> 47 + </div> 48 + </div> 49 + </template>
+4 -41
app/pages/package/[[org]]/[name].vue
··· 1153 1153 </div> 1154 1154 </section> 1155 1155 </section> 1156 - <div class="area-sidebar"> 1157 - <!-- Sidebar --> 1158 - <div 1159 - 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" 1160 - > 1156 + 1157 + <PackageSidebar class="area-sidebar"> 1158 + <div class="flex flex-col gap-4 sm:gap-6 xl:(pt-2)"> 1161 1159 <!-- Team access controls (for scoped packages when connected) --> 1162 1160 <ClientOnly> 1163 1161 <PackageAccessControls :package-name="pkg.name" /> ··· 1217 1215 <!-- Maintainers (with admin actions when connected) --> 1218 1216 <PackageMaintainers :package-name="pkg.name" :maintainers="pkg.maintainers" /> 1219 1217 </div> 1220 - </div> 1218 + </PackageSidebar> 1221 1219 </article> 1222 1220 1223 1221 <!-- Error state --> ··· 1309 1307 1310 1308 .area-sidebar { 1311 1309 grid-area: sidebar; 1312 - } 1313 - 1314 - /* Sidebar scrollbar: hidden by default, shown on hover/focus */ 1315 - @media (min-width: 1024px) { 1316 - .sidebar-scroll { 1317 - scrollbar-gutter: stable; 1318 - scrollbar-width: 8px; 1319 - scrollbar-color: transparent transparent; 1320 - } 1321 - 1322 - .sidebar-scroll::-webkit-scrollbar { 1323 - width: 8px; 1324 - height: 8px; 1325 - } 1326 - 1327 - .sidebar-scroll::-webkit-scrollbar-track, 1328 - .sidebar-scroll::-webkit-scrollbar-thumb { 1329 - background: transparent; 1330 - } 1331 - 1332 - .sidebar-scroll:hover, 1333 - .sidebar-scroll:focus-within { 1334 - scrollbar-color: var(--border) transparent; 1335 - } 1336 - 1337 - .sidebar-scroll:hover::-webkit-scrollbar-thumb, 1338 - .sidebar-scroll:focus-within::-webkit-scrollbar-thumb { 1339 - background-color: var(--border); 1340 - border-radius: 9999px; 1341 - } 1342 - 1343 - .sidebar-scroll:hover::-webkit-scrollbar-track, 1344 - .sidebar-scroll:focus-within::-webkit-scrollbar-track { 1345 - background: transparent; 1346 - } 1347 1310 } 1348 1311 1349 1312 /* Improve package name wrapping for narrow screens */
+13
test/nuxt/a11y.spec.ts
··· 127 127 PackageMetricsBadges, 128 128 PackagePlaygrounds, 129 129 PackageReplacement, 130 + PackageSidebar, 130 131 PackageSkeleton, 131 132 PackageSkillsCard, 132 133 PackageTable, ··· 2021 2022 moduleName: 'moment', 2022 2023 docPath: 'moment', 2023 2024 }, 2025 + }, 2026 + }) 2027 + const results = await runAxe(component) 2028 + expect(results.violations).toEqual([]) 2029 + }) 2030 + }) 2031 + 2032 + describe('PackageSidebar', () => { 2033 + it('should have no accessibility violations with slot content', async () => { 2034 + const component = await mountSuspended(PackageSidebar, { 2035 + slots: { 2036 + default: () => h('div', 'Sidebar content'), 2024 2037 }, 2025 2038 }) 2026 2039 const results = await runAxe(component)
+48
test/nuxt/components/PackageSidebar.spec.ts
··· 1 + import { afterEach, describe, expect, it } from 'vitest' 2 + import { mountSuspended } from '@nuxt/test-utils/runtime' 3 + import type { VueWrapper } from '@vue/test-utils' 4 + import Sidebar from '~/components/Package/Sidebar.vue' 5 + 6 + const VIEWPORT_HEIGHT = window.innerHeight 7 + 8 + function mountSidebar(contentHeight?: number) { 9 + return mountSuspended(Sidebar, { 10 + attachTo: document.body, 11 + slots: contentHeight 12 + ? { default: () => h('div', { style: `height:${contentHeight}px` }) } 13 + : { default: () => 'Sidebar Content' }, 14 + }) 15 + } 16 + 17 + describe('PackageSidebar', () => { 18 + let wrapper: VueWrapper 19 + 20 + afterEach(() => { 21 + wrapper?.unmount() 22 + }) 23 + 24 + it('renders slot content', async () => { 25 + wrapper = await mountSidebar() 26 + 27 + expect(wrapper.text()).toContain('Sidebar Content') 28 + }) 29 + 30 + it('sets active=false when content is shorter than viewport', async () => { 31 + wrapper = await mountSidebar(100) 32 + 33 + expect(wrapper.attributes('data-active')).toBe('false') 34 + }) 35 + 36 + it('sets active=true when content is taller than viewport', async () => { 37 + wrapper = await mountSidebar(VIEWPORT_HEIGHT + 500) 38 + await nextTick() 39 + 40 + expect(wrapper.attributes('data-active')).toBe('true') 41 + }) 42 + 43 + it('renders with direction=up by default', async () => { 44 + wrapper = await mountSidebar() 45 + 46 + expect(wrapper.attributes('data-direction')).toBe('up') 47 + }) 48 + })