diff --git a/__tests__/e2e/multi-sidebar/index.test.ts b/__tests__/e2e/multi-sidebar/index.test.ts index a8576993..294b3630 100644 --- a/__tests__/e2e/multi-sidebar/index.test.ts +++ b/__tests__/e2e/multi-sidebar/index.test.ts @@ -4,7 +4,9 @@ describe('test multi sidebar sort root', () => { }) test('using / sidebar', async () => { - const sidebarLocator = page.locator('.VPSidebarGroup .title-text') + const sidebarLocator = page.locator( + '.VPSidebarItem.level-0 > .item > .link > .text' + ) const sidebarContent = await sidebarLocator.allTextContents() expect(sidebarContent).toEqual([ @@ -22,7 +24,9 @@ describe('test multi sidebar sort order', () => { }) test('using /multi-sidebar/ sidebar', async () => { - const sidebarLocator = page.locator('.VPSidebarGroup .title-text') + const sidebarLocator = page.locator( + '.VPSidebarItem.level-0 > .item > .link > .text' + ) const sidebarContent = await sidebarLocator.allTextContents() expect(sidebarContent).toEqual(['Multi Sidebar']) diff --git a/__tests__/unit/client/theme-default/support/sidebar.test.ts b/__tests__/unit/client/theme-default/support/sidebar.test.ts index 4b451c7b..5afdb95e 100644 --- a/__tests__/unit/client/theme-default/support/sidebar.test.ts +++ b/__tests__/unit/client/theme-default/support/sidebar.test.ts @@ -1,96 +1,131 @@ -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) - }) - }) - 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) + + 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('nested sidebar sort', () => { - const nested = [ - { - text: 'D', + + describe('hasActiveLink', () => { + 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('active-1', item)).toBe(true) + expect(hasActiveLink('inactive', item)).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('active-1', item)).toBe(true) + expect(hasActiveLink('active-3', item)).toBe(true) + expect(hasActiveLink('inactive', item)).toBe(false) }) }) }) diff --git a/docs/config/theme-configs.md b/docs/config/theme-configs.md index a92ad2c1..dd0b6e9f 100644 --- a/docs/config/theme-configs.md +++ b/docs/config/theme-configs.md @@ -124,22 +124,41 @@ export default { ``` ```ts -type Sidebar = SidebarGroup[] | SidebarMulti +export type Sidebar = SidebarItem[] | SidebarMulti -interface SidebarMulti { - [path: string]: SidebarGroup[] +export interface SidebarMulti { + [path: string]: SidebarItem[] } -interface SidebarGroup { - text: string - items: SidebarItem[] +export type SidebarItem = { + /** + * The text label of the item. + */ + text?: string + + /** + * The link of the item. + */ + link?: string + + /** + * The children of the item. + */ + items?: SidebarItem[] + + /** + * If `true`, toggle button is shown. + * + * @default false + */ collapsible?: boolean - collapsed?: boolean -} -interface SidebarItem { - text: string - link: string + /** + * If `true`, collapsible group is collapsed by default. + * + * @default false + */ + collapsed?: boolean } ``` diff --git a/docs/guide/theme-sidebar.md b/docs/guide/theme-sidebar.md index 21d78723..418cbf27 100644 --- a/docs/guide/theme-sidebar.md +++ b/docs/guide/theme-sidebar.md @@ -1,6 +1,6 @@ # Sidebar -The sidebar is the main navigation block for your documentation. You can configure the sidebar menu in `themeConfig.sidebar`. +The sidebar is the main navigation block for your documentation. You can configure the sidebar menu in [`themeConfig.sidebar`](/config/theme-configs#sidebar). ```js export default { @@ -21,7 +21,7 @@ export default { ## The Basics -The simplest form of the sidebar menu is passing in a single array of links. The first level item defines the "section" for the sidebar. It should contain `text`, which is the title of the section, and `items` which are the actual navigation links. +The simplest form of the sidebar menu is passing in a single array of links. The first level item defines the "section" for the sidebar. It should contain `text`, which is the title of the section, and `items` which are the actual navigation links. ```js export default { @@ -66,6 +66,33 @@ export default { } ``` +You may further nest the sidebar items up to 6 level deep counting up from the root level. Note that deeper than 6 level of nested items gets ignored and will not be displayed on the sidebar. + +```js +export default { + themeConfig: { + sidebar: [ + { + text: 'Level 1', + items: [ + { + text: 'Level 2', + items: [ + { + text: 'Level 3', + items: [ + ... + ] + } + ] + } + ] + } + ] + } +} +``` + ## Multiple Sidebars You may show different sidebar depending on the page path. For example, as shown on this site, you might want to create a separate sections of content in your documentation like "Guide" page and "Config" page. @@ -90,30 +117,28 @@ Then, update your configuration to define your sidebar for each section. This ti export default { themeConfig: { sidebar: { - // This sidebar gets displayed when user is - // under `guide` directory. + // This sidebar gets displayed when a user + // is on `guide` directory. '/guide/': [ { text: 'Guide', items: [ - // This shows `/guide/index.md` page. - { text: 'Index', link: '/guide/' }, // /guide/index.md - { text: 'One', link: '/guide/one' }, // /guide/one.md - { text: 'Two', link: '/guide/two' } // /guide/two.md + { text: 'Index', link: '/guide/' }, + { text: 'One', link: '/guide/one' }, + { text: 'Two', link: '/guide/two' } ] } ], - // This sidebar gets displayed when user is - // under `config` directory. + // This sidebar gets displayed when a user + // is on `config` directory. '/config/': [ { text: 'Config', items: [ - // This shows `/config/index.md` page. - { text: 'Index', link: '/config/' }, // /config/index.md - { text: 'Three', link: '/config/three' }, // /config/three.md - { text: 'Four', link: '/config/four' } // /config/four.md + { text: 'Index', link: '/config/' }, + { text: 'Three', link: '/config/three' }, + { text: 'Four', link: '/config/four' } ] } ] diff --git a/src/client/theme-default/components/VPLink.vue b/src/client/theme-default/components/VPLink.vue index f960d327..40b9e9d6 100644 --- a/src/client/theme-default/components/VPLink.vue +++ b/src/client/theme-default/components/VPLink.vue @@ -5,16 +5,18 @@ import VPIconExternalLink from './icons/VPIconExternalLink.vue' import { EXTERNAL_URL_RE } from '../../shared.js' const props = defineProps<{ + tag?: string href?: string noIcon?: boolean }>() +const tag = computed(() => props.tag ?? props.href ? 'a' : 'span') const isExternal = computed(() => props.href && EXTERNAL_URL_RE.test(props.href))