Skip to content

Commit

Permalink
feat: allow using components in navigation bar (#4000)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: Divyansh Singh <[email protected]>
  • Loading branch information
userquin and brc-dd committed Jul 7, 2024
1 parent fa81e89 commit fa87d81
Show file tree
Hide file tree
Showing 13 changed files with 969 additions and 437 deletions.
46 changes: 46 additions & 0 deletions __tests__/e2e/.vitepress/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,50 @@
import { defineConfig, type DefaultTheme } from 'vitepress'

const nav: DefaultTheme.Config['nav'] = [
{
text: 'Home',
link: '/'
},
{
text: 'API Reference',
items: [
{
text: 'Example',
link: '/home.html'
},
{
component: 'ApiPreference',
props: {
options: ['JavaScript', 'TypeScript', 'Flow'],
defaultOption: 'TypeScript'
}
},
{
component: 'ApiPreference',
props: {
options: ['Options', 'Composition'],
defaultOption: 'Composition'
}
}
]
},
{
component: 'NavVersion',
props: {
versions: [
{
text: 'v1.x',
link: '/'
},
{
text: 'v0.x',
link: '/v0.x/'
}
]
}
}
]

const sidebar: DefaultTheme.Config['sidebar'] = {
'/': [
{
Expand Down Expand Up @@ -92,6 +137,7 @@ export default defineConfig({
}
},
themeConfig: {
nav,
sidebar,
search: {
provider: 'local',
Expand Down
83 changes: 83 additions & 0 deletions __tests__/e2e/.vitepress/theme/components/ApiPreference.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<script setup lang="ts">
import { useLocalStorage } from '@vueuse/core'
const props = defineProps<{
options: string[]
defaultOption: string
screenMenu?: boolean
}>()
// reactivity isn't needed for props here
const key = removeSpaces(`api-preference-${props.options.join('-')}`)
const name = key + (props.screenMenu ? '-screen-menu' : '')
const selected = useLocalStorage(key, () => props.defaultOption)
const optionsWithKeys = props.options.map((option) => ({
key: name + '-' + removeSpaces(option),
value: option
}))
function removeSpaces(str: string) {
return str.replace(/\s/g, '_')
}
</script>

<template>
<div class="VPApiPreference" :class="{ 'screen-menu': screenMenu }">
<template v-for="option in optionsWithKeys" :key="option">
<input
type="radio"
:id="option.key"
:name="name"
:value="option.value"
v-model="selected"
/>
<label :for="option.key">{{ option.value }}</label>
</template>
</div>
</template>

<style scoped>
.VPApiPreference {
display: flex;
margin: 12px 0;
border: 1px solid var(--vp-c-border);
border-radius: 6px;
font-size: 14px;
color: var(--vp-c-text-1);
}
.VPApiPreference:first-child {
margin-top: 0;
}
.VPApiPreference:last-child {
margin-bottom: 0;
}
.VPApiPreference.screen-menu {
margin: 12px 0 0 12px;
}
.VPApiPreference input[type='radio'] {
pointer-events: none;
position: fixed;
opacity: 0;
}
.VPApiPreference label {
flex: 1;
margin: 2px;
padding: 4px 12px;
cursor: pointer;
border-radius: 4px;
text-align: center;
}
.VPApiPreference input[type='radio']:checked + label {
background-color: var(--vp-c-default-soft);
color: var(--vp-c-brand-1);
}
</style>
50 changes: 50 additions & 0 deletions __tests__/e2e/.vitepress/theme/components/NavVersion.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute } from 'vitepress'
import VPNavBarMenuGroup from 'vitepress/dist/client/theme-default/components/VPNavBarMenuGroup.vue'
import VPNavScreenMenuGroup from 'vitepress/dist/client/theme-default/components/VPNavScreenMenuGroup.vue'
const props = defineProps<{
versions: { text: string; link: string }[]
screenMenu?: boolean
}>()
const route = useRoute()
const sortedVersions = computed(() => {
return [...props.versions].sort(
(a, b) => b.link.split('/').length - a.link.split('/').length
)
})
const currentVersion = computed(() => {
return (
sortedVersions.value.find((version) => route.path.startsWith(version.link))
?.text || 'Versions'
)
})
</script>

<template>
<VPNavBarMenuGroup
v-if="!screenMenu"
:item="{ text: currentVersion, items: versions }"
class="VPNavVersion"
/>
<VPNavScreenMenuGroup
v-else
:text="currentVersion"
:items="versions"
class="VPNavVersion"
/>
</template>

<style scoped>
.VPNavVersion :deep(button .text) {
color: var(--vp-c-text-1) !important;
}
.VPNavVersion:hover :deep(button .text) {
color: var(--vp-c-text-2) !important;
}
</style>
12 changes: 12 additions & 0 deletions __tests__/e2e/.vitepress/theme/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { Theme } from 'vitepress'
import DefaultTheme from 'vitepress/theme'
import ApiPreference from './components/ApiPreference.vue'
import NavVersion from './components/NavVersion.vue'

export default {
extends: DefaultTheme,
enhanceApp({ app }) {
app.component('ApiPreference', ApiPreference)
app.component('NavVersion', NavVersion)
}
} satisfies Theme
54 changes: 54 additions & 0 deletions docs/en/reference/default-theme-nav.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,57 @@ export default {
## Social Links

Refer [`socialLinks`](./default-theme-config#sociallinks).

## Custom Components

You can include custom components in the navigation bar by using the `component` option. The `component` key should be the Vue component name, and must be registered globally using [Theme.enhanceApp](../guide/custom-theme#theme-interface).

```js
// .vitepress/config.js
export default {
themeConfig: {
nav: [
{
text: 'My Menu',
items: [
{
component: 'MyCustomComponent',
// Optional props to pass to the component
props: {
title: 'My Custom Component'
}
}
]
},
{
component: 'AnotherCustomComponent'
}
]
}
}
```

Then, you need to register the component globally:

```js
// .vitepress/theme/index.js
import DefaultTheme from 'vitepress/theme'

import MyCustomComponent from './components/MyCustomComponent.vue'
import AnotherCustomComponent from './components/AnotherCustomComponent.vue'

/** @type {import('vitepress').Theme} */
export default {
extends: DefaultTheme,
enhanceApp({ app }) {
app.component('MyCustomComponent', MyCustomComponent)
app.component('AnotherCustomComponent', AnotherCustomComponent)
}
}
```

Your component will be rendered in the navigation bar. VitePress will provide the following additional props to the component:

- `screenMenu`: an optional boolean indicating whether the component is inside mobile navigation menu

You can check an example in the e2e tests [here](https://github.com/vuejs/vitepress/tree/main/__tests__/e2e/.vitepress).
30 changes: 15 additions & 15 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,20 +100,20 @@
"dependencies": {
"@docsearch/css": "^3.6.0",
"@docsearch/js": "^3.6.0",
"@shikijs/core": "^1.9.0",
"@shikijs/transformers": "^1.9.0",
"@shikijs/core": "^1.10.3",
"@shikijs/transformers": "^1.10.3",
"@types/markdown-it": "^14.1.1",
"@vitejs/plugin-vue": "^5.0.5",
"@vue/devtools-api": "^7.3.4",
"@vue/shared": "^3.4.30",
"@vue/devtools-api": "^7.3.5",
"@vue/shared": "^3.4.31",
"@vueuse/core": "^10.11.0",
"@vueuse/integrations": "^10.11.0",
"focus-trap": "^7.5.4",
"mark.js": "8.11.1",
"minisearch": "^6.3.0",
"shiki": "^1.9.0",
"vite": "^5.3.1",
"vue": "^3.4.30"
"shiki": "^1.10.3",
"vite": "^5.3.3",
"vue": "^3.4.31"
},
"devDependencies": {
"@clack/prompts": "^0.7.0",
Expand All @@ -138,24 +138,24 @@
"@types/markdown-it-attrs": "^4.1.3",
"@types/markdown-it-container": "^2.0.10",
"@types/markdown-it-emoji": "^3.0.1",
"@types/micromatch": "^4.0.7",
"@types/micromatch": "^4.0.9",
"@types/minimist": "^1.2.5",
"@types/node": "^20.14.8",
"@types/node": "^20.14.10",
"@types/postcss-prefix-selector": "^1.16.3",
"@types/prompts": "^2.4.9",
"chokidar": "^3.6.0",
"conventional-changelog-cli": "^5.0.0",
"cross-spawn": "^7.0.3",
"debug": "^4.3.5",
"esbuild": "^0.21.5",
"esbuild": "^0.23.0",
"execa": "^9.3.0",
"fast-glob": "^3.3.2",
"fs-extra": "^11.2.0",
"get-port": "^7.1.0",
"gray-matter": "^4.0.3",
"lint-staged": "^15.2.7",
"lodash.template": "^4.5.0",
"lru-cache": "^10.2.2",
"lru-cache": "^10.3.1",
"markdown-it": "^14.1.0",
"markdown-it-anchor": "^9.0.1",
"markdown-it-attrs": "^4.1.6",
Expand All @@ -170,13 +170,13 @@
"path-to-regexp": "^6.2.2",
"picocolors": "^1.0.1",
"pkg-dir": "^8.0.0",
"playwright-chromium": "^1.44.1",
"playwright-chromium": "^1.45.1",
"polka": "^1.0.0-next.25",
"postcss-prefix-selector": "^1.16.1",
"prettier": "^3.3.2",
"prompts": "^2.4.2",
"punycode": "^2.3.1",
"rimraf": "^5.0.7",
"rimraf": "^5.0.8",
"rollup": "^4.18.0",
"rollup-plugin-dts": "^6.1.1",
"rollup-plugin-esbuild": "^6.1.1",
Expand All @@ -185,9 +185,9 @@
"sirv": "^2.0.4",
"sitemap": "^8.0.0",
"supports-color": "^9.4.0",
"typescript": "^5.5.2",
"typescript": "^5.5.3",
"vitest": "^1.6.0",
"vue-tsc": "^2.0.22",
"vue-tsc": "^2.0.26",
"wait-on": "^7.2.0"
},
"peerDependencies": {
Expand Down
Loading

0 comments on commit fa87d81

Please sign in to comment.