Add navigation scroll to techdocs

Currently the active navigation item might be hidden behind nested items or out
of view on load.

This change adds a techdocs transformer that scrolls any active item into view
and expands any nested active items.

Signed-off-by: Crevil <bjoern.soerensen@gmail.com>
This commit is contained in:
Crevil
2022-07-28 08:01:29 +02:00
parent 22a53de84e
commit 8acb22205c
5 changed files with 122 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-techdocs': patch
---
Scroll techdocs navigation into focus and expand any nested navigation items.
@@ -41,6 +41,7 @@ import {
rewriteDocLinks,
simplifyMkdocsFooter,
scrollIntoAnchor,
scrollIntoNavigation,
transform as transformer,
copyToClipboard,
useSanitizerTransformer,
@@ -166,6 +167,7 @@ export const useTechDocsReaderDom = (
async (transformedElement: Element) =>
transformer(transformedElement, [
scrollIntoAnchor(),
scrollIntoNavigation(),
copyToClipboard(theme),
addLinkClickListener({
baseUrl: window.location.origin,
@@ -26,4 +26,5 @@ export * from './removeMkdocsHeader';
export * from './simplifyMkdocsFooter';
export * from './onCssReady';
export * from './scrollIntoAnchor';
export * from './scrollIntoNavigation';
export * from './transformer';
@@ -0,0 +1,80 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { scrollIntoNavigation } from '.';
jest.useFakeTimers();
describe('scrollIntoNavigation', () => {
const transformer = scrollIntoNavigation();
const dom = { querySelectorAll: jest.fn().mockReturnValue([]) };
afterEach(() => {
jest.clearAllMocks();
});
it('scroll to active navigation item', async () => {
const scrollNavIntoView1 = jest.fn();
const scrollNavIntoView2 = jest.fn();
dom.querySelectorAll.mockReturnValue([
{
scrollIntoView: scrollNavIntoView1,
querySelector: jest.fn(),
click: jest.fn(),
},
{
scrollIntoView: scrollNavIntoView2,
querySelector: jest.fn(),
click: jest.fn(),
},
]);
transformer(dom as unknown as Element);
jest.advanceTimersByTime(200);
expect(dom.querySelectorAll).toHaveBeenCalledWith(
expect.stringMatching('li.md-nav__item--active'),
);
expect(scrollNavIntoView1).not.toHaveBeenCalled();
expect(scrollNavIntoView2).toHaveBeenCalledWith();
});
it('expand active navigation items', async () => {
const navItemClick1 = jest.fn();
const navItemClick2 = jest.fn();
dom.querySelectorAll.mockReturnValue([
{
scrollIntoView: jest.fn(),
querySelector: jest.fn().mockReturnValue({ click: navItemClick1 }),
},
{
scrollIntoView: jest.fn(),
querySelector: jest.fn().mockReturnValue({ click: navItemClick2 }),
},
]);
transformer(dom as unknown as Element);
jest.advanceTimersByTime(200);
expect(dom.querySelectorAll).toHaveBeenCalledWith(
expect.stringMatching('li.md-nav__item--active'),
);
expect(navItemClick1).toHaveBeenCalledWith();
expect(navItemClick2).toHaveBeenCalledWith();
});
});
@@ -0,0 +1,34 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Transformer } from './transformer';
export const scrollIntoNavigation = (): Transformer => {
return dom => {
setTimeout(() => {
const activeNavItems = dom?.querySelectorAll(`li.md-nav__item--active`);
if (activeNavItems.length !== 0) {
// expand all navigation items that are active
activeNavItems.forEach(activeNavItem => {
activeNavItem?.querySelector('input')?.click();
});
// scroll to the last active navigation item
activeNavItems[activeNavItems.length - 1].scrollIntoView();
}
}, 200);
return dom;
};
};