Merge pull request #11166 from realandersn/techdocs/expandable-nav-addon
[TechDocs] Expandable Navigation addon
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-techdocs-react': patch
|
||||
---
|
||||
|
||||
Added overload signatures for `createTechDocsAddonExtension` to handle TechDocs addons without props.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-techdocs-module-addons-contrib': patch
|
||||
---
|
||||
|
||||
Introducing the Expandable Navigation addon, which lets you expand and collapse the TechDocs main navigation and store your preference in local storage.
|
||||
@@ -111,10 +111,11 @@ page header, TechDocs Addons whose location is `Header` will not be rendered.
|
||||
Addons can, in principle, be provided by any plugin! To make it easier to
|
||||
discover available Addons, we've compiled a list of them here:
|
||||
|
||||
| Addon | Package/Plugin | Description |
|
||||
| ---------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [`<ReportIssue />`](https://backstage.io/docs/reference/plugin-techdocs-module-addons-contrib.reportissue) | `@backstage/plugin-techdocs-module-addons-contrib` | Allows TechDocs users to select a portion of text on a TechDocs page and open an issue against the repository that contains the documentation, populating the issue description with the selected text according to a configurable template. |
|
||||
| [`<TextSize />`](https://backstage.io/docs/reference/plugin-techdocs-module-addons-contrib.textsize) | `@backstage/plugin-techdocs-module-addons-contrib` | This TechDocs addon allows users to customize text size on documentation pages, they can select how much they want to increase or decrease the font size via slider or buttons. The default value for font size is 100% and this setting is kept in the browser's local storage whenever it is changed. |
|
||||
| Addon | Package/Plugin | Description |
|
||||
| ---------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [`<ExpandableNavigation />`](https://backstage.io/docs/reference/plugin-techdocs-module-addons-contrib.expandablenavigation) | `@backstage/plugin-techdocs-module-addons-contrib` | Allows TechDocs users to expand or collapse the entire TechDocs main navigation, and keeps the user's preferred state between documentation sites. |
|
||||
| [`<ReportIssue />`](https://backstage.io/docs/reference/plugin-techdocs-module-addons-contrib.reportissue) | `@backstage/plugin-techdocs-module-addons-contrib` | Allows TechDocs users to select a portion of text on a TechDocs page and open an issue against the repository that contains the documentation, populating the issue description with the selected text according to a configurable template. |
|
||||
| [`<TextSize />`](https://backstage.io/docs/reference/plugin-techdocs-module-addons-contrib.textsize) | `@backstage/plugin-techdocs-module-addons-contrib` | This TechDocs addon allows users to customize text size on documentation pages, they can select how much they want to increase or decrease the font size via slider or buttons. The default value for font size is 100% and this setting is kept in the browser's local storage whenever it is changed. |
|
||||
|
||||
Got an Addon to contribute? Feel free to add a row above!
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@ import {
|
||||
} from '@backstage/plugin-techdocs';
|
||||
import { TechDocsAddons } from '@backstage/plugin-techdocs-react';
|
||||
import {
|
||||
ExpandableNavigation,
|
||||
ReportIssue,
|
||||
TextSize,
|
||||
} from '@backstage/plugin-techdocs-module-addons-contrib';
|
||||
@@ -185,6 +186,7 @@ const routes = (
|
||||
>
|
||||
{techDocsPage}
|
||||
<TechDocsAddons>
|
||||
<ExpandableNavigation />
|
||||
<ReportIssue />
|
||||
<TextSize />
|
||||
</TechDocsAddons>
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
+146
@@ -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();
|
||||
});
|
||||
});
|
||||
+126
@@ -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
|
||||
*
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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>(
|
||||
|
||||
Reference in New Issue
Block a user