From c9e336d0686f3f423218dafccd083f0925954fc4 Mon Sep 17 00:00:00 2001 From: Luffy Date: Fri, 21 Aug 2026 14:16:14 +0800 Subject: [PATCH 1/4] feat(sidebar): normalize links and add root chevron control --- docs/themes.md | 12 +++ src/core/render/index.js | 33 ++++++- src/themes/shared/_classes.css | 9 ++ src/themes/shared/_sidebar.css | 2 +- test/e2e/sidebar.test.js | 153 ++++++++++++++++++++++++++++++++- 5 files changed, 203 insertions(+), 6 deletions(-) diff --git a/docs/themes.md b/docs/themes.md index 6eb436bfd1..d0187e31ee 100644 --- a/docs/themes.md +++ b/docs/themes.md @@ -117,6 +117,10 @@ Display expand/collapse icons on page links in the sidebar. +
+ ```html @@ -128,6 +132,14 @@ Display expand/collapse icons on page links in the sidebar. ``` +To hide chevrons on all root-level page links and group titles while retaining +chevrons on nested page links, add the `sidebar-chevron-root-hidden` class: + + +```html + +``` + To prevent chevrons from displaying for specific page links, add a `no-chevron` class as follows: ```md diff --git a/src/core/render/index.js b/src/core/render/index.js index ca09150b13..805747dccc 100644 --- a/src/core/render/index.js +++ b/src/core/render/index.js @@ -35,6 +35,32 @@ export function Render(Base) { }); } + /** + * Normalize links in loose Markdown lists from `
  • ` to + * `

  • ` so sidebar behavior and styling do not depend on list + * tightness. + * + * @param {Element} sidebarNavEl + */ + #normalizeSidebarPageLinks(sidebarNavEl) { + dom.findAll(sidebarNavEl, 'li > p').forEach(paragraph => { + const link = paragraph.firstElementChild; + const onlyContainsLink = [...paragraph.childNodes].every( + node => + node === link || (node.nodeType === 3 && !node.textContent?.trim()), + ); + + if ( + !paragraph.attributes.length && + paragraph.children.length === 1 && + link?.tagName === 'A' && + onlyContainsLink + ) { + paragraph.replaceWith(link); + } + }); + } + #executeScript() { const script = dom .findAll('.markdown-section>script') @@ -329,6 +355,7 @@ export function Render(Base) { ); dom.setHTML('.sidebar-nav', this.compiler.sidebar(text, maxLevel)); + this.#normalizeSidebarPageLinks(sidebarNavEl); sidebarToggleEl.setAttribute('aria-expanded', String(!isMobile())); @@ -358,18 +385,18 @@ export function Render(Base) { // Mark page links and groups const pageLinks = dom.findAll( sidebarNavEl, - 'a:is(li > a, li > p > a):not(.section-link, [target="_blank"])', + 'li > a:not(.section-link, [target="_blank"])', ); const pageLinkGroups = dom // NOTE: Using filter() method as a replacement for :has() selector. It - // would be preferable to use only 'li:not(:has(> a, > p > a))' selector + // would be preferable to use only 'li:not(:has(> a))' selector // but the :has() selector is not supported by our Jest test environment // See: https://github.com/jsdom/jsdom/issues/3506#issuecomment-1769782333 .findAll(sidebarEl, 'li') .filter( elm => elm.querySelector(':scope > ul') && - !elm.querySelectorAll(':scope > a, :scope > p > a').length, + !elm.querySelector(':scope > a'), ); pageLinks.forEach(elm => { diff --git a/src/themes/shared/_classes.css b/src/themes/shared/_classes.css index aab7f2c505..778197e384 100644 --- a/src/themes/shared/_classes.css +++ b/src/themes/shared/_classes.css @@ -94,6 +94,15 @@ body[class*='sidebar-chevron'] { } } +body.sidebar-chevron-root-hidden { + .sidebar-nav > ul > li { + > a.page-link, + > p.group-title[role='button'][aria-expanded] { + background: none; + } + } +} + /* Left */ /* -------------------------------------------------------------------------- */ :root:has(body.sidebar-chevron-left) { diff --git a/src/themes/shared/_sidebar.css b/src/themes/shared/_sidebar.css index 34cc7463d1..0ccdc5e2aa 100644 --- a/src/themes/shared/_sidebar.css +++ b/src/themes/shared/_sidebar.css @@ -99,7 +99,7 @@ } &.collapse { - > :not(a, p:has(> a.page-link)):not(.group-title) { + > :not(a, .group-title) { display: none; } } diff --git a/test/e2e/sidebar.test.js b/test/e2e/sidebar.test.js index 4ed35dc798..b797499400 100644 --- a/test/e2e/sidebar.test.js +++ b/test/e2e/sidebar.test.js @@ -236,6 +236,155 @@ test.describe('Sidebar Tests', () => { expect(collapsedBackground).not.toMatch(/rgb\(4,\s*5,\s*6\)/); }); + test('normalizes loose-list page links and shows expanded chevrons', async ({ + page, + }) => { + await docsifyInit({ + config: { + subMaxLevel: 2, + }, + styleURLs: ['/dist/themes/core.css'], + style: ` + :root:has(body[class*='sidebar-chevron']) { + --sidebar-chevron-collapsed-color: rgb(1, 2, 3); + --sidebar-chevron-expanded-color: rgb(4, 5, 6); + --sidebar-link-color-active: rgb(7, 8, 9); + } + `, + html: ` + + + + +
    + + + `, + markdown: { + homepage: '# Home', + sidebar: ` + * [Test](test.md) + + [Quick start](quickstart.md) + - [Adding pages](adding-pages.md) + + - Getting started + + - [Cover page](cover.md) + `, + }, + routes: { + '/test.md': '# Test', + '/quickstart.md': '# Quick start\n\n## Installation', + '/adding-pages.md': '# Adding pages\n\n## Sidebar', + '/cover.md': '# Cover page', + }, + }); + + const quickStartLink = page.locator('a[href="#/quickstart"]'); + const addingPagesLink = page.locator('a[href="#/adding-pages"]'); + const quickStartItem = page.locator( + '.sidebar-nav li:has(> a[href="#/quickstart"])', + ); + const addingPagesItem = page.locator( + '.sidebar-nav li:has(> a[href="#/adding-pages"])', + ); + + await expect(page.locator('.sidebar-nav li > p > a')).toHaveCount(0); + + await quickStartLink.click(); + await expect( + quickStartItem.locator(':scope > .app-sub-sidebar'), + ).toBeVisible(); + const quickStartBackground = await quickStartLink.evaluate( + element => getComputedStyle(element).backgroundImage, + ); + + await addingPagesLink.click(); + await expect( + addingPagesItem.locator(':scope > .app-sub-sidebar'), + ).toBeVisible(); + const addingPagesBackground = await addingPagesLink.evaluate( + element => getComputedStyle(element).backgroundImage, + ); + + expect(addingPagesBackground).toBe(quickStartBackground); + expect(addingPagesBackground).toMatch(/rgb\(4,\s*5,\s*6\)/); + await expect(addingPagesLink).toHaveCSS('color', 'rgb(7, 8, 9)'); + + await addingPagesLink.click(); + await expect(addingPagesItem).toHaveClass(/collapse/); + const collapsedBackground = await addingPagesLink.evaluate( + element => getComputedStyle(element).backgroundImage, + ); + + expect(collapsedBackground).not.toBe(addingPagesBackground); + }); + + test('hides root chevrons when configured by body class', async ({ + page, + }) => { + await docsifyInit({ + config: { + subMaxLevel: 2, + }, + styleURLs: ['/dist/themes/core.css'], + html: ` + + + + +
    + + + `, + markdown: { + homepage: '# Home', + sidebar: ` + + [Direct root page](direct.md) + - [Loose root page](loose.md) + + - Getting started + + - [Nested page](nested.md) + `, + }, + routes: { + '/direct.md': '# Direct root page', + '/loose.md': '# Loose root page\n\n## Child heading', + '/nested.md': '# Nested page', + }, + }); + + const directRootLink = page.locator('a[href="#/direct"]'); + const looseRootLink = page.locator('a[href="#/loose"]'); + const nestedLink = page.locator('a[href="#/nested"]'); + const groupTitle = page.locator('.group-title[role="button"]'); + const looseRootItem = page.locator( + '.sidebar-nav li:has(> a[href="#/loose"])', + ); + + for (const rootLink of [directRootLink, looseRootLink]) { + await expect(rootLink).toHaveCSS('background-image', 'none'); + } + + await expect(nestedLink).not.toHaveCSS('background-image', 'none'); + await expect(groupTitle).toHaveCSS('background-image', 'none'); + + await groupTitle.click(); + await expect(groupTitle).toHaveAttribute('aria-expanded', 'false'); + await expect(groupTitle).toHaveCSS('background-image', 'none'); + + await looseRootLink.click(); + await expect( + looseRootItem.locator(':scope > .app-sub-sidebar'), + ).toBeVisible(); + await expect(looseRootLink).toHaveCSS('background-image', 'none'); + + await looseRootLink.click(); + await expect(looseRootItem).toHaveClass(/collapse/); + await expect(looseRootLink).toHaveCSS('background-image', 'none'); + }); + test('keeps group border spacing when the last group collapses', async ({ page, }) => { @@ -288,7 +437,7 @@ test.describe('Sidebar Tests', () => { expect(spacing.titleToBorder).toBeGreaterThan(spacing.borderToAwesome); }); - test('keeps a loose-list page link visible when collapsed', async ({ + test('keeps a normalized loose-list page link visible when collapsed', async ({ page, }) => { await docsifyInit({ @@ -320,7 +469,7 @@ test.describe('Sidebar Tests', () => { await quickStartLink.click(); const quickStartItem = page.locator( - '.sidebar-nav li:has(> p > a[href="#/quickstart"])', + '.sidebar-nav li:has(> a[href="#/quickstart"])', ); const subSidebar = quickStartItem.locator(':scope > .app-sub-sidebar'); await expect(subSidebar).toBeVisible(); From 73b03c77edad00c6a30bee12b6512ae442e4e970 Mon Sep 17 00:00:00 2001 From: Luffy Date: Sat, 22 Aug 2026 22:16:47 +0800 Subject: [PATCH 2/4] feat(sidebar): replace group-title with group-toggle for improved accessibility fix https://github.com/docsifyjs/docsify/pull/2784#issuecomment-5371562687 --- src/core/event/index.js | 4 ++-- src/core/render/index.js | 9 +++++++-- src/themes/shared/_classes.css | 12 ++++++------ src/themes/shared/_sidebar.css | 16 ++++++++-------- test/e2e/sidebar.test.js | 31 +++++++++++++++++++++++++------ 5 files changed, 48 insertions(+), 24 deletions(-) diff --git a/src/core/event/index.js b/src/core/event/index.js index cc9f5e8216..d1f53b3bf7 100644 --- a/src/core/event/index.js +++ b/src/core/event/index.js @@ -254,7 +254,7 @@ export function Events(Base) { dom.on(sidebarElm, 'click', (/** @type {MouseEvent} */ { target }) => { const groupTitle = /** @type {HTMLElement | null} */ ( /** @type {HTMLElement} */ (target).closest( - '.group-title[role="button"]', + '.group-toggle[role="button"]', ) ); @@ -277,7 +277,7 @@ export function Events(Base) { dom.on(sidebarElm, 'keydown', (/** @type {KeyboardEvent} */ event) => { const groupTitle = /** @type {HTMLElement | null} */ ( /** @type {HTMLElement} */ (event.target).closest( - '.group-title[role="button"]', + '.group-toggle[role="button"]', ) ); diff --git a/src/core/render/index.js b/src/core/render/index.js index 805747dccc..10ff749771 100644 --- a/src/core/render/index.js +++ b/src/core/render/index.js @@ -346,7 +346,7 @@ export function Render(Base) { dom .findAll( sidebarNavEl, - 'li.group > .group-title[role="button"][data-group-id]', + 'li.group > .group-toggle[role="button"][data-group-id]', ) .map(elm => [ elm.getAttribute('data-group-id'), @@ -409,6 +409,10 @@ export function Render(Base) { let groupTitle = [...elm.children].find( child => child.tagName === 'P' && !child.querySelector('a'), ); + // Preserve the original styling behavior: only text-only paragraphs + // produced by Markdown receive the group-title class. + const styledGroupTitle = + groupTitle && !groupTitle.children.length ? groupTitle : null; if (!groupTitle) { const sublist = [...elm.children].find( @@ -432,7 +436,7 @@ export function Render(Base) { } } - groupTitle?.classList.add('group-title'); + styledGroupTitle?.classList.add('group-title'); const rootList = elm.parentElement; @@ -442,6 +446,7 @@ export function Render(Base) { sidebarGroupStates.get(groupId) ?? collapseSidebarGroups; elm.classList.toggle('collapse', isCollapsed); + groupTitle.classList.add('group-toggle'); groupTitle.setAttribute('data-group-id', groupId); groupTitle.setAttribute('role', 'button'); groupTitle.setAttribute('tabindex', '0'); diff --git a/src/themes/shared/_classes.css b/src/themes/shared/_classes.css index 778197e384..74776f05ce 100644 --- a/src/themes/shared/_classes.css +++ b/src/themes/shared/_classes.css @@ -81,11 +81,11 @@ } body[class*='sidebar-chevron'] { - .sidebar-nav :is(a.page-link, p.group-title[role='button']).no-chevron { + .sidebar-nav :is(a.page-link, p.group-toggle).no-chevron { background: none; } - .sidebar-nav p.group-title[role='button'] { + .sidebar-nav p.group-toggle { background: var(--sidebar-pagelink-bg); &[aria-expanded='true'] { @@ -97,7 +97,7 @@ body[class*='sidebar-chevron'] { body.sidebar-chevron-root-hidden { .sidebar-nav > ul > li { > a.page-link, - > p.group-title[role='button'][aria-expanded] { + > p.group-toggle[aria-expanded] { background: none; } } @@ -114,7 +114,7 @@ body.sidebar-chevron-left { --_inset: 18px; li { - :is(a.page-link, p.group-title[role='button']) { + :is(a.page-link, p.group-toggle) { padding-left: var(--_inset); } @@ -137,12 +137,12 @@ body.sidebar-chevron-left { body.sidebar-chevron-right { .sidebar-nav { - p.group-title[role='button'] { + p.group-toggle { margin-right: 0; } li { - :is(a, p.group-title[role='button']) { + :is(a, p.group-toggle) { padding-right: calc(var(--_sidebar-inset) + 15px); } } diff --git a/src/themes/shared/_sidebar.css b/src/themes/shared/_sidebar.css index 0ccdc5e2aa..f70f3d31d5 100644 --- a/src/themes/shared/_sidebar.css +++ b/src/themes/shared/_sidebar.css @@ -67,15 +67,15 @@ color: var(--sidebar-group-title-color); font-size: var(--sidebar-group-title-font-size); font-weight: var(--sidebar-group-title-font-weight); + } - &[role='button'] { - cursor: pointer; - user-select: none; + &.group-toggle { + cursor: pointer; + user-select: none; - &:focus-visible { - outline: 2px solid currentColor; - outline-offset: 2px; - } + &:focus-visible { + outline: 2px solid currentColor; + outline-offset: 2px; } } } @@ -99,7 +99,7 @@ } &.collapse { - > :not(a, .group-title) { + > :not(a, .group-toggle) { display: none; } } diff --git a/test/e2e/sidebar.test.js b/test/e2e/sidebar.test.js index b797499400..f34dbe639e 100644 --- a/test/e2e/sidebar.test.js +++ b/test/e2e/sidebar.test.js @@ -92,7 +92,7 @@ test.describe('Sidebar Tests', () => { }); const group = page.locator('.sidebar-nav > ul > li').first(); - const groupTitle = group.locator(':scope > p.group-title'); + const groupTitle = group.locator(':scope > p.group-toggle'); const childLink = group.locator(':scope > ul > li > a'); await expect(groupTitle).toHaveAttribute('role', 'button'); @@ -158,7 +158,7 @@ test.describe('Sidebar Tests', () => { const groups = page.locator('.sidebar-nav > ul > li.group'); const firstGroup = groups.first(); - const firstGroupTitle = firstGroup.locator(':scope > .group-title'); + const firstGroupTitle = firstGroup.locator(':scope > .group-toggle'); const firstGroupLink = firstGroup.locator(':scope > ul > li > a'); const secondGroup = groups.nth(1); @@ -185,6 +185,7 @@ test.describe('Sidebar Tests', () => { :root:has(body[class*='sidebar-chevron']) { --sidebar-chevron-collapsed-color: rgb(1, 2, 3); --sidebar-chevron-expanded-color: rgb(4, 5, 6); + --sidebar-group-title-font-weight: 700; } `, html: ` @@ -201,15 +202,25 @@ test.describe('Sidebar Tests', () => { - Getting started - [Quick start](quickstart) - [Standalone](standalone) + + 1. Styled group + + - [Styled child](styled-child) `, }, routes: { '/quickstart.md': '# Quick start', + '/styled-child.md': '# Styled child', '/standalone.md': '# Standalone', }, }); - const groupTitle = page.locator('.group-title[role="button"]'); + const groupTitle = page + .locator('.group-toggle[role="button"]') + .filter({ hasText: 'Getting started' }); + const styledGroupTitle = page + .locator('.group-title.group-toggle') + .filter({ hasText: 'Styled group' }); const standaloneLink = page.locator('a[href="#/standalone"]'); const background = await groupTitle.evaluate( element => getComputedStyle(element).backgroundImage, @@ -218,10 +229,18 @@ test.describe('Sidebar Tests', () => { groupTitle.boundingBox(), standaloneLink.boundingBox(), ]); + const [groupTitleFontWeight, standaloneLinkFontWeight] = await Promise.all([ + groupTitle.evaluate(element => getComputedStyle(element).fontWeight), + standaloneLink.evaluate(element => getComputedStyle(element).fontWeight), + ]); expect(background).not.toBe('none'); expect(background).toMatch(/rgb\(1,\s*2,\s*3\)/); expect(background).not.toMatch(/rgb\(4,\s*5,\s*6\)/); + await expect(groupTitle).not.toHaveClass(/group-title/); + expect(groupTitleFontWeight).toBe(standaloneLinkFontWeight); + await expect(styledGroupTitle).not.toHaveCSS('background-image', 'none'); + await expect(styledGroupTitle).toHaveCSS('font-weight', '700'); expect(groupTitleBox?.x + groupTitleBox?.width).toBe( standaloneLinkBox?.x + standaloneLinkBox?.width, ); @@ -358,7 +377,7 @@ test.describe('Sidebar Tests', () => { const directRootLink = page.locator('a[href="#/direct"]'); const looseRootLink = page.locator('a[href="#/loose"]'); const nestedLink = page.locator('a[href="#/nested"]'); - const groupTitle = page.locator('.group-title[role="button"]'); + const groupTitle = page.locator('.group-toggle[role="button"]'); const looseRootItem = page.locator( '.sidebar-nav li:has(> a[href="#/loose"])', ); @@ -416,12 +435,12 @@ test.describe('Sidebar Tests', () => { const upgradingGroup = page.locator( '.sidebar-nav > ul:first-of-type > li:last-child', ); - const groupTitle = upgradingGroup.locator(':scope > .group-title'); + const groupTitle = upgradingGroup.locator(':scope > .group-toggle'); await groupTitle.click(); const spacing = await page.evaluate(() => { - const title = document.querySelector('.group-title'); + const title = document.querySelector('.group-toggle'); const group = title.closest('li'); const awesome = document.querySelector('a[href="#/awesome"]'); const titleBox = title.getBoundingClientRect(); From ead54831f69e886f550db66dfc939e7d9edbcbc3 Mon Sep 17 00:00:00 2001 From: Luffy Date: Mon, 24 Aug 2026 20:01:11 +0800 Subject: [PATCH 3/4] feat(sidebar): add collapsibleSidebarGroups configuration for expandable root groups --- docs/configuration.md | 22 +++++++++++-- docs/index.html | 1 + docs/themes.md | 4 +++ src/core/config.js | 1 + src/core/event/index.js | 20 ++++++------ src/core/render/index.js | 7 ++++- test/consume-types/example.js | 1 + test/e2e/sidebar.test.js | 58 ++++++++++++++++++++++++++++++++++- 8 files changed, 99 insertions(+), 15 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 5ad0b8958d..55b5caf67f 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -306,18 +306,34 @@ window.$docsify = { }; ``` +## collapsibleSidebarGroups + +- Type: `Boolean` +- Default: `false` + +Enables visitors to expand and collapse root sidebar groups by selecting their +titles or using the Enter and Space keys. Enabling a +sidebar chevron theme class also displays chevrons on these group titles. + +```js +window.$docsify = { + collapsibleSidebarGroups: true, +}; +``` + ## collapseSidebarGroups - Type: `Boolean` - Default: `false` -Initially collapses all root sidebar groups. Visitors can still expand and -collapse each group by selecting its title. Their choices are preserved while -navigating between pages. +Initially collapses all root sidebar groups when `collapsibleSidebarGroups` is +enabled. Visitors can still expand and collapse each group by selecting its +title. Their choices are preserved while navigating between pages. ```js window.$docsify = { collapseSidebarGroups: true, + collapsibleSidebarGroups: true, }; ``` diff --git a/docs/index.html b/docs/index.html index cb9ee980d5..c9300f0ad7 100644 --- a/docs/index.html +++ b/docs/index.html @@ -112,6 +112,7 @@ 'https://cdn.jsdelivr.net/gh/docsifyjs/docs-zh@main/$1', }, auto2top: true, + collapsibleSidebarGroups: true, coverpage: true, executeScript: true, // hideSidebar: true, diff --git a/docs/themes.md b/docs/themes.md index d0187e31ee..698ad90551 100644 --- a/docs/themes.md +++ b/docs/themes.md @@ -110,6 +110,10 @@ Display a loading animation while waiting for Docsify to initialize. Display expand/collapse icons on page links in the sidebar. +Root group titles display chevrons only when +[`collapsibleSidebarGroups`](configuration.md?id=collapsiblesidebargroups) is +enabled. + diff --git a/src/core/config.js b/src/core/config.js index 4df2e29f30..b1fe528c5c 100644 --- a/src/core/config.js +++ b/src/core/config.js @@ -12,6 +12,7 @@ const defaultDocsifyConfig = () => ({ basePath: '', catchPluginErrors: true, collapseSidebarGroups: false, + collapsibleSidebarGroups: false, cornerExternalLinkTarget: /** @type {'_blank' | '_self' | '_parent' | '_top' | '_unfencedTop'} */ ( '_blank' diff --git a/src/core/event/index.js b/src/core/event/index.js index d1f53b3bf7..8c70108eb3 100644 --- a/src/core/event/index.js +++ b/src/core/event/index.js @@ -252,14 +252,14 @@ export function Events(Base) { // Collapse toggle dom.on(sidebarElm, 'click', (/** @type {MouseEvent} */ { target }) => { - const groupTitle = /** @type {HTMLElement | null} */ ( + const groupToggle = /** @type {HTMLElement | null} */ ( /** @type {HTMLElement} */ (target).closest( '.group-toggle[role="button"]', ) ); - if (groupTitle) { - this.#toggleSidebarGroup(groupTitle); + if (groupToggle) { + this.#toggleSidebarGroup(groupToggle); return; } @@ -275,15 +275,15 @@ export function Events(Base) { }); dom.on(sidebarElm, 'keydown', (/** @type {KeyboardEvent} */ event) => { - const groupTitle = /** @type {HTMLElement | null} */ ( + const groupToggle = /** @type {HTMLElement | null} */ ( /** @type {HTMLElement} */ (event.target).closest( '.group-toggle[role="button"]', ) ); - if (groupTitle && (event.key === 'Enter' || event.key === ' ')) { + if (groupToggle && (event.key === 'Enter' || event.key === ' ')) { event.preventDefault(); - this.#toggleSidebarGroup(groupTitle); + this.#toggleSidebarGroup(groupToggle); } }); } @@ -291,12 +291,12 @@ export function Events(Base) { /** * Toggle a root sidebar group and keep its accessible state in sync. * - * @param {HTMLElement} groupTitle + * @param {HTMLElement} groupToggle * @void */ - #toggleSidebarGroup(groupTitle) { + #toggleSidebarGroup(groupToggle) { const group = /** @type {HTMLLIElement | null} */ ( - groupTitle.closest('li') + groupToggle.closest('li') ); if (!group) { @@ -304,7 +304,7 @@ export function Events(Base) { } const isCollapsed = group.classList.toggle('collapse'); - groupTitle.setAttribute('aria-expanded', String(!isCollapsed)); + groupToggle.setAttribute('aria-expanded', String(!isCollapsed)); } /** diff --git a/src/core/render/index.js b/src/core/render/index.js index 10ff749771..d33dd3e80a 100644 --- a/src/core/render/index.js +++ b/src/core/render/index.js @@ -322,6 +322,7 @@ export function Render(Base) { _renderSidebar(text) { const { collapseSidebarGroups, + collapsibleSidebarGroups, maxLevel, subMaxLevel, loadSidebar, @@ -440,7 +441,11 @@ export function Render(Base) { const rootList = elm.parentElement; - if (groupTitle && rootList?.parentElement === sidebarNavEl) { + if ( + collapsibleSidebarGroups && + groupTitle && + rootList?.parentElement === sidebarNavEl + ) { const groupId = `${[...sidebarNavEl.children].indexOf(rootList)}:${[...rootList.children].indexOf(elm)}`; const isCollapsed = sidebarGroupStates.get(groupId) ?? collapseSidebarGroups; diff --git a/test/consume-types/example.js b/test/consume-types/example.js index 494b916ef7..9fd0c27392 100644 --- a/test/consume-types/example.js +++ b/test/consume-types/example.js @@ -34,6 +34,7 @@ const d = new Docsify({ themeColor: 'deeppink', hideSidebar: false, collapseSidebarGroups: true, + collapsibleSidebarGroups: true, // @ts-expect-error invalid property to test that type checking works blahblah: 123, diff --git a/test/e2e/sidebar.test.js b/test/e2e/sidebar.test.js index f34dbe639e..8fd6581326 100644 --- a/test/e2e/sidebar.test.js +++ b/test/e2e/sidebar.test.js @@ -69,8 +69,56 @@ test.describe('Sidebar Tests', () => { expect(page.url()).toMatch(/\/test%3Efoo$/); }); - test('collapses root sidebar groups', async ({ page }) => { + test('does not collapse root sidebar groups by default', async ({ page }) => { await docsifyInit({ + config: { + collapseSidebarGroups: true, + }, + styleURLs: ['/dist/themes/core.css'], + html: ` + + + + +
    + + + `, + markdown: { + sidebar: ` + - Getting started + - [Quick start](quickstart) + `, + }, + routes: { + '/quickstart.md': '# Quick start', + }, + }); + + const group = page.locator('.sidebar-nav > ul > li.group'); + const groupTitle = group.locator(':scope > p'); + const childLink = group.locator(':scope > ul > li > a'); + + await expect(group).not.toHaveClass(/collapse/); + await expect(groupTitle).not.toHaveClass(/group-toggle/); + await expect(groupTitle).not.toHaveAttribute('role'); + await expect(groupTitle).not.toHaveAttribute('tabindex'); + await expect(groupTitle).not.toHaveAttribute('aria-expanded'); + await expect(groupTitle).not.toHaveAttribute('data-group-id'); + await expect(groupTitle).toHaveCSS('background-image', 'none'); + await expect(groupTitle).toHaveCSS('cursor', 'auto'); + await expect(childLink).toBeVisible(); + + await groupTitle.click(); + await expect(group).not.toHaveClass(/collapse/); + await expect(childLink).toBeVisible(); + }); + + test('collapses root sidebar groups when configured', async ({ page }) => { + await docsifyInit({ + config: { + collapsibleSidebarGroups: true, + }, styleURLs: ['/dist/themes/core.css'], markdown: { sidebar: ` @@ -138,6 +186,7 @@ test.describe('Sidebar Tests', () => { await docsifyInit({ config: { collapseSidebarGroups: true, + collapsibleSidebarGroups: true, }, styleURLs: ['/dist/themes/core.css'], markdown: { @@ -180,6 +229,9 @@ test.describe('Sidebar Tests', () => { test('supports chevrons on collapsible root groups', async ({ page }) => { await docsifyInit({ + config: { + collapsibleSidebarGroups: true, + }, styleURLs: ['/dist/themes/core.css'], style: ` :root:has(body[class*='sidebar-chevron']) { @@ -344,6 +396,7 @@ test.describe('Sidebar Tests', () => { }) => { await docsifyInit({ config: { + collapsibleSidebarGroups: true, subMaxLevel: 2, }, styleURLs: ['/dist/themes/core.css'], @@ -408,6 +461,9 @@ test.describe('Sidebar Tests', () => { page, }) => { await docsifyInit({ + config: { + collapsibleSidebarGroups: true, + }, styleURLs: ['/dist/themes/core.css'], html: ` From dac8a68e4c9b1c7a5c7bc9b6ee6316fe970b15bb Mon Sep 17 00:00:00 2001 From: Luffy Date: Mon, 24 Aug 2026 20:29:22 +0800 Subject: [PATCH 4/4] docs(configuration): sort sidebar options alphabetically --- docs/configuration.md | 62 +++++++++++++++++++++---------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 55b5caf67f..ad55c93cf7 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -109,6 +109,37 @@ window.$docsify = { Determines if Docsify should handle uncaught _synchronous_ plugin errors automatically. This can prevent plugin errors from affecting docsify's ability to properly render live site content. +## collapseSidebarGroups + +- Type: `Boolean` +- Default: `false` + +Initially collapses all root sidebar groups when `collapsibleSidebarGroups` is +enabled. Visitors can still expand and collapse each group by selecting its +title. Their choices are preserved while navigating between pages. + +```js +window.$docsify = { + collapseSidebarGroups: true, + collapsibleSidebarGroups: true, +}; +``` + +## collapsibleSidebarGroups + +- Type: `Boolean` +- Default: `false` + +Enables visitors to expand and collapse root sidebar groups by selecting their +titles or using the Enter and Space keys. Enabling a +sidebar chevron theme class also displays chevrons on these group titles. + +```js +window.$docsify = { + collapsibleSidebarGroups: true, +}; +``` + ## cornerExternalLinkTarget - Type: `String` @@ -306,37 +337,6 @@ window.$docsify = { }; ``` -## collapsibleSidebarGroups - -- Type: `Boolean` -- Default: `false` - -Enables visitors to expand and collapse root sidebar groups by selecting their -titles or using the Enter and Space keys. Enabling a -sidebar chevron theme class also displays chevrons on these group titles. - -```js -window.$docsify = { - collapsibleSidebarGroups: true, -}; -``` - -## collapseSidebarGroups - -- Type: `Boolean` -- Default: `false` - -Initially collapses all root sidebar groups when `collapsibleSidebarGroups` is -enabled. Visitors can still expand and collapse each group by selecting its -title. Their choices are preserved while navigating between pages. - -```js -window.$docsify = { - collapseSidebarGroups: true, - collapsibleSidebarGroups: true, -}; -``` - ## sidebarPosition - Type: `String`