diff --git a/.changeset/techdocs-khaki-cheetahs-clap.md b/.changeset/techdocs-khaki-cheetahs-clap.md new file mode 100644 index 0000000000..5aba436534 --- /dev/null +++ b/.changeset/techdocs-khaki-cheetahs-clap.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-react': patch +--- + +Added overload signatures for `createTechDocsAddonExtension` to handle TechDocs addons without props. diff --git a/.changeset/techdocs-weak-boats-work.md b/.changeset/techdocs-weak-boats-work.md new file mode 100644 index 0000000000..ec4f089592 --- /dev/null +++ b/.changeset/techdocs-weak-boats-work.md @@ -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. diff --git a/docs/features/techdocs/addons.md b/docs/features/techdocs/addons.md index e8af56d3fc..5305e3bc68 100644 --- a/docs/features/techdocs/addons.md +++ b/docs/features/techdocs/addons.md @@ -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 | -| ---------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [``](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. | -| [``](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 | +| ---------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [``](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. | +| [``](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. | +| [``](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! diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 089900fb77..35c61f2f16 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -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} + diff --git a/plugins/techdocs-module-addons-contrib/api-report.md b/plugins/techdocs-module-addons-contrib/api-report.md index 19d2f70a2b..efe88e2d5c 100644 --- a/plugins/techdocs-module-addons-contrib/api-report.md +++ b/plugins/techdocs-module-addons-contrib/api-report.md @@ -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; ``` diff --git a/plugins/techdocs-module-addons-contrib/package.json b/plugins/techdocs-module-addons-contrib/package.json index 8a1801f0ba..074d12a505 100644 --- a/plugins/techdocs-module-addons-contrib/package.json +++ b/plugins/techdocs-module-addons-contrib/package.json @@ -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" }, diff --git a/plugins/techdocs-module-addons-contrib/src/ExpandableNavigation/ExpandableNavigation.test.tsx b/plugins/techdocs-module-addons-contrib/src/ExpandableNavigation/ExpandableNavigation.test.tsx new file mode 100644 index 0000000000..3efc923145 --- /dev/null +++ b/plugins/techdocs-module-addons-contrib/src/ExpandableNavigation/ExpandableNavigation.test.tsx @@ -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 = ( +
+ +
+); + +const mockNavWithoutSublevels = ( + +); + +describe('ExpandableNavigation', () => { + it('renders without exploding', async () => { + const { getByRole } = await TechDocsAddonTester.buildAddonsInTechDocs([ + , + ]) + .withDom(mockNavWithSublevels) + .renderWithEffects(); + + expect(getByRole('button', { name: 'expand-nav' })).toBeInTheDocument(); + }); + + it('expands and collapses navigation', async () => { + const { getByRole, shadowRoot } = + await TechDocsAddonTester.buildAddonsInTechDocs([ + , + ]) + .withDom(mockNavWithSublevels) + .renderWithEffects(); + + const toggles = + shadowRoot!.querySelectorAll('.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([ + , + ]) + .withDom(mockNavWithoutSublevels) + .renderWithEffects(); + + expect( + queryByRole('button', { name: 'expand-nav' }), + ).not.toBeInTheDocument(); + }); +}); diff --git a/plugins/techdocs-module-addons-contrib/src/ExpandableNavigation/ExpandableNavigation.tsx b/plugins/techdocs-module-addons-contrib/src/ExpandableNavigation/ExpandableNavigation.tsx new file mode 100644 index 0000000000..5ef76187e3 --- /dev/null +++ b/plugins/techdocs-module-addons-contrib/src/ExpandableNavigation/ExpandableNavigation.tsx @@ -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( + EXPANDABLE_NAVIGATION_LOCAL_STORAGE, + defaultValue, + ); + const [hasNavSubLevels, setHasNavSubLevels] = useState(false); + + const [...checkboxToggles] = useShadowRootElements([ + 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 ? ( + + {expanded?.expandAllNestedNavs ? : } + + ) : null} + + ); +}; diff --git a/plugins/techdocs-module-addons-contrib/src/ExpandableNavigation/index.ts b/plugins/techdocs-module-addons-contrib/src/ExpandableNavigation/index.ts new file mode 100644 index 0000000000..8f1c131d08 --- /dev/null +++ b/plugins/techdocs-module-addons-contrib/src/ExpandableNavigation/index.ts @@ -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'; diff --git a/plugins/techdocs-module-addons-contrib/src/ReportIssue/ReportIssue.test.tsx b/plugins/techdocs-module-addons-contrib/src/ReportIssue/ReportIssue.test.tsx index 68bde5c762..459fa1b65e 100644 --- a/plugins/techdocs-module-addons-contrib/src/ReportIssue/ReportIssue.test.tsx +++ b/plugins/techdocs-module-addons-contrib/src/ReportIssue/ReportIssue.test.tsx @@ -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(); diff --git a/plugins/techdocs-module-addons-contrib/src/TextSize/TextSize.test.tsx b/plugins/techdocs-module-addons-contrib/src/TextSize/TextSize.test.tsx index 282d54a0b6..6b4a055c2a 100644 --- a/plugins/techdocs-module-addons-contrib/src/TextSize/TextSize.test.tsx +++ b/plugins/techdocs-module-addons-contrib/src/TextSize/TextSize.test.tsx @@ -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 () => { diff --git a/plugins/techdocs-module-addons-contrib/src/index.ts b/plugins/techdocs-module-addons-contrib/src/index.ts index e2e57b15d1..528afe5c0d 100644 --- a/plugins/techdocs-module-addons-contrib/src/index.ts +++ b/plugins/techdocs-module-addons-contrib/src/index.ts @@ -22,6 +22,7 @@ export { techdocsModuleAddonsContribPlugin, + ExpandableNavigation, ReportIssue, TextSize, } from './plugin'; diff --git a/plugins/techdocs-module-addons-contrib/src/plugin.ts b/plugins/techdocs-module-addons-contrib/src/plugin.ts index 0d7743da9e..06fa7db07d 100644 --- a/plugins/techdocs-module-addons-contrib/src/plugin.ts +++ b/plugins/techdocs-module-addons-contrib/src/plugin.ts @@ -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 = () => { + * + * // other plugin routes + * }> + * + * + * } + * > + * + * + * + * + * ; + * }; + * ``` + * + * @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 * diff --git a/plugins/techdocs-react/api-report.md b/plugins/techdocs-react/api-report.md index e96cbebd1c..b3ee9168a6 100644 --- a/plugins/techdocs-react/api-report.md +++ b/plugins/techdocs-react/api-report.md @@ -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( options: TechDocsAddonOptions, diff --git a/plugins/techdocs-react/src/addons.tsx b/plugins/techdocs-react/src/addons.tsx index 49aa5b1bcc..ce5b3eb5cd 100644 --- a/plugins/techdocs-react/src/addons.tsx +++ b/plugins/techdocs-react/src/addons.tsx @@ -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( + options: TechDocsAddonOptions, +): Extension<(props: TComponentProps) => JSX.Element | null>; + +/** + * Create a TechDocs addon implementation. * @public */ export function createTechDocsAddonExtension(