mirror of
https://github.com/docsifyjs/docsify.git
synced 2026-10-17 04:31:18 +08:00
feat(sidebar): normalize page links and control root chevrons (#2790)
This commit is contained in:
+31
-15
@@ -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 <kbd>Enter</kbd> and <kbd>Space</kbd> keys. Enabling a
|
||||
sidebar chevron theme class also displays chevrons on these group titles.
|
||||
|
||||
```js
|
||||
window.$docsify = {
|
||||
collapsibleSidebarGroups: true,
|
||||
};
|
||||
```
|
||||
|
||||
## cornerExternalLinkTarget
|
||||
|
||||
- Type: `String`
|
||||
@@ -306,21 +337,6 @@ window.$docsify = {
|
||||
};
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
```js
|
||||
window.$docsify = {
|
||||
collapseSidebarGroups: true,
|
||||
};
|
||||
```
|
||||
|
||||
## sidebarPosition
|
||||
|
||||
- Type: `String`
|
||||
|
||||
@@ -112,6 +112,7 @@
|
||||
'https://cdn.jsdelivr.net/gh/docsifyjs/docs-zh@main/$1',
|
||||
},
|
||||
auto2top: true,
|
||||
collapsibleSidebarGroups: true,
|
||||
coverpage: true,
|
||||
executeScript: true,
|
||||
// hideSidebar: true,
|
||||
|
||||
@@ -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.
|
||||
|
||||
<label>
|
||||
<input class="toggle" type="checkbox" value="sidebar-chevron-right" data-class data-group="sidebar-chevron"> Preview <code>sidebar-chevron-right</code>
|
||||
</label>
|
||||
@@ -117,6 +121,10 @@ Display expand/collapse icons on page links in the sidebar.
|
||||
<label>
|
||||
<input class="toggle" type="checkbox" value="sidebar-chevron-left" data-class data-group="sidebar-chevron"> Preview <code>sidebar-chevron-left</code>
|
||||
</label>
|
||||
<br>
|
||||
<label>
|
||||
<input class="toggle" type="checkbox" value="sidebar-chevron-root-hidden" data-class> Hide root-level chevrons with <code>sidebar-chevron-root-hidden</code>
|
||||
</label>
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```html
|
||||
@@ -128,6 +136,14 @@ Display expand/collapse icons on page links in the sidebar.
|
||||
<body class="sidebar-chevron-left">
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```html
|
||||
<body class="sidebar-chevron-right sidebar-chevron-root-hidden">
|
||||
```
|
||||
|
||||
To prevent chevrons from displaying for specific page links, add a `no-chevron` class as follows:
|
||||
|
||||
```md
|
||||
|
||||
@@ -12,6 +12,7 @@ const defaultDocsifyConfig = () => ({
|
||||
basePath: '',
|
||||
catchPluginErrors: true,
|
||||
collapseSidebarGroups: false,
|
||||
collapsibleSidebarGroups: false,
|
||||
cornerExternalLinkTarget:
|
||||
/** @type {'_blank' | '_self' | '_parent' | '_top' | '_unfencedTop'} */ (
|
||||
'_blank'
|
||||
|
||||
+12
-12
@@ -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-title[role="button"]',
|
||||
'.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-title[role="button"]',
|
||||
'.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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -35,6 +35,32 @@ export function Render(Base) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize links in loose Markdown lists from `<li><p><a>` to
|
||||
* `<li><a>` 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')
|
||||
@@ -296,6 +322,7 @@ export function Render(Base) {
|
||||
_renderSidebar(text) {
|
||||
const {
|
||||
collapseSidebarGroups,
|
||||
collapsibleSidebarGroups,
|
||||
maxLevel,
|
||||
subMaxLevel,
|
||||
loadSidebar,
|
||||
@@ -320,7 +347,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'),
|
||||
@@ -329,6 +356,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 +386,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 => {
|
||||
@@ -382,6 +410,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(
|
||||
@@ -405,16 +437,21 @@ export function Render(Base) {
|
||||
}
|
||||
}
|
||||
|
||||
groupTitle?.classList.add('group-title');
|
||||
styledGroupTitle?.classList.add('group-title');
|
||||
|
||||
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;
|
||||
|
||||
elm.classList.toggle('collapse', isCollapsed);
|
||||
groupTitle.classList.add('group-toggle');
|
||||
groupTitle.setAttribute('data-group-id', groupId);
|
||||
groupTitle.setAttribute('role', 'button');
|
||||
groupTitle.setAttribute('tabindex', '0');
|
||||
|
||||
@@ -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'] {
|
||||
@@ -94,6 +94,15 @@ body[class*='sidebar-chevron'] {
|
||||
}
|
||||
}
|
||||
|
||||
body.sidebar-chevron-root-hidden {
|
||||
.sidebar-nav > ul > li {
|
||||
> a.page-link,
|
||||
> p.group-toggle[aria-expanded] {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Left */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
:root:has(body.sidebar-chevron-left) {
|
||||
@@ -105,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);
|
||||
}
|
||||
|
||||
@@ -128,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, p:has(> a.page-link)):not(.group-title) {
|
||||
> :not(a, .group-toggle) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -184,8 +184,7 @@
|
||||
|
||||
body.sticky & {
|
||||
position: fixed;
|
||||
overflow-y: auto;
|
||||
scrollbar-gutter: stable;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
+315
-8
@@ -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: `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><meta charset="UTF-8" /></head>
|
||||
<body class="sidebar-chevron-right">
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</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: `
|
||||
@@ -92,7 +140,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');
|
||||
@@ -132,12 +180,96 @@ test.describe('Sidebar Tests', () => {
|
||||
await expect(group).not.toHaveClass(/collapse/);
|
||||
});
|
||||
|
||||
for (const viewport of [
|
||||
{
|
||||
name: 'desktop',
|
||||
width: 1280,
|
||||
height: 720,
|
||||
prepareSidebar: async () => {},
|
||||
},
|
||||
{
|
||||
name: 'mobile',
|
||||
width: 390,
|
||||
height: 500,
|
||||
prepareSidebar: async (page, sidebar) => {
|
||||
await page.locator('.sidebar-toggle-button').click();
|
||||
await sidebar.evaluate(element =>
|
||||
Promise.all(
|
||||
element.getAnimations().map(animation => animation.finished),
|
||||
),
|
||||
);
|
||||
},
|
||||
},
|
||||
]) {
|
||||
test(`keeps sidebar content aligned when a root group removes the scrollbar on ${viewport.name}`, async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.setViewportSize(viewport);
|
||||
|
||||
const childLinks = Array.from(
|
||||
{ length: 40 },
|
||||
(_, index) => ` - [Page ${index + 1}](page-${index + 1})`,
|
||||
).join('\n');
|
||||
|
||||
await docsifyInit({
|
||||
config: {
|
||||
collapsibleSidebarGroups: true,
|
||||
},
|
||||
styleURLs: ['/dist/themes/core.css'],
|
||||
markdown: {
|
||||
homepage: '# Home',
|
||||
sidebar: `- Root\n${childLinks}`,
|
||||
},
|
||||
});
|
||||
|
||||
const sidebar = page.locator('.sidebar');
|
||||
const group = page.locator('.sidebar-nav > ul > li.group');
|
||||
const groupTitle = group.locator(':scope > .group-toggle');
|
||||
|
||||
await viewport.prepareSidebar(page, sidebar);
|
||||
|
||||
await expect(sidebar).toHaveCSS('overflow-y', 'scroll');
|
||||
|
||||
const expandedLayout = await page.evaluate(() => {
|
||||
const sidebar = document.querySelector('.sidebar');
|
||||
const groupTitle = document.querySelector('.group-toggle');
|
||||
|
||||
return {
|
||||
hasScrollbar: sidebar.scrollHeight > sidebar.clientHeight,
|
||||
titleRight: groupTitle.getBoundingClientRect().right,
|
||||
url: location.href,
|
||||
};
|
||||
});
|
||||
|
||||
expect(expandedLayout.hasScrollbar).toBe(true);
|
||||
|
||||
await groupTitle.click();
|
||||
await expect(group).toHaveClass(/collapse/);
|
||||
|
||||
const collapsedLayout = await page.evaluate(() => {
|
||||
const sidebar = document.querySelector('.sidebar');
|
||||
const groupTitle = document.querySelector('.group-toggle');
|
||||
|
||||
return {
|
||||
hasScrollbar: sidebar.scrollHeight > sidebar.clientHeight,
|
||||
titleRight: groupTitle.getBoundingClientRect().right,
|
||||
url: location.href,
|
||||
};
|
||||
});
|
||||
|
||||
expect(collapsedLayout.hasScrollbar).toBe(false);
|
||||
expect(collapsedLayout.titleRight).toBe(expandedLayout.titleRight);
|
||||
expect(collapsedLayout.url).toBe(expandedLayout.url);
|
||||
});
|
||||
}
|
||||
|
||||
test('initially collapses root sidebar groups when configured', async ({
|
||||
page,
|
||||
}) => {
|
||||
await docsifyInit({
|
||||
config: {
|
||||
collapseSidebarGroups: true,
|
||||
collapsibleSidebarGroups: true,
|
||||
},
|
||||
styleURLs: ['/dist/themes/core.css'],
|
||||
markdown: {
|
||||
@@ -158,7 +290,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);
|
||||
|
||||
@@ -180,11 +312,15 @@ 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']) {
|
||||
--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 +337,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 +364,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,
|
||||
);
|
||||
@@ -236,10 +390,163 @@ 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: `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><meta charset="UTF-8" /></head>
|
||||
<body class="sidebar-chevron-right">
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</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: {
|
||||
collapsibleSidebarGroups: true,
|
||||
subMaxLevel: 2,
|
||||
},
|
||||
styleURLs: ['/dist/themes/core.css'],
|
||||
html: `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><meta charset="UTF-8" /></head>
|
||||
<body class="sidebar-chevron-right sidebar-chevron-root-hidden">
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</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-toggle[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,
|
||||
}) => {
|
||||
await docsifyInit({
|
||||
config: {
|
||||
collapsibleSidebarGroups: true,
|
||||
},
|
||||
styleURLs: ['/dist/themes/core.css'],
|
||||
html: `
|
||||
<!DOCTYPE html>
|
||||
@@ -267,12 +574,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();
|
||||
@@ -288,7 +595,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 +627,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();
|
||||
|
||||
Reference in New Issue
Block a user