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
83 changes: 61 additions & 22 deletions src/core/event/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { isMobile, mobileBreakpoint } from '../util/env.js';
import { noop } from '../util/core.js';
import * as dom from '../util/dom.js';
import { stripUrlExceptId } from '../router/util.js';
import {
findLinkByHref,
resolveHref,
stripUrlExceptId,
} from '../router/util.js';

/** @typedef {import('../Docsify.js').Constructor} Constructor */

Expand Down Expand Up @@ -403,9 +407,11 @@ 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 {import('../router/util.js').SidebarNavigationTarget} [focusTarget]
* Sidebar link to restore after rendering
* @void
*/
onNavigate(source) {
onNavigate(source, focusTarget) {
const { auto2top, topMargin } = this.config;
const { path, query } = this.route;
const activeSidebarElm = this.#markSidebarActiveElm();
Expand Down Expand Up @@ -446,7 +452,12 @@ export function Events(Base) {

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

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

Expand Down Expand Up @@ -494,11 +505,47 @@ export function Events(Base) {
return focusEl;
}

/**
* Restore focus to the rendered sidebar link that initiated navigation.
*
* @param {import('../router/util.js').SidebarNavigationTarget} [target]
* Sidebar navigation target
* @returns {boolean} True when focus was restored
*/
#focusSidebarNavigation(target) {
if (!target || isMobile()) {
return false;
}

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

if (!sidebarElm) {
return false;
}

const focusElm = /** @type {HTMLElement|undefined} */ (
dom
.findAll(sidebarElm, 'a')
.find(
linkElm =>
linkElm.classList.contains(target.className) &&
/** @type {HTMLAnchorElement} */ (linkElm).href === target.href,
)
);

if (!focusElm) {
return false;
}

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

/**
* Marks the active app nav item
*/
#markAppNavActiveElm() {
const href = decodeURIComponent(this.router.toURL(this.route.path));
const href = resolveHref(this.router.toURL(this.route.path));

['.app-nav', '.app-nav-merged'].forEach(selector => {
const navElm = dom.find(selector);
Expand All @@ -511,13 +558,7 @@ export function Events(Base) {
dom.findAll(navElm, 'a')
)
.sort((a, b) => b.href.length - a.href.length)
.find(
a =>
href.includes(/** @type {string} */ (a.getAttribute('href'))) ||
href.includes(
decodeURI(/** @type {string} */ (a.getAttribute('href'))),
),
)
.find(a => href.includes(a.href))
?.closest('li');
const oldActive = dom.find(navElm, 'li.active');

Expand All @@ -544,13 +585,14 @@ export function Events(Base) {
return;
}

href = stripUrlExceptId(href);
const matchingHref = stripUrlExceptId(/** @type {string} */ (href));

const oldActive = dom.find(sidebar, 'li.active');
const sidebarSelector = `.sidebar-nav a[href="${href}"], .sidebar-nav a[href="${decodeURIComponent(
/** @type {string} */ (href),
)}"]`;
const newActive = dom.find(sidebar, sidebarSelector)?.closest('li');
const newActive = findLinkByHref(
sidebar,
matchingHref,
'.sidebar-nav a',
)?.closest('li');

if (newActive && newActive !== oldActive) {
oldActive?.classList.remove('active');
Expand Down Expand Up @@ -578,12 +620,9 @@ export function Events(Base) {

const path = href?.split('?')[0];
const oldPage = dom.find(sidebar, 'li[aria-current]');
const newPage = dom
.find(
sidebar,
`a[href="${path}"], a[href="${decodeURIComponent(/** @type {string} */ (path))}"]`,
)
?.closest('li');
const newPage = path
? findLinkByHref(sidebar, path, '.sidebar-nav a')?.closest('li')
: undefined;

if (newPage && newPage !== oldPage) {
oldPage?.removeAttribute('aria-current');
Expand Down
12 changes: 7 additions & 5 deletions src/core/render/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import tinydate from 'tinydate';
import * as dom from '../util/dom.js';
import { cleanPath, getPath, isAbsolutePath } from '../router/util.js';
import {
cleanPath,
findLinkByHref,
getPath,
isAbsolutePath,
} from '../router/util.js';
import { isMobile } from '../util/env.js';
import { isExternal, isPrimitive } from '../util/core.js';
import { Compiler } from './compiler.js';
Expand Down Expand Up @@ -360,11 +365,8 @@ export function Render(Base) {

sidebarToggleEl.setAttribute('aria-expanded', String(!isMobile()));

const activeElmHref = decodeURIComponent(
this.router.toURL(this.route.path),
);
const activeEl = /** @type {HTMLElement | null} */ (
dom.find(`.sidebar-nav a[href="${activeElmHref}"]`)
findLinkByHref(sidebarNavEl, this.router.toURL(this.route.path), 'a')
);

this.#addTextAsTitleAttribute('.sidebar-nav a');
Expand Down
34 changes: 26 additions & 8 deletions src/core/router/history/hash.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { isExternal, noop } from '../../util/core.js';
import { on } from '../../util/dom.js';
import { parseQuery, cleanPath, replaceSlug } from '../util.js';
import {
cleanPath,
getClickedLink,
getSidebarNavigationTarget,
isCurrentContextNavigation,
parseQuery,
replaceSlug,
} from '../util.js';
import { History } from './base.js';

function replaceHash(path) {
Expand Down Expand Up @@ -34,35 +41,46 @@ export class HashHistory extends History {
return index === -1 ? '' : href.slice(index + 1);
}

/** @param {(params: {source: any, event?: any}) => void} [cb] */
/** @param {(params: {source: any, focusTarget?: import('../util.js').SidebarNavigationTarget}) => void} [cb] */
onchange(cb = noop) {
// The hashchange event does not tell us if it originated from
// a clicked link or by moving back/forward in the 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 navigatingFocusTarget;

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

if (el && el.tagName === 'A' && !isExternal(el.href)) {
if (el && isCurrentContextNavigation(e, el) && !isExternal(el.href)) {
navigating = true;
navigatingFocusTarget = getSidebarNavigationTarget(el);

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

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

on('hashchange', e => {
on('hashchange', () => {
const source = navigating ? 'navigate' : 'history';
const focusTarget = navigating ? navigatingFocusTarget : undefined;

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

Expand Down
21 changes: 15 additions & 6 deletions src/core/router/history/html5.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { isExternal, noop } from '../../util/core.js';
import { on } from '../../util/dom.js';
import { parseQuery, getPath } from '../util.js';
import {
getClickedLink,
getPath,
getSidebarNavigationTarget,
isCurrentContextNavigation,
parseQuery,
} from '../util.js';
import { History } from './base.js';

export class HTML5History extends History {
Expand All @@ -20,18 +26,21 @@ export class HTML5History extends History {
/** @param {(params: any) => void} [cb] */
onchange(cb = noop) {
on('click', e => {
const el = e.target.tagName === 'A' ? e.target : e.target.parentNode;
const el = getClickedLink(e);

if (el && el.tagName === 'A' && !isExternal(el.href)) {
if (el && isCurrentContextNavigation(e, el) && !isExternal(el.href)) {
e.preventDefault();
const url = el.href;
window.history.pushState({ key: url }, '', url);
cb({ event: e, source: 'navigate' });
cb({
focusTarget: getSidebarNavigationTarget(el),
source: 'navigate',
});
}
});

on('popstate', e => {
cb({ event: e, source: 'history' });
on('popstate', () => {
cb({ source: 'history' });
});
}

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.focusTarget);
return;
}

this.$fetch(noop, this.onNavigate.bind(this, params.source));
this.$fetch(
noop,
this.onNavigate.bind(this, params.source, params.focusTarget),
);
lastRoute = this.route;
});
}
Expand Down
Loading