Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add "Skip to main content" link and update nav behavior #2253

Merged
merged 24 commits into from
Nov 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
956bd1e
Add skip link to main content
jhildenbiddle Oct 7, 2023
d9ca92b
Focus on content after navigation
jhildenbiddle Oct 7, 2023
14c4641
Allow config options & dynamic text
jhildenbiddle Oct 7, 2023
7eff474
Update docs
jhildenbiddle Oct 7, 2023
e8e748e
Update test snapshots
jhildenbiddle Oct 8, 2023
ff44dea
Add tests for skip link
jhildenbiddle Oct 8, 2023
64d69f9
Merge branch 'develop' into 494-skip-link
jhildenbiddle Oct 17, 2023
54e5d27
Merge branch 'develop' into 494-skip-link
Koooooo-7 Oct 22, 2023
28fda03
Update docs/configuration.md
jhildenbiddle Oct 22, 2023
277e429
Move skip link focus to id=“main” element
jhildenbiddle Oct 27, 2023
f300d40
Add tabindex comment
jhildenbiddle Oct 27, 2023
b42de58
Replace prop accessor with regex
jhildenbiddle Oct 28, 2023
965aa88
Update skipLink tests
jhildenbiddle Oct 30, 2023
5effec8
Add localized skipLink strings to docs site
jhildenbiddle Oct 30, 2023
c09e7cf
Move focus to first heading on page load
jhildenbiddle Oct 30, 2023
90a58e5
Fix loading empty file and focus on 404
jhildenbiddle Oct 30, 2023
fff037c
Remove unused import (lint issue)
jhildenbiddle Oct 31, 2023
3f71225
Update default 404 text
jhildenbiddle Oct 31, 2023
d846037
Merge branch 'develop' into 494-skip-link
jhildenbiddle Oct 31, 2023
e277339
Fix content focus on initial page load
jhildenbiddle Nov 2, 2023
11ba2b5
Merge branch 'develop' into 494-skip-link
jhildenbiddle Nov 16, 2023
d7101bb
Merge branch 'develop' into 494-skip-link
jhildenbiddle Nov 17, 2023
346a4e7
Focus on URL heading on page load
jhildenbiddle Nov 22, 2023
ce9198f
Merge branch 'develop' into 494-skip-link
jhildenbiddle Nov 30, 2023
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
33 changes: 33 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 for 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`
Expand Down
6 changes: 6 additions & 0 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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 */ `<button @click="count += 1">You clicked me {{ count }} times</button>`,
Expand Down
6 changes: 6 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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 */ `<button @click="count += 1">You clicked me {{ count }} times</button>`,
Expand Down
52 changes: 46 additions & 6 deletions src/core/event/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,29 +14,39 @@ 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
// Note: Scroll position set by browser on forward/back (i.e. "history")
if (source !== 'history') {
// Scroll to ID if specified
if (this.route.query.id) {
this.#scrollIntoView(this.route.path, this.route.query.id);
if (query.id) {
this.#scrollIntoView(path, query.id, true);
}
// Scroll to top if a link was clicked and auto2top is enabled
if (source === 'navigate') {
else if (source === 'navigate') {
auto2top && this.#scroll2Top(auto2top);
}
}

if (this.config.loadNavbar) {
// Move focus to content
if (query.id || source === 'navigate') {
this.focusContent();
}

if (loadNavbar) {
this.__getAndActive(this.router, 'nav');
}
}

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);
Expand All @@ -53,6 +63,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('#main');

evt.preventDefault();
target && target.focus();
this.#scrollTo(target);
});
}

#scrollTo(el, offset = 0) {
if (this.#scroller) {
this.#scroller.stop();
Expand All @@ -75,6 +101,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;
Expand Down
6 changes: 5 additions & 1 deletion src/core/render/compiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,11 @@ export class Compiler {
nextToc.slug = url;
_self.toc.push(nextToc);

return `<h${level} id="${slug}"><a href="${url}" data-id="${slug}" class="anchor"><span>${str}</span></a></h${level}>`;
// 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 `<h${level} id="${slug}" tabindex="-1"><a href="${url}" data-id="${slug}" class="anchor"><span>${str}</span></a></h${level}>`;
};

origin.code = highlightCodeCompiler({ renderer });
Expand Down
61 changes: 45 additions & 16 deletions src/core/render/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,34 @@ 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');

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 = skipLinkText;
} else {
const html = `<button id="skip-to-content">${skipLinkText}</button>`;
dom.body.insertAdjacentHTML('afterbegin', html);
}
}
}

_renderTo(el, content, replace) {
const node = dom.getNode(el);
if (node) {
Expand Down Expand Up @@ -396,6 +424,9 @@ export function Render(Base) {
_updateRender() {
// Render name link
this.#renderNameLink(this);

// Render skip link
this.#renderSkipLink(this);
}

initRender() {
Expand All @@ -409,14 +440,10 @@ 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) {
navEl.setAttribute('aria-label', 'secondary');
let html = '';

if (config.repo) {
html += tpl.corner(config.repo, config.cornerExternalLinkTarget);
Expand All @@ -437,25 +464,27 @@ 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;

navEl.setAttribute('aria-label', 'secondary');

if (isMergedSidebar) {
dom.find('.sidebar').prepend(navEl);
} else {
dom.body.prepend(navEl);
navEl.classList.add('app-nav');
navEl.classList.toggle('no-badge', !config.repo);
}
}

if (config.themeColor) {
Expand Down
2 changes: 1 addition & 1 deletion src/core/render/tpl.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export function main(config) {
return /* html */ `
<main role="presentation">${aside}
<section class="content">
<article class="markdown-section" id="main" role="main"><!--main--></article>
<article id="main" class="markdown-section" role="main" tabindex="-1"><!--main--></article>
</section>
</main>
`;
Expand Down
32 changes: 32 additions & 0 deletions src/themes/basic/_layout.styl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion test/integration/__snapshots__/docs.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ exports[`Docs Site coverpage renders and is unchanged 1`] = `
)
\\">
<div class=\\"mask\\"></div>
<div class=\\"cover-main\\"><p><img src=\\"http://127.0.0.1:3001/_media/icon.svg\\" data-origin=\\"_media/icon.svg\\" alt=\\"logo\\"></p><h1 id=\\"docsify-4130\\"><a href=\\"#/?id=docsify-4130\\" data-id=\\"docsify-4130\\" class=\\"anchor\\"><span>docsify <small>4.13.0</small></span></a></h1><blockquote>
<div class=\\"cover-main\\"><p><img src=\\"http://127.0.0.1:3001/_media/icon.svg\\" data-origin=\\"_media/icon.svg\\" alt=\\"logo\\"></p><h1 id=\\"docsify-4130\\" tabindex=\\"-1\\"><a href=\\"#/?id=docsify-4130\\" data-id=\\"docsify-4130\\" class=\\"anchor\\"><span>docsify <small>4.13.0</small></span></a></h1><blockquote>
<p>A magical documentation site generator.</p></blockquote>
<ul><li>Simple and lightweight</li><li>No statically built html files</li><li>Multiple themes</li></ul><p><a href=\\"https://github.com/docsifyjs/docsify/\\" target=\\"_blank\\" rel=\\"noopener\\">GitHub</a>
<a href=\\"#/?id=docsify\\">Getting Started</a></p></div>
Expand Down
Loading