diff --git a/__tests__/e2e/.vitepress/config.ts b/__tests__/e2e/.vitepress/config.ts index 2525f02b..a6f73cab 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 3d475f85..e26a8e66 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 de1eb2f8..a00dcf03 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 f7f285e6..5fee9fc7 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?: string | 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#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`. @@ -143,4 +145,4 @@ watchEffect(() => { ## RTL Support (Experimental) -For RTL support, specify `dir: 'rtl'` in config and use some RTLCSS PostCSS plugin like , or . You'll need to configure your PostCSS plugin to use `:where([dir="ltr"])` and `:where([dir="rtl"])` as prefixes to prevent CSS specificity issues. +For RTL support, specify `dir: 'rtl'` in config and use some RTLCSS PostCSS plugin like , or . You'll need to configure your PostCSS plugin to use `:where([dir="ltr"])` and `:where([dir="rtl"])` as prefixes to prevent CSS specificity issues. If users can switch direction at runtime, set `dir: false` and update `document.documentElement.dir` in your application code. diff --git a/docs/en/reference/runtime-api.md b/docs/en/reference/runtime-api.md index 86d9a20b..a9af45bb 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 + dir: Ref localeIndex: Ref /** * Current location hash diff --git a/src/client/app/data.ts b/src/client/app/data.ts index 65f17f8a..673abbb3 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 952e52f5..652eb46d 100644 --- a/src/client/app/index.ts +++ b/src/client/app/index.ts @@ -49,7 +49,9 @@ const VitePressApp = defineComponent({ onMounted(() => { 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 0fb6e06b..ba5edf42 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 0e8864d8..0a4ebcbd 100644 --- a/src/node/build/render.ts +++ b/src/node/build/render.ts @@ -99,7 +99,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 // the initial load only needs the lean page js — the static content is @@ -200,7 +202,7 @@ export async function renderPage( } const html = ` - + ${ diff --git a/src/node/config.ts b/src/node/config.ts index baea4a86..e939776b 100644 --- a/src/node/config.ts +++ b/src/node/config.ts @@ -409,7 +409,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/types/shared.d.ts b/types/shared.d.ts index a35663f9..2b05831d 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: string + dir: string | false /** * The title of the site. * @default 'VitePress' @@ -295,7 +296,7 @@ export interface VitePressData { /** * The text direction of the active locale. */ - dir: Ref + dir: Ref /** * 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?: string + dir?: string | false /** * The title of the site in the locale. */