Skip to content

Commit

Permalink
feat(theme): enable 2nd level sidebar collapse
Browse files Browse the repository at this point in the history
  • Loading branch information
kiaking committed Jan 20, 2023
1 parent 05430e4 commit 89fa070
Show file tree
Hide file tree
Showing 8 changed files with 320 additions and 161 deletions.
204 changes: 123 additions & 81 deletions __tests__/unit/client/theme-default/support/sidebar.test.ts
Original file line number Diff line number Diff line change
@@ -1,96 +1,138 @@
import { getSidebar } from 'client/theme-default/support/sidebar'
import { getSidebar, hasActiveLink } from 'client/theme-default/support/sidebar'

describe('client/theme-default/support/sidebar', () => {
const root = [
{
text: 'A',
collapsible: true,
items: [
{
text: 'A',
link: ''
}
]
},
{
text: 'B',
items: [
{
text: 'B',
link: ''
}
]
}
]
const another = [
{
text: 'C',
items: [
describe('getSidebar', () => {
const root = [
{
text: 'A',
collapsible: true,
items: [{ text: 'A', link: '' }]
},
{
text: 'B',
items: [{ text: 'B', link: '' }]
}
]

const another = [
{
text: 'C',
items: [{ text: 'C', link: '' }]
}
]

describe('normal sidebar sort', () => {
const normalSidebar = {
'/': root,
'/multi-sidebar/': another
}

test('gets `/` sidebar', () => {
expect(getSidebar(normalSidebar, '/')).toBe(root)
})

test('gets `/multi-sidebar/` sidebar', () => {
expect(getSidebar(normalSidebar, '/multi-sidebar/')).toBe(another)
})

test('gets `/` sidebar again', () => {
expect(getSidebar(normalSidebar, '/some-entry.html')).toBe(root)
})
})

describe('reversed sidebar sort', () => {
const reversedSidebar = {
'/multi-sidebar/': another,
'/': root
}

test('gets `/` sidebar', () => {
expect(getSidebar(reversedSidebar, '/')).toBe(root)
})

test('gets `/multi-sidebar/` sidebar', () => {
expect(getSidebar(reversedSidebar, '/multi-sidebar/')).toBe(another)
})

test('gets `/` sidebar again', () => {
expect(getSidebar(reversedSidebar, '/some-entry.html')).toBe(root)
})
})

describe('nested sidebar sort', () => {
const nested = [
{
text: 'C',
link: ''
text: 'D',
items: [{ text: 'D', link: '' }]
}
]
}
]
describe('normal sidebar sort', () => {
const normalSidebar = {
'/': root,
'/multi-sidebar/': another
}
test('gets / sidebar', () => {
expect(getSidebar(normalSidebar, '/')).toBe(root)
})
test('gets /multi-sidebar/ sidebar', () => {
expect(getSidebar(normalSidebar, '/multi-sidebar/')).toBe(another)
})
test('gets / sidebar again', () => {
expect(getSidebar(normalSidebar, '/some-entry.html')).toBe(root)

const nestedSidebar = {
'/': root,
'/multi-sidebar/': another,
'/multi-sidebar/nested/': nested
}

test('gets `/` sidebar', () => {
expect(getSidebar(nestedSidebar, '/')).toBe(root)
})

test('gets `/multi-sidebar/` sidebar', () => {
expect(getSidebar(nestedSidebar, '/multi-sidebar/')).toBe(another)
})

test('gets `/multi-sidebar/nested/` sidebar', () => {
expect(getSidebar(nestedSidebar, '/multi-sidebar/nested/')).toBe(nested)
})

test('gets `/` sidebar again', () => {
expect(getSidebar(nestedSidebar, '/some-entry.html')).toBe(root)
})
})
})
describe('reversed sidebar sort', () => {
const reversedSidebar = {
'/multi-sidebar/': another,
'/': root
}
test('gets / sidebar', () => {
expect(getSidebar(reversedSidebar, '/')).toBe(root)
})
test('gets /multi-sidebar/ sidebar', () => {
expect(getSidebar(reversedSidebar, '/multi-sidebar/')).toBe(another)
})
test('gets / sidebar again', () => {
expect(getSidebar(reversedSidebar, '/some-entry.html')).toBe(root)

describe('hasActiveLink', () => {
test('checks `SidebarLink`', () => {
const item = { text: 'Item 001', link: '/active' }

expect(hasActiveLink(item, 'active')).toBe(true)
expect(hasActiveLink(item, 'inactive')).toBe(false)
})
})
describe('nested sidebar sort', () => {
const nested = [
{
text: 'D',

test('checks `SidebarItem`', () => {
const item = {
text: 'Item 001',
items: [
{
text: 'D',
link: ''
}
{ text: 'Item 001', link: '/active-1' },
{ text: 'Item 002', link: '/active-2' }
]
}
]
const nestedSidebar = {
'/': root,
'/multi-sidebar/': another,
'/multi-sidebar/nested/': nested
}
test('gets / sidebar', () => {
expect(getSidebar(nestedSidebar, '/')).toBe(root)
})
test('gets /multi-sidebar/ sidebar', () => {
expect(getSidebar(nestedSidebar, '/multi-sidebar/')).toBe(another)
})
test('gets /multi-sidebar/nested/ sidebar', () => {
expect(getSidebar(nestedSidebar, '/multi-sidebar/nested/')).toBe(nested)

expect(hasActiveLink(item, 'active-1')).toBe(true)
expect(hasActiveLink(item, 'inactive')).toBe(false)
})
test('gets / sidebar again', () => {
expect(getSidebar(nestedSidebar, '/some-entry.html')).toBe(root)

test('checks `SidebarItem[]`', () => {
const item = [
{
text: 'Item 001',
items: [
{ text: 'Item 001', link: '/active-1' },
{ text: 'Item 002', link: '/active-2' }
]
},
{
text: 'Item 002',
items: [
{ text: 'Item 003', link: '/active-3' },
{ text: 'Item 004', link: '/active-4' }
]
}
]

expect(hasActiveLink(item, 'active-1')).toBe(true)
expect(hasActiveLink(item, 'active-3')).toBe(true)
expect(hasActiveLink(item, 'inactive')).toBe(false)
})
})
})
7 changes: 1 addition & 6 deletions src/client/theme-default/components/VPSidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,7 @@ watchPostEffect(async () => {
<slot name="sidebar-nav-before" />

<div v-for="group in sidebar" :key="group.text" class="group">
<VPSidebarGroup
:text="group.text"
:items="group.items"
:collapsible="group.collapsible"
:collapsed="group.collapsed"
/>
<VPSidebarGroup :group="group" />
</div>

<slot name="sidebar-nav-after" />
Expand Down
45 changes: 16 additions & 29 deletions src/client/theme-default/components/VPSidebarGroup.vue
Original file line number Diff line number Diff line change
@@ -1,56 +1,43 @@
<script lang="ts" setup>
import { computed } from 'vue'
import type { DefaultTheme } from 'vitepress/theme'
import { ref, watchEffect } from 'vue'
import { useData } from '../composables/data.js'
import { isActive } from '../support/utils.js'
import { useSidebarControl } from '../composables/sidebar.js'
import VPIconPlusSquare from './icons/VPIconPlusSquare.vue'
import VPIconMinusSquare from './icons/VPIconMinusSquare.vue'
import VPSidebarSection from './VPSidebarSection.vue'
import VPSidebarLink from './VPSidebarLink.vue'
const props = defineProps<{
text?: string
items: DefaultTheme.SidebarItem[]
collapsible?: boolean
collapsed?: boolean
group: DefaultTheme.SidebarGroup
}>()
const collapsed = ref(false)
watchEffect(() => {
collapsed.value = !!(props.collapsible && props.collapsed)
})
const { collapsed, toggle } = useSidebarControl(computed(() => props.group))
const { page } = useData()
watchEffect(() => {
if(props.items.some((item) => { return isActive(page.value.relativePath, item.link) })){
collapsed.value = false
}
})
function toggle() {
if (props.collapsible) {
collapsed.value = !collapsed.value
}
}
const classes = computed(() => ({
collapsible: props.group.collapsible,
collapsed: collapsed.value
}))
</script>

<template>
<section class="VPSidebarGroup" :class="{ collapsible, collapsed }">
<section class="VPSidebarGroup" :class="classes">
<div
v-if="text"
v-if="group.text"
class="title"
:role="collapsible ? 'button' : undefined"
:role="group.collapsible ? 'button' : undefined"
@click="toggle"
>
<h2 v-html="text" class="title-text"></h2>
<h2 class="title-text" v-html="group.text" />
<div class="action">
<VPIconMinusSquare class="icon minus" />
<VPIconPlusSquare class="icon plus" />
</div>
</div>

<div class="items">
<template v-for="item in items" :key="item.link">
<VPSidebarLink :item="item" />
<template v-for="item in group.items">
<VPSidebarSection v-if="'items' in item" :key="item.text" :item="item" />
<VPSidebarLink v-else :key="item.link" :item="item" />
</template>
</div>
</section>
Expand Down
44 changes: 14 additions & 30 deletions src/client/theme-default/components/VPSidebarLink.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,11 @@ import { useSidebar } from '../composables/sidebar.js'
import { isActive } from '../support/utils.js'
import VPLink from './VPLink.vue'
const props = withDefaults(
defineProps<{ item: DefaultTheme.SidebarItem; depth?: number }>(),
{ depth: 1 }
)
const props = defineProps<{
item: DefaultTheme.SidebarLink
}>()
const { page, frontmatter } = useData()
const maxDepth = computed<number>(
() => frontmatter.value.sidebarDepth || Infinity
)
const { page } = useData()
const active = computed(() =>
isActive(page.value.relativePath, props.item.link)
Expand All @@ -34,55 +30,43 @@ watchEffect(() => {

<template>
<VPLink
class="link"
class="VPSidebarLink"
:class="{ active }"
:style="{ paddingLeft: 16 * (depth - 1) + 'px' }"
:href="item.link"
:tabindex="isSidebarEnabled || isSidebarOpen ? 0 : -1"
@click="closeSideBar"
ref="link"
@click="closeSideBar"
>
<span v-html="item.text" class="link-text" :class="{ light: depth > 1 }"></span>
<span class="link-text" v-html="item.text" />
</VPLink>
<template
v-if="'items' in item && depth < maxDepth"
v-for="child in item.items"
:key="child.link"
>
<VPSidebarLink :item="child" :depth="depth + 1" />
</template>
</template>

<style scoped>
.link {
.VPSidebarLink {
display: block;
margin: 4px 0;
padding: 4px 0;
color: var(--vp-c-text-2);
transition: color 0.5s;
}
.link:hover {
.VPSidebarLink:hover {
color: var(--vp-c-text-1);
}
.link.active {
.VPSidebarLink.active {
color: var(--vp-c-brand);
}
.link :deep(.icon) {
.VPSidebarLink :deep(.icon) {
width: 12px;
height: 12px;
fill: currentColor;
}
.link-text {
line-height: 20px;
display: inline-block;
line-height: 24px;
font-size: 14px;
font-weight: 500;
}
.link-text.light {
font-size: 13px;
font-weight: 400;
}
</style>
Loading

0 comments on commit 89fa070

Please sign in to comment.