Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 66 additions & 3 deletions src/core/event/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { isMobile, mobileBreakpoint } from '../util/env.js';
import { noop } from '../util/core.js';
import { isExternal, noop } from '../util/core.js';
import * as dom from '../util/dom.js';
import { stripUrlExceptId } from '../router/util.js';

Expand Down Expand Up @@ -360,9 +360,10 @@ export function Events(Base) {
* @param {undefined|"history"|"navigate"} source Type of navigation where
* undefined is initial load, "history" is forward/back, and "navigate" is
* user click/tap
* @param {Event} [event] Navigation event
* @void
*/
onNavigate(source) {
onNavigate(source, event) {
const { auto2top, topMargin } = this.config;
const { path, query } = this.route;
const activeSidebarElm = this.#markSidebarActiveElm();
Expand Down Expand Up @@ -403,7 +404,13 @@ export function Events(Base) {

// Clicked anchor link or page load with anchor ID
if (hasId || isNavigate) {
this.#focusContent();
const sidebarFocused =
isNavigate &&
this.#focusSidebarNavigation(this.#getSidebarNavigationHref(event));

if (!sidebarFocused) {
this.#focusContent();
}
}
}

Expand Down Expand Up @@ -451,6 +458,62 @@ export function Events(Base) {
return focusEl;
}

/**
* Get the clicked sidebar link HREF from a navigation event.
*
* @param {Event} [event] Navigation event
* @returns {string|undefined}
*/
#getSidebarNavigationHref(event) {
const target = event?.target;
const linkElm =
target instanceof Element
? /** @type {HTMLAnchorElement|null} */ (target.closest('a'))
: null;

if (
!linkElm ||
!linkElm.matches('.app-name-link, .page-link, .section-link') ||
isExternal(linkElm.href)
) {
return;
}

return linkElm.getAttribute('href') || undefined;
}

/**
* Restore focus to the rendered sidebar link that initiated navigation.
*
* @param {string} [href] Sidebar link HREF
* @returns {boolean} True when focus was restored
*/
#focusSidebarNavigation(href) {
if (!href || isMobile()) {
return false;
}

const sidebarElm = dom.find('.sidebar');

if (!sidebarElm) {
return false;
}

const focusElm = /** @type {HTMLElement|null} */ (
dom.find(
sidebarElm,
`a[href="${href}"], a[href="${decodeURIComponent(href)}"]`,
)
);

if (!focusElm) {
return false;
}

focusElm.focus({ preventScroll: true });
return true;
}

/**
* Marks the active app nav item
*/
Expand Down
13 changes: 12 additions & 1 deletion src/core/router/history/hash.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,28 +41,39 @@ export class HashHistory extends History {
// therefore we set a `navigating` flag when a link is clicked
// to be able to tell these two scenarios apart
let navigating = false;
let navigatingEvent;

on('click', e => {
const el = e.target.tagName === 'A' ? e.target : e.target.parentNode;

if (el && el.tagName === 'A' && !isExternal(el.href)) {
navigating = true;
navigatingEvent = e;

// Do not compare hash containing these classes.
if (['app-name-link', 'page-link'].includes(el.className)) {
if (el.hash === location.hash) {
navigating = false;
navigatingEvent = undefined;
}
return;
}

if (el.hash === location.hash) {
cb({ event: e, source: 'navigate' });
navigating = false;
navigatingEvent = undefined;
}
}
});

on('hashchange', e => {
const source = navigating ? 'navigate' : 'history';
const event = navigating ? navigatingEvent || e : e;

navigating = false;
cb({ event: e, source });
navigatingEvent = undefined;
cb({ event, source });
});
}

Expand Down
7 changes: 5 additions & 2 deletions src/core/router/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,14 @@ export function Router(Base) {
this._updateRender();

if (lastRoute.path === this.route.path) {
this.onNavigate(params.source);
this.onNavigate(params.source, params.event);
return;
}

this.$fetch(noop, this.onNavigate.bind(this, params.source));
this.$fetch(
noop,
this.onNavigate.bind(this, params.source, params.event),
);
lastRoute = this.route;
});
}
Expand Down
37 changes: 37 additions & 0 deletions test/e2e/sidebar.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,43 @@ test.describe('Sidebar Tests', () => {
await expect(activeLinkElm).toHaveText('Test >');
expect(page.url()).toMatch(/\/test%3Efoo$/);
});

test('keeps focus on activated sidebar page links', async ({ page }) => {
const docsifyInitConfig = {
markdown: {
homepage: `
# Home
`,
sidebar: `
- [Home](/)
- [Guide](guide)
`,
},
routes: {
'/guide.md': `
# Guide
`,
},
};

await docsifyInit(docsifyInitConfig);

const guideLinkElm = page.locator('.sidebar-nav a[href="#/guide"]');

await guideLinkElm.focus();
await page.keyboard.press('Enter');
await expect(page).toHaveURL(/#\/guide$/);
await expect(page.locator('#guide')).toBeVisible();
await expect(guideLinkElm).toBeFocused();

const homeLinkElm = page.locator('.sidebar-nav a[href="#/"]');

await homeLinkElm.focus();
await page.keyboard.press('Enter');
await expect(page).toHaveURL(/#\/$/);
await expect(page.locator('#home')).toBeVisible();
await expect(homeLinkElm).toBeFocused();
});
});

test.describe('Configuration: autoHeader', () => {
Expand Down
Loading