From 8acb22205c3dcc84404c6de8d3eb15ed9d5a1d84 Mon Sep 17 00:00:00 2001 From: Crevil Date: Thu, 28 Jul 2022 08:01:29 +0200 Subject: [PATCH] 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 --- .changeset/rich-readers-return.md | 5 ++ .../TechDocsReaderPageContent/dom.tsx | 2 + .../techdocs/src/reader/transformers/index.ts | 1 + .../transformers/scrollIntoNavigation.test.ts | 80 +++++++++++++++++++ .../transformers/scrollIntoNavigation.ts | 34 ++++++++ 5 files changed, 122 insertions(+) create mode 100644 .changeset/rich-readers-return.md create mode 100644 plugins/techdocs/src/reader/transformers/scrollIntoNavigation.test.ts create mode 100644 plugins/techdocs/src/reader/transformers/scrollIntoNavigation.ts diff --git a/.changeset/rich-readers-return.md b/.changeset/rich-readers-return.md new file mode 100644 index 0000000000..3c8f3d7b5a --- /dev/null +++ b/.changeset/rich-readers-return.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Scroll techdocs navigation into focus and expand any nested navigation items. diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx index 76b07c9d32..98bd6d867c 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx @@ -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, diff --git a/plugins/techdocs/src/reader/transformers/index.ts b/plugins/techdocs/src/reader/transformers/index.ts index dc3c43a584..ca17261572 100644 --- a/plugins/techdocs/src/reader/transformers/index.ts +++ b/plugins/techdocs/src/reader/transformers/index.ts @@ -26,4 +26,5 @@ export * from './removeMkdocsHeader'; export * from './simplifyMkdocsFooter'; export * from './onCssReady'; export * from './scrollIntoAnchor'; +export * from './scrollIntoNavigation'; export * from './transformer'; diff --git a/plugins/techdocs/src/reader/transformers/scrollIntoNavigation.test.ts b/plugins/techdocs/src/reader/transformers/scrollIntoNavigation.test.ts new file mode 100644 index 0000000000..ca8cf04b5e --- /dev/null +++ b/plugins/techdocs/src/reader/transformers/scrollIntoNavigation.test.ts @@ -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(); + }); +}); diff --git a/plugins/techdocs/src/reader/transformers/scrollIntoNavigation.ts b/plugins/techdocs/src/reader/transformers/scrollIntoNavigation.ts new file mode 100644 index 0000000000..ff6901686a --- /dev/null +++ b/plugins/techdocs/src/reader/transformers/scrollIntoNavigation.ts @@ -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; + }; +};