Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(theme): add 'inert' attribute to prevent unnecessary traversal of hidden content #3392

Draft
wants to merge 18 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/client/app/index.ts
Expand Up @@ -17,6 +17,7 @@ import { usePrefetch } from './composables/preFetch'
import { dataSymbol, initData, siteDataRef, useData } from './data'
import { RouterSymbol, createRouter, scrollTo, type Router } from './router'
import { inBrowser, pathToFile } from './utils'
import { provideInert } from './inert'

function resolveThemeExtends(theme: typeof RawTheme): typeof RawTheme {
if (theme.extends) {
Expand Down Expand Up @@ -73,6 +74,8 @@ export async function createApp() {
const data = initData(router.route)
app.provide(dataSymbol, data)

provideInert(app)

// install global components
app.component('Content', Content)
app.component('ClientOnly', ClientOnly)
Expand Down
60 changes: 60 additions & 0 deletions src/client/app/inert.ts
@@ -0,0 +1,60 @@
import {
type App,
computed,
inject,
reactive,
type UnwrapNestedRefs
} from 'vue'

const inertSymbol = Symbol()
const inertStateSymbol = Symbol()

export interface Inert {
isSidebarOpen: boolean
isScreenOpen: boolean
isSidebarEnabled: boolean
onAfterRouteChanged: () => void
}

export interface InertState {
inertSkipLink: boolean
inertNav: boolean
inertLocalNav: boolean
inertSidebar: boolean
inertContent: boolean
inertFooter: boolean
}

export function useInert() {
return inject<UnwrapNestedRefs<Inert>>(inertSymbol)
}

export function useInertState() {
return inject<UnwrapNestedRefs<InertState>>(inertStateSymbol)
}

export function provideInert(app: App) {
const inert = reactive({
isSidebarOpen: false,
isScreenOpen: false,
isSidebarEnabled: false,
onAfterRouteChanged() {
userquin marked this conversation as resolved.
Show resolved Hide resolved
inert.isSidebarOpen = false
inert.isScreenOpen = false
}
})
const inertState = reactive({
inertSkipLink: computed(() => inert.isSidebarOpen || inert.isScreenOpen),
inertNav: computed(() => inert.isSidebarOpen),
inertLocalNav: computed(() => inert.isSidebarOpen || inert.isScreenOpen),
inertSidebar: computed(
() =>
!inert.isSidebarEnabled && (!inert.isSidebarOpen || inert.isScreenOpen)
),
inertContent: computed(() => inert.isSidebarOpen || inert.isScreenOpen),
inertFooter: computed(() => inert.isSidebarOpen || inert.isScreenOpen)
})

app.provide(inertSymbol, inert)
app.provide(inertStateSymbol, inertState)
}
2 changes: 2 additions & 0 deletions src/client/index.ts
Expand Up @@ -3,6 +3,7 @@

// generic types
export type { VitePressData } from './app/data'
export type { Inert, InertState } from './app/inert'
export type { Route, Router } from './app/router'

// theme types
Expand All @@ -13,6 +14,7 @@ export type { HeadConfig, Header, PageData, SiteData } from '../../types/shared'

// composables
export { useData, dataSymbol } from './app/data'
export { useInert, useInertState } from './app/inert'
export { useRoute, useRouter } from './app/router'

// utilities
Expand Down
15 changes: 8 additions & 7 deletions src/client/theme-default/Layout.vue
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { useRoute } from 'vitepress'
import { useInertState, useRoute } from 'vitepress'
import { computed, provide, useSlots, watch } from 'vue'
import VPBackdrop from './components/VPBackdrop.vue'
import VPContent from './components/VPContent.vue'
Expand All @@ -17,6 +17,7 @@ const {
close: closeSidebar
} = useSidebar()

const inert = useInertState()
const route = useRoute()
watch(() => route.path, closeSidebar)

Expand All @@ -33,24 +34,24 @@ provide('hero-image-slot-exists', heroImageSlotExists)
<template>
<div v-if="frontmatter.layout !== false" class="Layout" :class="frontmatter.pageClass" >
<slot name="layout-top" />
<VPSkipLink />
<VPSkipLink :inert="inert?.inertSkipLink" />
<VPBackdrop class="backdrop" :show="isSidebarOpen" @click="closeSidebar" />
<VPNav>
<VPNav :inert="inert?.inertNav">
<template #nav-bar-title-before><slot name="nav-bar-title-before" /></template>
<template #nav-bar-title-after><slot name="nav-bar-title-after" /></template>
<template #nav-bar-content-before><slot name="nav-bar-content-before" /></template>
<template #nav-bar-content-after><slot name="nav-bar-content-after" /></template>
<template #nav-screen-content-before><slot name="nav-screen-content-before" /></template>
<template #nav-screen-content-after><slot name="nav-screen-content-after" /></template>
</VPNav>
<VPLocalNav :open="isSidebarOpen" @open-menu="openSidebar" />
<VPLocalNav :inert="inert?.inertLocalNav" :open="isSidebarOpen" @open-menu="openSidebar" />

<VPSidebar :open="isSidebarOpen">
<VPSidebar :inert="inert?.inertSidebar" :open="isSidebarOpen">
<template #sidebar-nav-before><slot name="sidebar-nav-before" /></template>
<template #sidebar-nav-after><slot name="sidebar-nav-after" /></template>
</VPSidebar>

<VPContent>
<VPContent :inert="inert?.inertContent">
<template #page-top><slot name="page-top" /></template>
<template #page-bottom><slot name="page-bottom" /></template>

Expand All @@ -76,7 +77,7 @@ provide('hero-image-slot-exists', heroImageSlotExists)
<template #aside-ads-after><slot name="aside-ads-after" /></template>
</VPContent>

<VPFooter />
<VPFooter :inert="inert?.inertFooter" />
<slot name="layout-bottom" />
</div>
<Content v-else />
Expand Down
44 changes: 28 additions & 16 deletions src/client/theme-default/components/VPLocalNavOutlineDropdown.vue
@@ -1,37 +1,50 @@
<script setup lang="ts">
import { onClickOutside, onKeyStroke } from '@vueuse/core'
import { onKeyStroke } from '@vueuse/core'
import { onContentUpdated } from 'vitepress'
import { nextTick, ref } from 'vue'
import { useData } from '../composables/data'
import { resolveTitle, type MenuItem } from '../composables/outline'
import VPDocOutlineItem from './VPDocOutlineItem.vue'
import VPIconChevronRight from './icons/VPIconChevronRight.vue'
import { useFocusTrap } from '@vueuse/integrations/useFocusTrap'

const props = defineProps<{
headers: MenuItem[]
navHeight: number
}>()

const { theme } = useData()
const open = ref(false)
const vh = ref(0)
const main = ref<HTMLDivElement>()
const items = ref<HTMLDivElement>()

onClickOutside(main, () => {
open.value = false
const open = ref(false)

const { theme } = useData()
const { activate, deactivate } = useFocusTrap(items, {
immediate: true,
allowOutsideClick: true,
clickOutsideDeactivates: true,
escapeDeactivates: true,
delayInitialFocus: false,
onDeactivate: () => {
open.value = false
},
})

const vh = ref(0)

onKeyStroke('Escape', () => {
userquin marked this conversation as resolved.
Show resolved Hide resolved
open.value = false
deactivate()
})

onContentUpdated(() => {
open.value = false
})
onContentUpdated(deactivate)

function toggle() {
open.value = !open.value
if (open.value) {
deactivate()
} else {
open.value = true
nextTick(() => activate())
}
vh.value = window.innerHeight + Math.min(window.scrollY - props.navHeight, 0)
}

Expand All @@ -41,14 +54,12 @@ function onItemClick(e: Event) {
if (items.value) {
items.value.style.transition = 'none'
}
nextTick(() => {
open.value = false
})
nextTick(() => deactivate())
}
}

function scrollToTop() {
open.value = false
deactivate()
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' })
}
</script>
Expand Down Expand Up @@ -155,11 +166,12 @@ function scrollToTop() {

.header {
background-color: var(--vp-c-bg-soft);
padding: 2px 16px;
}

.top-link {
display: block;
padding: 0 16px;
padding: 0;
line-height: 48px;
brc-dd marked this conversation as resolved.
Show resolved Hide resolved
font-size: 14px;
font-weight: 500;
Expand Down
18 changes: 10 additions & 8 deletions src/client/theme-default/components/VPSkipLink.vue
Expand Up @@ -27,14 +27,16 @@ function focusOnTargetAnchor({ target }: Event) {
</script>

<template>
<span ref="backToTop" tabindex="-1" />
<a
href="#VPContent"
class="VPSkipLink visually-hidden"
@click="focusOnTargetAnchor"
>
Skip to content
</a>
<div>
<span ref="backToTop" tabindex="-1" />
<a
href="#VPContent"
class="VPSkipLink visually-hidden"
@click="focusOnTargetAnchor"
>
Skip to content
</a>
</div>
</template>

<style scoped>
Expand Down
13 changes: 7 additions & 6 deletions src/client/theme-default/composables/nav.ts
@@ -1,16 +1,17 @@
import { ref, watch } from 'vue'
import { useRoute } from 'vitepress'
import { computed, watch } from 'vue'
import { useInert, useRoute } from 'vitepress'

export function useNav() {
const isScreenOpen = ref(false)
const inert = useInert()!
const isScreenOpen = computed(() => inert.isScreenOpen)

function openScreen() {
isScreenOpen.value = true
inert.isScreenOpen = true
window.addEventListener('resize', closeScreenOnTabletWindow)
}

function closeScreen() {
isScreenOpen.value = false
inert.isScreenOpen = false
window.removeEventListener('resize', closeScreenOnTabletWindow)
}

Expand All @@ -19,7 +20,7 @@ export function useNav() {
}

/**
* Close screen when the user resizes the window wider than tablet size.
* Close the screen when the user resizes the window wider than tablet size.
*/
function closeScreenOnTabletWindow() {
window.outerWidth >= 768 && closeScreen()
Expand Down
8 changes: 5 additions & 3 deletions src/client/theme-default/composables/sidebar.ts
Expand Up @@ -18,6 +18,7 @@ import {
getSidebarGroups
} from '../support/sidebar'
import { useData } from './data'
import { useInert } from 'vitepress'

export interface SidebarControl {
collapsed: Ref<boolean>
Expand All @@ -33,7 +34,8 @@ export function useSidebar() {
const { frontmatter, page, theme } = useData()
const is960 = useMediaQuery('(min-width: 960px)')

const isOpen = ref(false)
const inert = useInert()!
const isOpen = computed(() => inert.isSidebarOpen)

const _sidebar = computed(() => {
const sidebarConfig = theme.value.sidebar
Expand Down Expand Up @@ -77,11 +79,11 @@ export function useSidebar() {
})

function open() {
isOpen.value = true
inert.isSidebarOpen = true
}

function close() {
isOpen.value = false
inert.isSidebarOpen = false
}

function toggle() {
Expand Down
6 changes: 5 additions & 1 deletion src/node/plugin.ts
Expand Up @@ -157,7 +157,11 @@ export async function createVitePressPlugin(
},
optimizeDeps: {
// force include vue to avoid duplicated copies when linked + optimized
include: ['vue', 'vitepress > @vue/devtools-api'],
include: [
'vue',
'vitepress > @vue/devtools-api',
'vitepress > @vueuse/integrations/useFocusTrap'
],
exclude: ['@docsearch/js', 'vitepress']
},
server: {
Expand Down
1 change: 0 additions & 1 deletion src/node/plugins/localSearchPlugin.ts
Expand Up @@ -168,7 +168,6 @@ export async function localSearchPlugin(
config: () => ({
optimizeDeps: {
include: [
'vitepress > @vueuse/integrations/useFocusTrap',
'vitepress > mark.js/src/vanilla.js',
'vitepress > minisearch'
]
Expand Down