diff --git a/__tests__/e2e/.vitepress/config.ts b/__tests__/e2e/.vitepress/config.ts index 2525f02ba..a6f73cabc 100644 --- a/__tests__/e2e/.vitepress/config.ts +++ b/__tests__/e2e/.vitepress/config.ts @@ -197,6 +197,7 @@ const sidebar: DefaultTheme.Config['sidebar'] = { export default defineConfig({ title: 'Example', description: 'An example app using VitePress.', + dir: false, srcExclude: ['**/parts/**'], markdown: { image: { lazyLoad: true } diff --git a/__tests__/e2e/navigation.test.ts b/__tests__/e2e/navigation.test.ts index 3d475f851..e26a8e66a 100644 --- a/__tests__/e2e/navigation.test.ts +++ b/__tests__/e2e/navigation.test.ts @@ -42,6 +42,25 @@ describe('navigation accessibility', () => { expect(await sectionLink.getAttribute('aria-current')).toBeNull() }) + test('preserves a runtime-managed document direction', async () => { + await goto('/') + + const html = page.locator('html') + expect(await html.getAttribute('dir')).toBeNull() + + await page.evaluate(() => { + document.documentElement.dir = 'rtl' + }) + await page + .locator('.VPNavBarMenuLink[href="/markdown-extensions/"]') + .click() + await page.waitForFunction(() => + location.pathname.startsWith('/markdown-extensions') + ) + + expect(await html.getAttribute('dir')).toBe('rtl') + }) + test('marks only exact sidebar links, including fragments', async () => { const overview = '.VPSidebarItem .link[href="/sidebar-hash/"]' const sectionOne = '.VPSidebarItem .link[href="/sidebar-hash/#section-one"]' diff --git a/__tests__/unit/node/config.test.ts b/__tests__/unit/node/config.test.ts index de1eb2f88..a00dcf038 100644 --- a/__tests__/unit/node/config.test.ts +++ b/__tests__/unit/node/config.test.ts @@ -3,10 +3,16 @@ import { mergeConfig, normalizeAssetsBase, normalizeSiteBase, + resolveSiteData, type UserConfig } from 'node/config' describe('node/config', () => { + test('preserves disabled automatic direction handling', async () => { + expect((await resolveSiteData('', { dir: false })).dir).toBe(false) + expect((await resolveSiteData('', {})).dir).toBe('ltr') + }) + test('merges markdown hooks from extended configs', async () => { const calls: string[] = [] const md = {} as MarkdownItAsync diff --git a/docs/en/guide/i18n.md b/docs/en/guide/i18n.md index ff0bcff08..f688a62bb 100644 --- a/docs/en/guide/i18n.md +++ b/docs/en/guide/i18n.md @@ -44,7 +44,7 @@ The following properties can be overridden for each locale (including root): ```ts interface LocaleSpecificConfig { lang?: string - dir?: string + dir?: 'ltr' | 'rtl' | 'auto' | false title?: string titleTemplate?: string | boolean description?: string @@ -53,6 +53,8 @@ interface LocaleSpecificConfig { } ``` +Set `dir: false` at the site or locale level when application code manages the `` attribute at runtime. VitePress will omit the attribute from generated HTML and will not overwrite it during client-side navigation. + Refer [`DefaultTheme.Config`](https://github.com/vuejs/vitepress/blob/main/types/default-theme.d.ts) interface for details on customizing the placeholder texts of the default theme. Don't override `themeConfig.algolia` or `themeConfig.carbonAds` at locale-level. Refer [Algolia docs](../reference/default-theme-search#algolia-search-i18n) for using multilingual search. **Pro tip:** Config file can be stored at `docs/.vitepress/config/index.ts` too. It might help you organize stuff by creating a configuration file per locale and then merge and export them from `index.ts`. diff --git a/docs/en/reference/runtime-api.md b/docs/en/reference/runtime-api.md index bdaa49773..151ca19f3 100644 --- a/docs/en/reference/runtime-api.md +++ b/docs/en/reference/runtime-api.md @@ -40,7 +40,7 @@ interface VitePressData { description: Ref lang: Ref isDark: Ref - dir: Ref<'ltr' | 'rtl' | 'auto'> + dir: Ref<'ltr' | 'rtl' | 'auto' | false> localeIndex: Ref /** * Current location hash diff --git a/docs/en/reference/site-config.md b/docs/en/reference/site-config.md index 762784cd5..063baaccc 100644 --- a/docs/en/reference/site-config.md +++ b/docs/en/reference/site-config.md @@ -373,7 +373,7 @@ export default { - Default: `ltr` - Can be overridden at the [directory level](#directory-level-overrides) -The text direction of the site. This will render as a `` tag in the page HTML, and the default theme mirrors its layout for right-to-left languages. It can also be overridden per page via [frontmatter](./frontmatter-config#dir). See [RTL Support](../guide/i18n#rtl-support). +The text direction of the site. This will render as a `` tag in the page HTML, and the default theme mirrors its layout for right-to-left languages. It can also be overridden per page via [frontmatter](./frontmatter-config#dir). Set it to `false` to let application code manage the attribute. See [RTL Support](../guide/i18n#rtl-support). ```ts export default { diff --git a/src/client/app/data.ts b/src/client/app/data.ts index 65f17f8af..673abbb38 100644 --- a/src/client/app/data.ts +++ b/src/client/app/data.ts @@ -58,7 +58,10 @@ export function initData(route: Route): VitePressData { frontmatter: computed(() => route.data.frontmatter), params: computed(() => route.data.params), lang: computed(() => site.value.lang), - dir: computed(() => route.data.frontmatter.dir || site.value.dir), + dir: computed(() => { + const dir = route.data.frontmatter.dir + return dir === false ? false : dir || site.value.dir + }), localeIndex: computed(() => site.value.localeIndex || 'root'), title: computed(() => createTitle(site.value, route.data)), description: computed( diff --git a/src/client/app/index.ts b/src/client/app/index.ts index 76b156a81..401a3ba8a 100644 --- a/src/client/app/index.ts +++ b/src/client/app/index.ts @@ -49,7 +49,9 @@ const VitePressApp = defineComponent({ if (inBrowser) { watchEffect(() => { document.documentElement.lang = lang.value - document.documentElement.dir = dir.value + if (dir.value !== false) { + document.documentElement.dir = dir.value + } }) } diff --git a/src/client/theme-default/composables/langs.ts b/src/client/theme-default/composables/langs.ts index 0fb6e06b8..ba5edf42c 100644 --- a/src/client/theme-default/composables/langs.ts +++ b/src/client/theme-default/composables/langs.ts @@ -41,7 +41,7 @@ export function useLangs({ linkToCorrespondingPage }), lang: value.lang, - dir: value.dir + dir: value.dir === false ? undefined : value.dir } ) ) diff --git a/src/node/build/render.ts b/src/node/build/render.ts index 538e0238d..89e940f05 100644 --- a/src/node/build/render.ts +++ b/src/node/build/render.ts @@ -100,7 +100,9 @@ export async function renderPage( const title = createTitle(siteData, pageData) const description = pageData.description || siteData.description - const dir = pageData.frontmatter.dir || siteData.dir || 'ltr' + const frontmatterDir = pageData.frontmatter.dir + const dir = frontmatterDir === false ? false : frontmatterDir || siteData.dir + const dirAttr = dir === false ? '' : ` dir="${dir || 'ltr'}"` const isDefault404 = page === '404.md' && !hasCustom404 let preloadLinks: string[] = [] @@ -199,7 +201,7 @@ export async function renderPage( } const html = ` - + ${ diff --git a/src/node/config.ts b/src/node/config.ts index 78d94c11d..7ca584fa4 100644 --- a/src/node/config.ts +++ b/src/node/config.ts @@ -420,7 +420,7 @@ export async function resolveSiteData( return { lang: userConfig.lang || 'en-US', - dir: userConfig.dir || 'ltr', + dir: userConfig.dir === false ? false : userConfig.dir || 'ltr', title: userConfig.title || 'VitePress', titleTemplate: userConfig.titleTemplate, description: userConfig.description || 'A VitePress site', diff --git a/src/node/plugin.ts b/src/node/plugin.ts index 9c5307764..864e27535 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -334,9 +334,10 @@ export async function createVitePressPlugin( } catch { // malformed percent-encoding: keep the site-level values } + const dirAttr = dir === false ? '' : ` dir="${dir}"` let html = `\ - + diff --git a/types/shared.d.ts b/types/shared.d.ts index 3d50e11f2..e0fb98561 100644 --- a/types/shared.d.ts +++ b/types/shared.d.ts @@ -184,10 +184,11 @@ export interface SiteData { */ lang: string /** - * The text direction (`dir` attribute) of the site. + * The text direction (`dir` attribute) of the site. Set to `false` to let + * application code manage the attribute. * @default 'ltr' */ - dir: 'ltr' | 'rtl' | 'auto' + dir: 'ltr' | 'rtl' | 'auto' | false /** * The title of the site. * @default 'VitePress' @@ -295,7 +296,7 @@ export interface VitePressData { /** * The text direction of the active locale. */ - dir: Ref<'ltr' | 'rtl' | 'auto'> + dir: Ref<'ltr' | 'rtl' | 'auto' | false> /** * The key of the active locale. */ @@ -379,9 +380,10 @@ export interface LocaleSpecificConfig { */ lang?: string /** - * The text direction of the locale. + * The text direction of the locale. Set to `false` to let application code + * manage the `dir` attribute. */ - dir?: 'ltr' | 'rtl' | 'auto' + dir?: 'ltr' | 'rtl' | 'auto' | false /** * The title of the site in the locale. */