Merge pull request #11166 from realandersn/techdocs/expandable-nav-addon

[TechDocs] Expandable Navigation addon
This commit is contained in:
Anders Näsman
2022-05-16 14:33:50 +02:00
committed by GitHub
15 changed files with 383 additions and 8 deletions
@@ -7,6 +7,9 @@
import { BackstagePlugin } from '@backstage/core-plugin-api';
// @public
export const ExpandableNavigation: () => JSX.Element | null;
// @public
export const ReportIssue: (props: ReportIssueProps) => JSX.Element | null;
@@ -33,5 +36,5 @@ export type ReportIssueTemplateBuilder = ({
export const techdocsModuleAddonsContribPlugin: BackstagePlugin<{}, {}>;
// @public
export const TextSize: (props: unknown) => JSX.Element | null;
export const TextSize: () => JSX.Element | null;
```
@@ -43,6 +43,7 @@
"@material-ui/core": "^4.9.13",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.57",
"@react-hookz/web": "^13.0.0",
"git-url-parse": "^11.6.0",
"react-use": "^17.2.4"
},
@@ -0,0 +1,146 @@
/*
* Copyright 2022 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 { TechDocsAddonTester } from '@backstage/plugin-techdocs-addons-test-utils';
import React from 'react';
import { fireEvent, waitFor } from '@testing-library/react';
import { ExpandableNavigation } from '../plugin';
const mockNavWithSublevels = (
<div data-md-component="navigation">
<nav>
<ul>
<li className="md-nav__item md-nav__item--nested">
<input
id="nav-2"
type="checkbox"
data-md-toggle="nav-2"
className="md-nav__toggle md-toggle"
/>
<ul data-md-scrollfix="" className="md-nav__list">
<li className="md-nav__item">
<a className="md-nav__link" title="First Level Nested" href="/">
First Level Nested
</a>
</li>
<li className="md-nav__item md-nav__item--nested">
<input
id="nav-2-2"
type="checkbox"
data-md-toggle="nav-2-2"
className="md-nav__toggle md-toggle"
/>
<ul data-md-scrollfix="" className="md-nav__list">
<li className="md-nav__item">
<a
className="md-nav__link"
title="Second Level Nested"
href="/"
>
Second Level Nested
</a>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</nav>
</div>
);
const mockNavWithoutSublevels = (
<div data-md-component="navigation">
<nav>
<ul>
<li className="md-nav__item">
<a className="md-nav__link" title="Nav Item" href="/">
Nav Item
</a>
</li>
<li className="md-nav__item">
<a className="md-nav__link" title="Second Nav Item" href="/">
Second Nav Item
</a>
</li>
</ul>
</nav>
</div>
);
describe('ExpandableNavigation', () => {
it('renders without exploding', async () => {
const { getByRole } = await TechDocsAddonTester.buildAddonsInTechDocs([
<ExpandableNavigation />,
])
.withDom(mockNavWithSublevels)
.renderWithEffects();
expect(getByRole('button', { name: 'expand-nav' })).toBeInTheDocument();
});
it('expands and collapses navigation', async () => {
const { getByRole, shadowRoot } =
await TechDocsAddonTester.buildAddonsInTechDocs([
<ExpandableNavigation />,
])
.withDom(mockNavWithSublevels)
.renderWithEffects();
const toggles =
shadowRoot!.querySelectorAll<HTMLInputElement>('.md-toggle');
expect(toggles).toHaveLength(2);
toggles.forEach(item => {
expect(item).not.toBeChecked();
});
const expandButton = getByRole('button', { name: 'expand-nav' });
fireEvent.click(expandButton);
await waitFor(() => {
expect(getByRole('button', { name: 'collapse-nav' })).toBeInTheDocument();
toggles.forEach(item => {
expect(item).toBeChecked();
});
});
const collapseButton = getByRole('button', { name: 'collapse-nav' });
fireEvent.click(collapseButton);
await waitFor(() => {
toggles.forEach(item => {
expect(item).not.toBeChecked();
});
});
});
it('does not render when navigation has no sublevels', async () => {
const { queryByRole } = await TechDocsAddonTester.buildAddonsInTechDocs([
<ExpandableNavigation />,
])
.withDom(mockNavWithoutSublevels)
.renderWithEffects();
expect(
queryByRole('button', { name: 'expand-nav' }),
).not.toBeInTheDocument();
});
});
@@ -0,0 +1,126 @@
/*
* Copyright 2022 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 React, { useEffect, useCallback, useState } from 'react';
import { useLocalStorageValue } from '@react-hookz/web';
import { Button, withStyles } from '@material-ui/core';
import ChevronRightIcon from '@material-ui/icons/ChevronRight';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import { useShadowRootElements } from '@backstage/plugin-techdocs-react';
const NESTED_LIST_TOGGLE = '.md-nav__item--nested .md-toggle';
const EXPANDABLE_NAVIGATION_LOCAL_STORAGE =
'@backstage/techdocs-addons/nav-expanded';
const StyledButton = withStyles({
root: {
position: 'absolute',
left: '220px',
top: '19px',
padding: 0,
minWidth: 0,
},
})(Button);
const CollapsedIcon = withStyles({
root: {
height: '20px',
width: '20px',
},
})(ChevronRightIcon);
const ExpandedIcon = withStyles({
root: {
height: '20px',
width: '20px',
},
})(ExpandMoreIcon);
type expandableNavigationLocalStorage = {
expandAllNestedNavs: boolean;
};
/**
* Show expand/collapse navigation button next to site name in main
* navigation menu if documentation site has nested navigation.
*/
export const ExpandableNavigationAddon = () => {
const defaultValue = { expandAllNestedNavs: false };
const [expanded, setExpanded] =
useLocalStorageValue<expandableNavigationLocalStorage>(
EXPANDABLE_NAVIGATION_LOCAL_STORAGE,
defaultValue,
);
const [hasNavSubLevels, setHasNavSubLevels] = useState<boolean>(false);
const [...checkboxToggles] = useShadowRootElements<HTMLInputElement>([
NESTED_LIST_TOGGLE,
]);
const shouldToggle = useCallback(
(item: HTMLInputElement) => {
const isExpanded = item.checked;
const shouldExpand = expanded?.expandAllNestedNavs;
// Is collapsed but should expand
if (shouldExpand && !isExpanded) {
return true;
}
// Is expanded but should collapse
if (!shouldExpand && isExpanded) {
return true;
}
return false;
},
[expanded],
);
useEffect(() => {
// There is no nested navs
if (!checkboxToggles?.length) return;
setHasNavSubLevels(true);
checkboxToggles.forEach(item => {
if (shouldToggle(item)) item.click();
});
}, [expanded, shouldToggle, checkboxToggles]);
const handleState = () => {
setExpanded(prevState => ({
expandAllNestedNavs: !prevState?.expandAllNestedNavs,
}));
};
return (
<>
{hasNavSubLevels ? (
<StyledButton
size="small"
onClick={handleState}
aria-label={
expanded?.expandAllNestedNavs ? 'collapse-nav' : 'expand-nav'
}
>
{expanded?.expandAllNestedNavs ? <ExpandedIcon /> : <CollapsedIcon />}
</StyledButton>
) : null}
</>
);
};
@@ -0,0 +1,17 @@
/*
* Copyright 2022 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.
*/
export * from './ExpandableNavigation';
@@ -20,7 +20,7 @@ import React from 'react';
import { fireEvent, waitFor } from '@testing-library/react';
import { scmIntegrationsApiRef } from '@backstage/integration-react';
import { ReportIssue } from '..';
import { ReportIssue } from '../plugin';
const byUrl = jest.fn();
@@ -20,7 +20,7 @@ import React from 'react';
import { fireEvent, waitFor } from '@testing-library/react';
import { TextSize } from '..';
import { TextSize } from '../plugin';
describe('TextSize', () => {
it('renders without exploding', async () => {
@@ -22,6 +22,7 @@
export {
techdocsModuleAddonsContribPlugin,
ExpandableNavigation,
ReportIssue,
TextSize,
} from './plugin';
@@ -19,6 +19,7 @@ import {
createTechDocsAddonExtension,
TechDocsAddonLocations,
} from '@backstage/plugin-techdocs-react';
import { ExpandableNavigationAddon } from './ExpandableNavigation';
import { ReportIssueAddon, ReportIssueProps } from './ReportIssue';
import { TextSizeAddon } from './TextSize';
@@ -32,6 +33,52 @@ export const techdocsModuleAddonsContribPlugin = createPlugin({
id: 'techdocsModuleAddonsContrib',
});
/**
* TechDocs addon that lets you expand/collapse the TechDocs main navigation
* and keep the preferred state in local storage. The addon will render as
* a button next to the site name if the documentation has nested navigation.
*
* @example
* Here's a simple example:
* ```
* import {
* DefaultTechDocsHome,
* TechDocsIndexPage,
* TechDocsReaderPage,
* } from '@backstage/plugin-techdocs';
* import { TechDocsAddons } from '@backstage/plugin-techdocs-react/alpha';
* import { ExpandableNavigation } from '@backstage/plugin-techdocs-module-addons-contrib';
*
*
* const AppRoutes = () => {
* <FlatRoutes>
* // other plugin routes
* <Route path="/docs" element={<TechDocsIndexPage />}>
* <DefaultTechDocsHome />
* </Route>
* <Route
* path="/docs/:namespace/:kind/:name/*"
* element={<TechDocsReaderPage />}
* >
* <TechDocsAddons>
* <ExpandableNavigation />
* </TechDocsAddons>
* </Route>
* </FlatRoutes>;
* };
* ```
*
* @public
*/
export const ExpandableNavigation = techdocsModuleAddonsContribPlugin.provide(
createTechDocsAddonExtension({
name: 'ExpandableNavigation',
location: TechDocsAddonLocations.PrimarySidebar,
component: ExpandableNavigationAddon,
}),
);
/**
* TechDocs addon that lets you select text and open GitHub/Gitlab issues
*
+5
View File
@@ -14,6 +14,11 @@ import { default as React_2 } from 'react';
import { ReactNode } from 'react';
import { SetStateAction } from 'react';
// @public
export function createTechDocsAddonExtension(
options: TechDocsAddonOptions,
): Extension<() => JSX.Element | null>;
// @public
export function createTechDocsAddonExtension<TComponentProps>(
options: TechDocsAddonOptions<TComponentProps>,
+17 -1
View File
@@ -48,7 +48,23 @@ const getDataKeyByName = (name: string) => {
};
/**
* Create a TechDocs addon.
* Create a TechDocs addon overload signature without props.
* @public
*/
export function createTechDocsAddonExtension(
options: TechDocsAddonOptions,
): Extension<() => JSX.Element | null>;
/**
* Create a TechDocs addon overload signature with props.
* @public
*/
export function createTechDocsAddonExtension<TComponentProps>(
options: TechDocsAddonOptions<TComponentProps>,
): Extension<(props: TComponentProps) => JSX.Element | null>;
/**
* Create a TechDocs addon implementation.
* @public
*/
export function createTechDocsAddonExtension<TComponentProps>(