From 956bd1efcbe2067442cf30c31ad51d122a273b83 Mon Sep 17 00:00:00 2001 From: John Hildenbiddle Date: Sat, 7 Oct 2023 14:52:23 -0500 Subject: [PATCH 01/18] Add skip link to main content Fix #494 --- src/core/event/index.js | 20 ++++++++++++++++++++ src/core/render/index.js | 31 ++++++++++++++++--------------- src/core/render/tpl.js | 14 +++++++++++++- src/themes/basic/_layout.styl | 32 ++++++++++++++++++++++++++++++++ 4 files changed, 81 insertions(+), 16 deletions(-) diff --git a/src/core/event/index.js b/src/core/event/index.js index 516f1fb27..4e1630edd 100644 --- a/src/core/event/index.js +++ b/src/core/event/index.js @@ -34,9 +34,13 @@ export function Events(Base) { } initEvent() { + // Bind skip link + this.#skipLink('#skip-to-content'); + // Bind toggle button this.#btn('button.sidebar-toggle', this.router); this.#collapse('.sidebar', this.router); + // Bind sticky effect if (this.config.coverpage) { !isMobile && on('scroll', this.__sticky); @@ -53,6 +57,22 @@ export function Events(Base) { #enableScrollEvent = true; #coverHeight = 0; + #skipLink(el) { + el = dom.getNode(el); + + if (el === null || el === undefined) { + return; + } + + dom.on(el, 'click', evt => { + const target = dom.getNode('.content'); + + evt.preventDefault(); + target && target.focus(); + this.#scrollTo(target); + }); + } + #scrollTo(el, offset = 0) { if (this.#scroller) { this.#scroller.stop(); diff --git a/src/core/render/index.js b/src/core/render/index.js index 2ee9755be..62b68030b 100644 --- a/src/core/render/index.js +++ b/src/core/render/index.js @@ -409,13 +409,13 @@ export function Render(Base) { } const id = config.el || '#app'; - const navEl = dom.find('nav') || dom.create('nav'); - const el = dom.find(id); - let html = ''; - let navAppendToTarget = dom.body; if (el) { + let html = ''; + + html += tpl.skipLink(); + if (config.repo) { html += tpl.corner(config.repo, config.cornerExternalLinkTarget); } @@ -435,25 +435,26 @@ export function Render(Base) { } html += tpl.main(config); + // Render main app this._renderTo(el, html, true); } else { this.rendered = true; } - if (config.mergeNavbar && isMobile) { - navAppendToTarget = dom.find('.sidebar'); - } else { - navEl.classList.add('app-nav'); - - if (!config.repo) { - navEl.classList.add('no-badge'); - } - } - // Add nav if (config.loadNavbar) { - dom.before(navAppendToTarget, navEl); + const navEl = dom.find('nav') || dom.create('nav'); + const isMergedSidebar = config.mergeNavbar && isMobile; + + if (isMergedSidebar) { + dom.find('.sidebar').prepend(navEl); + } else { + dom.find('#skip-to-content').after(navEl); + + navEl.classList.add('app-nav'); + navEl.classList.toggle('no-badge', !config.repo); + } } if (config.themeColor) { diff --git a/src/core/render/tpl.js b/src/core/render/tpl.js index 424ffe88f..b50895d2a 100644 --- a/src/core/render/tpl.js +++ b/src/core/render/tpl.js @@ -58,7 +58,7 @@ export function main(config) { return /* html */ `
${aside} -
+
@@ -122,3 +122,15 @@ export function helper(className, content) { export function theme(color) { return /* html */ ``; } + +/** + * Renders skip link + * @returns {String} HTML of the skip link + */ +export function skipLink() { + return /* html */ ` + + `; +} diff --git a/src/themes/basic/_layout.styl b/src/themes/basic/_layout.styl index 04b7fed82..73463ccd1 100644 --- a/src/themes/basic/_layout.styl +++ b/src/themes/basic/_layout.styl @@ -86,6 +86,38 @@ li input[type='checkbox'] margin 0 0.2em 0.25em 0 vertical-align middle +[tabindex="-1"]:focus + outline none !important + +/* skip link */ +#skip-to-content + appearance none + display block + position fixed + z-index 2147483647 + top 0 + left 50% + padding 0.5rem 1.5rem + border 0 + border-radius: 100vw + background-color $color-primary + background-color var(--theme-color, $color-primary) + color $color-bg + color var(--theme-bg, $color-bg) + opacity 0 + font-size inherit + text-decoration none + transform translate(-50%, -100%) + transition-property opacity, transform + transition-duration 0s, 0.2s + transition-delay 0.2s, 0s + + &:focus + opacity 1 + transform translate(-50%, 0.75rem) + transition-duration 0s, 0.2s + transition-delay 0s, 0s + /* navbar */ .app-nav margin 25px 60px 0 0 From d9ca92b1ac66622ba9c1dc4592f27e0458bc5e14 Mon Sep 17 00:00:00 2001 From: John Hildenbiddle Date: Sat, 7 Oct 2023 16:01:14 -0500 Subject: [PATCH 02/18] Focus on content after navigation --- src/core/event/index.js | 10 ++++++++-- src/core/render/compiler.js | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/core/event/index.js b/src/core/event/index.js index 4e1630edd..9a1fc250e 100644 --- a/src/core/event/index.js +++ b/src/core/event/index.js @@ -18,14 +18,20 @@ export function Events(Base) { // If 'history', rely on the browser's scroll auto-restoration when going back or forward if (source !== 'history') { + let focusEl; + // Scroll to ID if specified if (this.route.query.id) { - this.#scrollIntoView(this.route.path, this.route.query.id); + focusEl = dom.find(`#${this.route.query.id}`); + this.#scrollIntoView(this.route.path, this.route.query.id, true); } // Scroll to top if a link was clicked and auto2top is enabled - if (source === 'navigate') { + else if (source === 'navigate') { + focusEl = dom.find('.content'); auto2top && this.#scroll2Top(auto2top); } + + focusEl && focusEl.focus(); } if (this.config.loadNavbar) { diff --git a/src/core/render/compiler.js b/src/core/render/compiler.js index 3514136a4..23d344357 100644 --- a/src/core/render/compiler.js +++ b/src/core/render/compiler.js @@ -225,7 +225,7 @@ export class Compiler { nextToc.slug = url; _self.toc.push(nextToc); - return `${str}`; + return `${str}`; }; origin.code = highlightCodeCompiler({ renderer }); From 14c4641713981e1f49c0dff440dd9e90cfcb2c6c Mon Sep 17 00:00:00 2001 From: John Hildenbiddle Date: Sat, 7 Oct 2023 16:55:32 -0500 Subject: [PATCH 03/18] Allow config options & dynamic text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Allows skip link text to be updated dynamically based on the route path. See docisfy site’s “Translations” menu as an example of why this is necessary. --- index.html | 6 ++++++ src/core/render/index.js | 28 ++++++++++++++++++++++++---- src/core/render/tpl.js | 12 ------------ 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/index.html b/index.html index 17d28d098..6bed6543f 100644 --- a/index.html +++ b/index.html @@ -89,6 +89,12 @@ }, pathNamespaces: ['/es', '/de-de', '/ru-ru', '/zh-cn'], }, + skipLink: { + '/es/': 'Saltar al contenido principal', + '/de-de/': 'Ga naar de hoofdinhoud', + '/ru-ru/': 'Перейти к основному содержанию', + '/zh-cn/': '跳到主要内容', + }, vueComponents: { 'button-counter': { template: /* html */ ``, diff --git a/src/core/render/index.js b/src/core/render/index.js index 62b68030b..6b49f7c87 100644 --- a/src/core/render/index.js +++ b/src/core/render/index.js @@ -242,6 +242,26 @@ export function Render(Base) { el.setAttribute('href', nameLink[match]); } } + + #renderSkipLink(vm) { + const { skipLink } = vm.config; + + if (skipLink !== false) { + const el = dom.getNode('#skip-to-content'); + const text = + typeof skipLink === 'string' + ? skipLink + : skipLink?.[vm.route.path] || 'Skip to main content'; + + if (el) { + el.innerHTML = text; + } else { + const html = ``; + dom.body.insertAdjacentHTML('afterbegin', html); + } + } + } + _renderTo(el, content, replace) { const node = dom.getNode(el); if (node) { @@ -396,6 +416,9 @@ export function Render(Base) { _updateRender() { // Render name link this.#renderNameLink(this); + + // Render skip link + this.#renderSkipLink(this); } initRender() { @@ -414,8 +437,6 @@ export function Render(Base) { if (el) { let html = ''; - html += tpl.skipLink(); - if (config.repo) { html += tpl.corner(config.repo, config.cornerExternalLinkTarget); } @@ -450,8 +471,7 @@ export function Render(Base) { if (isMergedSidebar) { dom.find('.sidebar').prepend(navEl); } else { - dom.find('#skip-to-content').after(navEl); - + dom.body.prepend(navEl); navEl.classList.add('app-nav'); navEl.classList.toggle('no-badge', !config.repo); } diff --git a/src/core/render/tpl.js b/src/core/render/tpl.js index b50895d2a..b4451ef3e 100644 --- a/src/core/render/tpl.js +++ b/src/core/render/tpl.js @@ -122,15 +122,3 @@ export function helper(className, content) { export function theme(color) { return /* html */ ``; } - -/** - * Renders skip link - * @returns {String} HTML of the skip link - */ -export function skipLink() { - return /* html */ ` - - `; -} From 7eff474d81e215b2c7d0f6f4d455a0af85312e3b Mon Sep 17 00:00:00 2001 From: John Hildenbiddle Date: Sat, 7 Oct 2023 17:18:32 -0500 Subject: [PATCH 04/18] Update docs --- docs/configuration.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/docs/configuration.md b/docs/configuration.md index f6748e6c1..bda7b848c 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -811,6 +811,39 @@ window.$docsify = { } ``` +## skipLink + +- Type: `Boolean|String|Object` +- Default: `'Skip to main content'` + +Determines if/how the site's [skip navigation link](https://webaim.org/techniques/skipnav/) will be rendered. + +```js +// Render skip link or all routes (default) +window.$docsify = { + skipLink: 'Skip to main content', +}; +``` + +```js +// Render localized skip links based on route paths +window.$docsify = { + skipLink: { + '/es/': 'Saltar al contenido principal', + '/de-de/': 'Ga naar de hoofdinhoud', + '/ru-ru/': 'Перейти к основному содержанию', + '/zh-cn/': '跳到主要内容', + }, +}; +``` + +```js +// Do not render skip link +window.$docsify = { + skipLink: false, +}; +``` + ## subMaxLevel - Type: `Number` From e8e748e4b98dfa57e086090d6dcbef7a3660ac3f Mon Sep 17 00:00:00 2001 From: John Hildenbiddle Date: Sun, 8 Oct 2023 08:42:25 -0500 Subject: [PATCH 05/18] Update test snapshots --- test/integration/__snapshots__/docs.test.js.snap | 2 +- test/integration/render.test.js | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/test/integration/__snapshots__/docs.test.js.snap b/test/integration/__snapshots__/docs.test.js.snap index 2e620b9dd..6f606f95d 100644 --- a/test/integration/__snapshots__/docs.test.js.snap +++ b/test/integration/__snapshots__/docs.test.js.snap @@ -9,7 +9,7 @@ exports[`Docs Site coverpage renders and is unchanged 1`] = ` ) \\">
-

\\"logo\\"

docsify 4.13.0

+

\\"logo\\"

docsify 4.13.0

A magical documentation site generator.

  • Simple and lightweight
  • No statically built html files
  • Multiple themes

GitHub Getting Started

diff --git a/test/integration/render.test.js b/test/integration/render.test.js index 8b65f11ca..bf7e77111 100644 --- a/test/integration/render.test.js +++ b/test/integration/render.test.js @@ -164,7 +164,7 @@ describe('render', function () { const output = window.marked('# h1 tag'); expect(output).toMatchInlineSnapshot( - `"

h1 tag

"` + `"

h1 tag

"` ); }); @@ -172,7 +172,7 @@ describe('render', function () { const output = window.marked('## h2 tag'); expect(output).toMatchInlineSnapshot( - `"

h2 tag

"` + `"

h2 tag

"` ); }); @@ -180,7 +180,7 @@ describe('render', function () { const output = window.marked('### h3 tag'); expect(output).toMatchInlineSnapshot( - `"

h3 tag

"` + `"

h3 tag

"` ); }); @@ -188,7 +188,7 @@ describe('render', function () { const output = window.marked('#### h4 tag'); expect(output).toMatchInlineSnapshot( - `"

h4 tag

"` + `"

h4 tag

"` ); }); @@ -196,7 +196,7 @@ describe('render', function () { const output = window.marked('##### h5 tag'); expect(output).toMatchInlineSnapshot( - `"
h5 tag
"` + `"
h5 tag
"` ); }); @@ -204,7 +204,7 @@ describe('render', function () { const output = window.marked('###### h6 tag'); expect(output).toMatchInlineSnapshot( - `"
h6 tag
"` + `"
h6 tag
"` ); }); }); From ff44dea70f3e47f736cc0efaadfe69c9552718bd Mon Sep 17 00:00:00 2001 From: John Hildenbiddle Date: Sun, 8 Oct 2023 10:47:20 -0500 Subject: [PATCH 06/18] Add tests for skip link --- test/integration/render.test.js | 113 ++++++++++++++++++++++++++------ 1 file changed, 92 insertions(+), 21 deletions(-) diff --git a/test/integration/render.test.js b/test/integration/render.test.js index bf7e77111..3bd6d4081 100644 --- a/test/integration/render.test.js +++ b/test/integration/render.test.js @@ -4,15 +4,13 @@ import docsifyInit from '../helpers/docsify-init.js'; // Suite // ----------------------------------------------------------------------------- describe('render', function () { - // Setup & Teardown - // ------------------------------------------------------------------------- - beforeEach(async () => { - await docsifyInit(); - }); - // Helpers // --------------------------------------------------------------------------- describe('helpers', () => { + beforeEach(async () => { + await docsifyInit(); + }); + test('important content', () => { const output = window.marked('!> Important content'); @@ -33,6 +31,10 @@ describe('render', function () { // Lists // --------------------------------------------------------------------------- describe('lists', function () { + beforeEach(async () => { + await docsifyInit(); + }); + test('as unordered task list', async function () { const output = window.marked(stripIndent` - [x] Task 1 @@ -100,6 +102,10 @@ describe('render', function () { // Images // --------------------------------------------------------------------------- describe('images', function () { + beforeEach(async () => { + await docsifyInit(); + }); + test('regular', async function () { const output = window.marked('![alt text](http://imageUrl)'); @@ -136,30 +142,32 @@ describe('render', function () { ); }); - describe('size', function () { - test('width and height', async function () { - const output = window.marked( - "![alt text](http://imageUrl ':size=WIDTHxHEIGHT')" - ); + test('width and height', async function () { + const output = window.marked( + "![alt text](http://imageUrl ':size=WIDTHxHEIGHT')" + ); - expect(output).toMatchInlineSnapshot( - `"

\\"alt

"` - ); - }); + expect(output).toMatchInlineSnapshot( + `"

\\"alt

"` + ); + }); - test('width', async function () { - const output = window.marked("![alt text](http://imageUrl ':size=50')"); + test('width', async function () { + const output = window.marked("![alt text](http://imageUrl ':size=50')"); - expect(output).toMatchInlineSnapshot( - `"

\\"alt

"` - ); - }); + expect(output).toMatchInlineSnapshot( + `"

\\"alt

"` + ); }); }); // Headings // --------------------------------------------------------------------------- describe('headings', function () { + beforeEach(async () => { + await docsifyInit(); + }); + test('h1', async function () { const output = window.marked('# h1 tag'); @@ -209,7 +217,13 @@ describe('render', function () { }); }); + // Links + // --------------------------------------------------------------------------- describe('link', function () { + beforeEach(async () => { + await docsifyInit(); + }); + test('regular', async function () { const output = window.marked('[alt text](http://url)'); @@ -264,4 +278,61 @@ describe('render', function () { ); }); }); + + // Skip Link + // --------------------------------------------------------------------------- + describe('skip link', () => { + test('renders default skip link and label', async () => { + await docsifyInit(); + + const elm = document.getElementById('skip-to-content'); + const expectText = 'Skip to main content'; + + expect(elm.textContent).toBe(expectText); + expect(elm.outerHTML).toMatchInlineSnapshot( + `""` + ); + }); + + test('renders custom label from config string', async () => { + const expectText = 'test'; + + await docsifyInit({ + config: { + skipLink: expectText, + }, + }); + + const elm = document.getElementById('skip-to-content'); + + expect(elm.textContent).toBe(expectText); + }); + + test('renders custom label from config object', async () => { + const expectText = 'test'; + + await docsifyInit({ + config: { + skipLink: { + '/': expectText, + }, + }, + }); + + const elm = document.getElementById('skip-to-content'); + + expect(elm.textContent).toBe(expectText); + }); + + test('does not render skip link when false', async () => { + await docsifyInit({ + config: { + skipLink: false, + }, + }); + const elm = document.getElementById('skip-to-content') || false; + + expect(elm).toBe(false); + }); + }); }); From 28fda0322cc6e895d83b689837a3be1984818845 Mon Sep 17 00:00:00 2001 From: John Hildenbiddle Date: Sun, 22 Oct 2023 14:10:19 -0500 Subject: [PATCH 07/18] Update docs/configuration.md Co-authored-by: Joe Pea --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index bda7b848c..1329d2aef 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -819,7 +819,7 @@ window.$docsify = { Determines if/how the site's [skip navigation link](https://webaim.org/techniques/skipnav/) will be rendered. ```js -// Render skip link or all routes (default) +// Render skip link for all routes (default) window.$docsify = { skipLink: 'Skip to main content', }; From 277e429cd62a5920b484752d5a75b3f5d00b2f01 Mon Sep 17 00:00:00 2001 From: John Hildenbiddle Date: Fri, 27 Oct 2023 14:00:10 -0500 Subject: [PATCH 08/18] =?UTF-8?q?Move=20skip=20link=20focus=20to=20id=3D?= =?UTF-8?q?=E2=80=9Cmain=E2=80=9D=20element?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/event/index.js | 4 ++-- src/core/render/tpl.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core/event/index.js b/src/core/event/index.js index 9a1fc250e..9aa44ccdb 100644 --- a/src/core/event/index.js +++ b/src/core/event/index.js @@ -27,7 +27,7 @@ export function Events(Base) { } // Scroll to top if a link was clicked and auto2top is enabled else if (source === 'navigate') { - focusEl = dom.find('.content'); + focusEl = dom.find('#main'); auto2top && this.#scroll2Top(auto2top); } @@ -71,7 +71,7 @@ export function Events(Base) { } dom.on(el, 'click', evt => { - const target = dom.getNode('.content'); + const target = dom.getNode('#main'); evt.preventDefault(); target && target.focus(); diff --git a/src/core/render/tpl.js b/src/core/render/tpl.js index b4451ef3e..77776306c 100644 --- a/src/core/render/tpl.js +++ b/src/core/render/tpl.js @@ -58,8 +58,8 @@ export function main(config) { return /* html */ `
${aside} -
-
+
+
`; From f300d40aa029cb4e95d215d416d473e5ce696d8c Mon Sep 17 00:00:00 2001 From: John Hildenbiddle Date: Fri, 27 Oct 2023 16:26:40 -0500 Subject: [PATCH 09/18] Add tabindex comment --- src/core/render/compiler.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/core/render/compiler.js b/src/core/render/compiler.js index 23d344357..26c66c615 100644 --- a/src/core/render/compiler.js +++ b/src/core/render/compiler.js @@ -225,6 +225,10 @@ export class Compiler { nextToc.slug = url; _self.toc.push(nextToc); + // Note: tabindex="-1" allows programmatically focusing on heading + // elements after navigation. This is preferred over focusing on the link + // within the heading because it matches the focus behavior of screen + // readers when navigating page content. return `${str}`; }; From b42de5879f4413940e9bcada273e21846e420a56 Mon Sep 17 00:00:00 2001 From: John Hildenbiddle Date: Sat, 28 Oct 2023 12:28:55 -0500 Subject: [PATCH 10/18] Replace prop accessor with regex --- src/core/render/index.js | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/core/render/index.js b/src/core/render/index.js index 6b49f7c87..d0ee90425 100644 --- a/src/core/render/index.js +++ b/src/core/render/index.js @@ -248,15 +248,23 @@ export function Render(Base) { if (skipLink !== false) { const el = dom.getNode('#skip-to-content'); - const text = - typeof skipLink === 'string' - ? skipLink - : skipLink?.[vm.route.path] || 'Skip to main content'; + + let skipLinkText = + typeof skipLink === 'string' ? skipLink : 'Skip to main content'; + + if (skipLink?.constructor === Object) { + const matchingPath = Object.keys(skipLink).find(path => + vm.route.path.startsWith(path.startsWith('/') ? path : `/${path}`) + ); + const matchingText = matchingPath && skipLink[matchingPath]; + + skipLinkText = matchingText || skipLinkText; + } if (el) { - el.innerHTML = text; + el.innerHTML = skipLinkText; } else { - const html = ``; + const html = ``; dom.body.insertAdjacentHTML('afterbegin', html); } } From 965aa884d0efe8600293fdd4429661f0e420d4d2 Mon Sep 17 00:00:00 2001 From: John Hildenbiddle Date: Sun, 29 Oct 2023 21:41:05 -0500 Subject: [PATCH 11/18] Update skipLink tests --- test/integration/render.test.js | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/test/integration/render.test.js b/test/integration/render.test.js index 3bd6d4081..af52274fa 100644 --- a/test/integration/render.test.js +++ b/test/integration/render.test.js @@ -1,5 +1,6 @@ import stripIndent from 'common-tags/lib/stripIndent/index.js'; import docsifyInit from '../helpers/docsify-init.js'; +import { waitForSelector, waitForText } from '../helpers/wait-for.js'; // Suite // ----------------------------------------------------------------------------- @@ -309,19 +310,30 @@ describe('render', function () { }); test('renders custom label from config object', async () => { - const expectText = 'test'; + const getSkipLinkText = () => + document.getElementById('skip-to-content').textContent; await docsifyInit({ config: { skipLink: { - '/': expectText, + '/dir1/dir2/': 'baz', + '/dir1/': 'bar', }, }, }); - const elm = document.getElementById('skip-to-content'); + window.location.hash = '/dir1/dir2/'; + await waitForText('#skip-to-content', 'baz'); + expect(getSkipLinkText()).toBe('baz'); - expect(elm.textContent).toBe(expectText); + window.location.hash = '/dir1/'; + await waitForText('#skip-to-content', 'bar'); + expect(getSkipLinkText()).toBe('bar'); + + // Fallback to default + window.location.hash = ''; + await waitForText('#skip-to-content', 'Skip to main content'); + expect(getSkipLinkText()).toBe('Skip to main content'); }); test('does not render skip link when false', async () => { From 5effec8c16af0e3c4cac39bf36bb985be43528c4 Mon Sep 17 00:00:00 2001 From: John Hildenbiddle Date: Sun, 29 Oct 2023 21:43:06 -0500 Subject: [PATCH 12/18] Add localized skipLink strings to docs site --- docs/index.html | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/index.html b/docs/index.html index 2eaeb3acb..6c7a3e434 100644 --- a/docs/index.html +++ b/docs/index.html @@ -125,6 +125,12 @@ }, pathNamespaces: ['/es', '/de-de', '/ru-ru', '/zh-cn'], }, + skipLink: { + '/es/': 'Saltar al contenido principal', + '/de-de/': 'Ga naar de hoofdinhoud', + '/ru-ru/': 'Перейти к основному содержанию', + '/zh-cn/': '跳到主要内容', + }, vueComponents: { 'button-counter': { template: /* html */ ``, From c09e7cfbe8bdd80ae556aa53374bb67b8ddb47ca Mon Sep 17 00:00:00 2001 From: John Hildenbiddle Date: Mon, 30 Oct 2023 10:50:52 -0500 Subject: [PATCH 13/18] Move focus to first heading on page load --- src/core/event/index.js | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/src/core/event/index.js b/src/core/event/index.js index 9aa44ccdb..0aae6c0d6 100644 --- a/src/core/event/index.js +++ b/src/core/event/index.js @@ -14,27 +14,25 @@ import config from '../config.js'; export function Events(Base) { return class Events extends Base { $resetEvents(source) { - const { auto2top } = this.config; + const { auto2top, loadNavbar } = this.config; + const { path, query } = this.route; - // If 'history', rely on the browser's scroll auto-restoration when going back or forward + // If navigation occurred via forward/back, rely on the browser's native + // focus and scroll auto-restoration behavior if (source !== 'history') { - let focusEl; - // Scroll to ID if specified - if (this.route.query.id) { - focusEl = dom.find(`#${this.route.query.id}`); - this.#scrollIntoView(this.route.path, this.route.query.id, true); + if (query.id) { + this.#scrollIntoView(path, query.id, true); } // Scroll to top if a link was clicked and auto2top is enabled else if (source === 'navigate') { - focusEl = dom.find('#main'); auto2top && this.#scroll2Top(auto2top); } - - focusEl && focusEl.focus(); } - if (this.config.loadNavbar) { + this.focusContent(); + + if (loadNavbar) { this.__getAndActive(this.router, 'nav'); } } @@ -101,6 +99,20 @@ export function Events(Base) { .begin(); } + focusContent() { + const { query } = this.route; + const focusEl = query.id + ? // Heading ID + dom.find(`#${query.id}`) + : // First heading + dom.find('#main :where(h1, h2, h3, h4, h5, h6)') || + // Content container + dom.find('#main'); + + // Move focus to content area + focusEl && focusEl.focus(); + } + #highlight(path) { if (!this.#enableScrollEvent) { return; From 90a58e56303a9e71ad61057727a98c114e487765 Mon Sep 17 00:00:00 2001 From: John Hildenbiddle Date: Mon, 30 Oct 2023 15:42:08 -0500 Subject: [PATCH 14/18] Fix loading empty file and focus on 404 --- src/core/render/index.js | 8 +++++--- test/e2e/virtual-routes.test.js | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/core/render/index.js b/src/core/render/index.js index d0ee90425..259435654 100644 --- a/src/core/render/index.js +++ b/src/core/render/index.js @@ -61,7 +61,7 @@ export function Render(Base) { }; if (!html) { - html = /* html */ `

404 - Not found

`; + html = this.compiler.header('404 - Not found', 1); } if ('Vue' in window) { @@ -338,8 +338,10 @@ export function Render(Base) { } _renderMain(text, opt = {}, next) { - if (!text) { - return this.#renderMain(text); + if (typeof text !== 'string') { + this.#renderMain(text); + this.focusContent(); + return; } this.callHook('beforeEach', text, result => { diff --git a/test/e2e/virtual-routes.test.js b/test/e2e/virtual-routes.test.js index b84ca30ca..0b2e3f86c 100644 --- a/test/e2e/virtual-routes.test.js +++ b/test/e2e/virtual-routes.test.js @@ -221,7 +221,7 @@ test.describe('Virtual Routes - Generate Dynamic Content via Config', () => { await navigateToRoute(page, '/d'); const mainElm = page.locator('#main'); - await expect(mainElm).toContainText('404 - Not found'); + await expect(mainElm).toContainText('404 - Not Found'); }); test('skip routes that returned a falsy value that is not a boolean', async ({ @@ -263,7 +263,7 @@ test.describe('Virtual Routes - Generate Dynamic Content via Config', () => { await navigateToRoute(page, '/multiple/matches'); const mainElm = page.locator('#main'); - await expect(mainElm).toContainText('404 - Not found'); + await expect(mainElm).toContainText('404 - Not Found'); }); test('skip routes that are not a valid string or function', async ({ From fff037c1e26a3bac43bf9b13d066418f9670879f Mon Sep 17 00:00:00 2001 From: John Hildenbiddle Date: Mon, 30 Oct 2023 23:34:13 -0500 Subject: [PATCH 15/18] Remove unused import (lint issue) --- test/integration/render.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/render.test.js b/test/integration/render.test.js index af52274fa..4b6fbc1b3 100644 --- a/test/integration/render.test.js +++ b/test/integration/render.test.js @@ -1,6 +1,6 @@ import stripIndent from 'common-tags/lib/stripIndent/index.js'; import docsifyInit from '../helpers/docsify-init.js'; -import { waitForSelector, waitForText } from '../helpers/wait-for.js'; +import { waitForText } from '../helpers/wait-for.js'; // Suite // ----------------------------------------------------------------------------- From 3f712259488e30d77d0bc4844f85751cc4d139d2 Mon Sep 17 00:00:00 2001 From: John Hildenbiddle Date: Mon, 30 Oct 2023 23:40:05 -0500 Subject: [PATCH 16/18] Update default 404 text --- docs/configuration.md | 2 +- src/core/render/index.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 1329d2aef..2a7a3c280 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -534,7 +534,7 @@ To disable emoji parsing of individual shorthand codes, replace `:` characters w - Type: `Boolean` | `String` | `Object` - Default: `false` -Display default "404 - Not found" message: +Display default "404 - Not Found" message: ```js window.$docsify = { diff --git a/src/core/render/index.js b/src/core/render/index.js index 259435654..c4a7837dd 100644 --- a/src/core/render/index.js +++ b/src/core/render/index.js @@ -61,7 +61,7 @@ export function Render(Base) { }; if (!html) { - html = this.compiler.header('404 - Not found', 1); + html = this.compiler.header('404 - Not Found', 1); } if ('Vue' in window) { From e277339e8256c1d1dfee81891b4eb2ed8538bd3a Mon Sep 17 00:00:00 2001 From: John Hildenbiddle Date: Thu, 2 Nov 2023 11:26:34 -0500 Subject: [PATCH 17/18] Fix content focus on initial page load --- src/core/event/index.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/core/event/index.js b/src/core/event/index.js index 0aae6c0d6..8f4da0c78 100644 --- a/src/core/event/index.js +++ b/src/core/event/index.js @@ -17,8 +17,7 @@ export function Events(Base) { const { auto2top, loadNavbar } = this.config; const { path, query } = this.route; - // If navigation occurred via forward/back, rely on the browser's native - // focus and scroll auto-restoration behavior + // Note: Scroll position set by browser on forward/back (i.e. "history") if (source !== 'history') { // Scroll to ID if specified if (query.id) { @@ -30,7 +29,10 @@ export function Events(Base) { } } - this.focusContent(); + // Focus on the content area after navigation + if (source === 'navigate') { + this.focusContent(); + } if (loadNavbar) { this.__getAndActive(this.router, 'nav'); From 346a4e795fc190b7534dfd294d762e1c8325e38f Mon Sep 17 00:00:00 2001 From: John Hildenbiddle Date: Wed, 22 Nov 2023 12:15:29 -0600 Subject: [PATCH 18/18] Focus on URL heading on page load --- src/core/event/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/event/index.js b/src/core/event/index.js index 8f4da0c78..7d5e3aac2 100644 --- a/src/core/event/index.js +++ b/src/core/event/index.js @@ -29,8 +29,8 @@ export function Events(Base) { } } - // Focus on the content area after navigation - if (source === 'navigate') { + // Move focus to content + if (query.id || source === 'navigate') { this.focusContent(); }