Improve documentation

- Fix tests

Signed-off-by: Philipp Hugenroth <philipph@spotify.com>
This commit is contained in:
Philipp Hugenroth
2021-12-29 18:05:17 +01:00
parent 858b6e40c9
commit 02338a6515
15 changed files with 221 additions and 86 deletions
+24 -13
View File
@@ -700,11 +700,20 @@ export function MissingAnnotationEmptyState(props: Props_3): JSX.Element;
// @public (undocumented)
export type MissingAnnotationEmptyStateClassKey = 'code';
// @public (undocumented)
// @public
export const MobileSidebar: (
props: React_2.PropsWithChildren<{}>,
) => JSX.Element | null;
// @public
export const MobileSidebarContext: React_2.Context<MobileSidebarContextType>;
// @public
export type MobileSidebarContextType = {
selectedMenuItemIndex: number;
setSelectedMenuItemIndex: React_2.Dispatch<React_2.SetStateAction<number>>;
};
// Warning: (ae-missing-release-tag) "OAuthRequestDialog" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -844,7 +853,7 @@ export type SelectItem = {
value: string | number;
};
// @public (undocumented)
// @public
export const Sidebar: (
props: React_2.PropsWithChildren<SidebarProps>,
) => JSX.Element;
@@ -879,12 +888,12 @@ export const sidebarConfig: {
// Warning: (ae-missing-release-tag) "SidebarContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @public
export const SidebarContext: Context<SidebarContextType>;
// Warning: (ae-missing-release-tag) "SidebarContextType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @public
export type SidebarContextType = {
isOpen: boolean;
setOpen?: (open: boolean) => void;
@@ -1167,16 +1176,14 @@ export type SidebarDividerClassKey = 'root';
// @public
export const SidebarExpandButton: () => JSX.Element | null;
// @public (undocumented)
// @public
export const SidebarGroup: (
props: React_2.PropsWithChildren<SidebarGroupProps>,
) => JSX.Element;
// @public (undocumented)
// @public
export interface SidebarGroupProps extends BottomNavigationActionProps {
// (undocumented)
priority?: number;
// (undocumented)
to?: string;
}
@@ -1196,19 +1203,19 @@ export type SidebarIntroClassKey =
// Warning: (ae-forgotten-export) The symbol "SidebarItemProps" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "SidebarItem" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @public
export const SidebarItem: React_2.ForwardRefExoticComponent<
SidebarItemProps & React_2.RefAttributes<any>
>;
// Warning: (ae-missing-release-tag) "SidebarItemClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type SidebarItemClassKey =
| 'root'
| 'buttonItem'
| 'closed'
| 'open'
| 'highlightable'
| 'highlighted'
| 'label'
| 'iconContainer'
| 'searchRoot'
@@ -1216,6 +1223,10 @@ export type SidebarItemClassKey =
| 'searchFieldHTMLInput'
| 'searchContainer'
| 'secondaryAction'
| 'closedItemIcon'
| 'submenuArrow'
| 'expandButton'
| 'arrows'
| 'selected';
// Warning: (ae-missing-release-tag) "SidebarPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@@ -2058,10 +2069,10 @@ export const SidebarSpacer: React_2.ComponentType<
// @public (undocumented)
export type SidebarSpacerClassKey = 'root';
// @public (undocumented)
// @public
export const SidebarStateContext: React_2.Context<SidebarStateContextType>;
// @public (undocumented)
// @public
export type SidebarStateContextType = {
isPinned: boolean;
toggleSidebarPinState: () => any;
@@ -14,9 +14,9 @@
* limitations under the License.
*/
import React, { PropsWithChildren } from 'react';
import { BackstageTheme } from '@backstage/theme';
import { makeStyles, ThemeProvider } from '@material-ui/core/styles';
import React, { PropsWithChildren } from 'react';
export type PageClassKey = 'root';
@@ -14,19 +14,23 @@
* limitations under the License.
*/
import React from 'react';
import { renderInTestApp } from '@backstage/test-utils';
import AcUnitIcon from '@material-ui/icons/AcUnit';
import CreateComponentIcon from '@material-ui/icons/AddCircleOutline';
import BuildRoundedIcon from '@material-ui/icons/BuildRounded';
import MenuBookIcon from '@material-ui/icons/MenuBook';
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import BuildRoundedIcon from '@material-ui/icons/BuildRounded';
import CreateComponentIcon from '@material-ui/icons/AddCircleOutline';
import MenuBookIcon from '@material-ui/icons/MenuBook';
import AcUnitIcon from '@material-ui/icons/AcUnit';
import { Sidebar } from './Bar';
import { SidebarItem, SidebarSearchField, SidebarExpandButton } from './Items';
import { SidebarSubmenuItem } from './SidebarSubmenuItem';
import { SidebarSubmenu } from './SidebarSubmenu';
import { SidebarStateContext } from '.';
import React from 'react';
import {
Sidebar,
SidebarExpandButton,
SidebarItem,
SidebarSearchField,
SidebarStateContext,
SidebarSubmenu,
SidebarSubmenuItem,
} from '.';
async function renderScalableSidebar() {
await renderInTestApp(
@@ -37,7 +41,7 @@ async function renderScalableSidebar() {
toggleSidebarPinState: () => {},
}}
>
<Sidebar>
<Sidebar disableExpandOnHover>
<SidebarSearchField onSearch={() => {}} to="/search" />
<SidebarItem icon={MenuBookIcon} onClick={() => {}} text="Catalog">
<SidebarSubmenu title="Catalog">
@@ -36,7 +36,7 @@ const useStyles = makeStyles<BackstageTheme>(
left: 0,
top: 0,
bottom: 0,
zIndex: 1000,
zIndex: theme.zIndex.appBar,
background: theme.palette.navigation.background,
overflowX: 'hidden',
msOverflowStyle: 'none',
@@ -77,12 +77,23 @@ export type SidebarProps = {
disableExpandOnHover?: boolean;
};
const DesktopSidebar = ({
openDelayMs = sidebarConfig.defaultOpenDelayMs,
closeDelayMs = sidebarConfig.defaultCloseDelayMs,
disableExpandOnHover,
children,
}: PropsWithChildren<SidebarProps>) => {
/**
* Places the Sidebar & wraps the children providing context weather the `Sidebar` is open or not.
*
* Handles & delays hover events for expanding the `Sidebar`
*
* @param props `disableExpandOnHover` disables the default hover behaviour;
* `openDelayMs` & `closeDelayMs` set delay until sidebar will open/close on hover
* @returns
* @internal
*/
const DesktopSidebar = (props: PropsWithChildren<SidebarProps>) => {
const {
openDelayMs = sidebarConfig.defaultOpenDelayMs,
closeDelayMs = sidebarConfig.defaultCloseDelayMs,
disableExpandOnHover,
children,
} = props;
const classes = useStyles();
const isSmallScreen = useMediaQuery<BackstageTheme>(
theme => theme.breakpoints.down('md'),
@@ -130,6 +141,9 @@ const DesktopSidebar = ({
const isOpen = (state === State.Open && !isSmallScreen) || isPinned;
/**
* Close/Open Sidebar directily without delays. Also toggles `SidebarPinState` to avoid hidden content behind Sidebar.
*/
const setOpen = (open: boolean) => {
if (open) {
setState(State.Open);
@@ -163,7 +177,11 @@ const DesktopSidebar = ({
);
};
/** @public */
/**
* Passing children into the desktop or mobile sidebar depending on the context
*
* @public
*/
export const Sidebar = (props: React.PropsWithChildren<SidebarProps>) => {
const { children, openDelayMs, closeDelayMs, disableExpandOnHover } = props;
const { isMobile } = useContext(SidebarStateContext);
@@ -37,9 +37,9 @@ import React, {
import {
Link,
NavLinkProps,
resolvePath,
useLocation,
useResolvedPath,
resolvePath,
} from 'react-router-dom';
import {
sidebarConfig,
@@ -56,11 +56,14 @@ import DoubleArrowRight from './icons/DoubleArrowRight';
import { isLocationMatch } from './utils';
import { Location } from 'history';
/** @public */
export type SidebarItemClassKey =
| 'root'
| 'buttonItem'
| 'closed'
| 'open'
| 'highlightable'
| 'highlighted'
| 'label'
| 'iconContainer'
| 'searchRoot'
@@ -68,6 +71,10 @@ export type SidebarItemClassKey =
| 'searchFieldHTMLInput'
| 'searchContainer'
| 'secondaryAction'
| 'closedItemIcon'
| 'submenuArrow'
| 'expandButton'
| 'arrows'
| 'selected';
const useStyles = makeStyles<BackstageTheme>(
@@ -238,12 +245,18 @@ const useStyles = makeStyles<BackstageTheme>(
{ name: 'BackstageSidebarItem' },
);
/**
* Evaluates the routes of the SubmenuItems & nested DropdownItems.
* The reeveluation is only triggered, if the `locationPathname` changes, as `useElementFilter` uses memorization.
*
* @param submenu SidebarSubmenu component
* @param location Location
* @returns boolean
*/
const useLocationMatch = (
submenu: React.ReactElement<SidebarSubmenuProps>,
location: Location,
): boolean =>
// Evaluates the routes of the SubmenuItems & nested DropdownItems.
// The reeveluation is only triggered, if the `locationPathname` changes, as `useElementFilter` uses memorization
useElementFilter(
submenu.props.children,
elements => {
@@ -367,6 +380,9 @@ export const WorkaroundNavLink = React.forwardRef<
);
});
/**
* Common component used by SidebarItem & SidebarItemWithSubmenu
*/
const SidebarItemBase = forwardRef<any, SidebarItemProps>((props, ref) => {
const {
icon: Icon,
@@ -507,6 +523,11 @@ const SidebarItemWithSubmenu = ({
);
};
/**
* Creates a `SidebarItem`
*
* If children contain a `SidebarSubmenu` component the `SidebarItem` will have a expandable submenu
*/
export const SidebarItem = forwardRef<any, SidebarItemProps>((props, ref) => {
// Filter children for SidebarSubmenu components
const [submenu] = useElementFilter(props.children, elements =>
@@ -655,6 +676,8 @@ export const SidebarScrollWrapper = styled('div')(({ theme }) => {
* A button which allows you to expand the sidebar when clicked.
* Use optionally to replace sidebar's expand-on-hover feature with expand-on-click.
*
* If you are using this you might want to set the `disableExpandOnHover` of the `Sidebar` to `true`.
*
* @public
*/
export const SidebarExpandButton = () => {
@@ -21,32 +21,39 @@ import LayersIcon from '@material-ui/icons/Layers';
import LibraryBooks from '@material-ui/icons/LibraryBooks';
import { fireEvent } from '@testing-library/react';
import React from 'react';
import { Sidebar } from './Bar';
import { SidebarItem } from './Items';
import { MobileSidebar } from './MobileSidebar';
import { SidebarGroup } from './SidebarGroup';
import {
MobileSidebar,
Sidebar,
SidebarGroup,
SidebarItem,
SidebarPage,
} from '.';
const MobileSidebarWithGroups = () => (
<MobileSidebar>
<h1>Header</h1>
<SidebarGroup icon={<HomeIcon />} label="Menu">
<SidebarItem icon={HomeIcon} to="/" text="Home" />
<SidebarItem icon={LayersIcon} to="/" text="Explore" />
<SidebarItem icon={LibraryBooks} to="/" text="Docs" />
</SidebarGroup>
<div>Content</div>
<div>More Content</div>
<SidebarGroup icon={<CreateComponentIcon />} label="Create" to="#" />
<footer>Footer</footer>
</MobileSidebar>
<SidebarPage>
<MobileSidebar>
<h1>Header</h1>
<SidebarGroup icon={<HomeIcon />} label="Menu">
<SidebarItem icon={HomeIcon} to="/" text="Home" />
<SidebarItem icon={LayersIcon} to="/" text="Explore" />
<SidebarItem icon={LibraryBooks} to="/" text="Docs" />
</SidebarGroup>
<div>Content</div>
<div>More Content</div>
<SidebarGroup icon={<CreateComponentIcon />} label="Create" to="#" />
<footer>Footer</footer>
</MobileSidebar>
</SidebarPage>
);
const MobileSidebarWithoutGroups = () => (
<MobileSidebar>
<SidebarItem icon={HomeIcon} to="/one" text="Home" />
<SidebarItem icon={LayersIcon} to="/two" text="Explore" />
<SidebarItem icon={LibraryBooks} to="/three" text="Docs" />
</MobileSidebar>
<SidebarPage>
<MobileSidebar>
<SidebarItem icon={HomeIcon} to="/one" text="Home" />
<SidebarItem icon={LayersIcon} to="/two" text="Explore" />
<SidebarItem icon={LibraryBooks} to="/three" text="Docs" />
</MobileSidebar>
</SidebarPage>
);
describe('<MobileSidebar />', () => {
@@ -56,9 +63,11 @@ describe('<MobileSidebar />', () => {
it('should render MobileSidebar on smaller screens', async () => {
const { getByTestId } = await renderInTestApp(
<Sidebar>
<SidebarItem icon={HomeIcon} to="/one" text="Home" />
</Sidebar>,
<SidebarPage>
<Sidebar>
<SidebarItem icon={HomeIcon} to="/one" text="Home" />
</Sidebar>
</SidebarPage>,
);
expect(getByTestId('mobile-sidebar-root')).toBeVisible();
});
@@ -31,7 +31,12 @@ import { SidebarContext } from '.';
import { sidebarConfig } from './config';
import { SidebarGroup } from './SidebarGroup';
type MobileSidebarContextType = {
/**
* Type of `MobileSidebarContext`
*
* @public
*/
export type MobileSidebarContextType = {
selectedMenuItemIndex: number;
setSelectedMenuItemIndex: React.Dispatch<React.SetStateAction<number>>;
};
@@ -111,12 +116,25 @@ const OverlayMenu = ({
);
};
/**
* Context on which `SidebarGroup` is currently selected
*
* @public
*/
export const MobileSidebarContext = createContext<MobileSidebarContextType>({
selectedMenuItemIndex: -1,
setSelectedMenuItemIndex: () => {},
});
/** @public */
/**
* A navigation component for mobile screens, which sticks to the bottom.
*
* It alternates the normal sidebar by grouping the `SidebarItems` based on provided `SidebarGroups`
* either rendering them as a link or an overlay menu.
* If no `SidebarGroups` are provided the sidebar content is wrapped in an default overlay menu.
*
* @public
*/
export const MobileSidebar = (props: React.PropsWithChildren<{}>) => {
const { children } = props;
const classes = useStyles();
@@ -160,9 +178,8 @@ export const MobileSidebar = (props: React.PropsWithChildren<{}>) => {
>
<OverlayMenu
label={
sidebarGroups[selectedMenuItemIndex]
? (sidebarGroups[selectedMenuItemIndex].props.label as string)
: ''
sidebarGroups[selectedMenuItemIndex] &&
(sidebarGroups[selectedMenuItemIndex]!.props.label as string)
}
open={shouldShowGroupChildren}
onClose={() => setSelectedMenuItemIndex(-1)}
@@ -47,14 +47,22 @@ const useStyles = makeStyles<BackstageTheme, { isPinned: boolean }>(
{ name: 'BackstageSidebarPage' },
);
/** @public */
/**
* Type of `SidebarStateContext`
*
* @public
*/
export type SidebarStateContextType = {
isPinned: boolean;
toggleSidebarPinState: () => any;
isMobile: boolean;
};
/** @public */
/**
* Contains the state on how the `Sidebar` is rendered
*
* @public
*/
export const SidebarStateContext = createContext<SidebarStateContextType>({
isPinned: true,
toggleSidebarPinState: () => {},
@@ -20,32 +20,42 @@ import LayersIcon from '@material-ui/icons/Layers';
import LibraryBooks from '@material-ui/icons/LibraryBooks';
import { fireEvent } from '@testing-library/react';
import React from 'react';
import { SidebarItem } from './Items';
import { MobileSidebarContext } from './MobileSidebar';
import { SidebarGroup } from './SidebarGroup';
import {
MobileSidebarContext,
SidebarGroup,
SidebarItem,
SidebarPage,
} from '.';
const SidebarGroupWithItems = () => (
<SidebarGroup icon={<HomeIcon />} label="Menu">
<SidebarItem icon={HomeIcon} to="/one" text="Home" />
<SidebarItem icon={LayersIcon} to="/two" text="Explore" />
<SidebarItem icon={LibraryBooks} to="/three" text="Docs" />
</SidebarGroup>
<SidebarPage>
<SidebarGroup icon={<HomeIcon />} label="Menu">
<SidebarItem icon={HomeIcon} to="/one" text="Home" />
<SidebarItem icon={LayersIcon} to="/two" text="Explore" />
<SidebarItem icon={LibraryBooks} to="/three" text="Docs" />
</SidebarGroup>
</SidebarPage>
);
describe('<SidebarGroup />', () => {
it('should render Items in BottomNavigationAciton on small screens', async () => {
it('should render Items in BottomNavigationAction on small screens', async () => {
mockBreakpoint({ matches: true });
const { findByRole, container } = await renderInTestApp(
const { getByRole, getAllByRole } = await renderInTestApp(
<SidebarGroupWithItems />,
);
expect(container.childNodes.length).toBe(1);
expect(await findByRole('button')).toBeVisible();
expect(getAllByRole('button').length).toEqual(1);
expect(getByRole('button')).toBeVisible();
});
it('should render Items without wrapper on bigger screens', async () => {
mockBreakpoint({ matches: false });
const { container } = await renderInTestApp(<SidebarGroupWithItems />);
expect(container.childNodes.length).toBe(3);
const { getByRole, queryByRole } = await renderInTestApp(
<SidebarGroupWithItems />,
);
expect(queryByRole('button')).not.toBeInTheDocument();
expect(getByRole('link', { name: 'Home' })).toBeVisible();
expect(getByRole('link', { name: 'Explore' })).toBeVisible();
expect(getByRole('link', { name: 'Docs' })).toBeVisible();
});
it('should trigger update of MobileSidebarContext', async () => {
@@ -27,9 +27,20 @@ import { Link } from '../../components';
import { sidebarConfig } from './config';
import { MobileSidebarContext } from './MobileSidebar';
/** @public */
/**
* Props for the `SidebarGroup`
*
* @public
*/
export interface SidebarGroupProps extends BottomNavigationActionProps {
/**
* If the `SidebarGroup` should be a `Link`, `to` should be a pathname to that location
*/
to?: string;
/**
* If the `SidebarGroups` should be in a different order than in the normal `Sidebar`, you can provide
* each `SidebarGroup` it's own priority to reorder them.
*/
priority?: number;
}
@@ -51,6 +62,13 @@ const useStyles = makeStyles<BackstageTheme>(theme => ({
},
}));
/**
* Returns a MUI `BottomNavigationAction`, which is aware of the current location & the selected item in the `BottomNavigation`,
* such that it will highlight a `MobileSidebarGroup` either on location change or if the selected item changes.
*
* @param props `to`: pathname of link; `value`: index of the selected item
* @internal
*/
const MobileSidebarGroup = (props: SidebarGroupProps) => {
const { to, label, icon, value } = props;
const classes = useStyles();
@@ -88,7 +106,14 @@ const MobileSidebarGroup = (props: SidebarGroupProps) => {
);
};
/** @public */
/**
* Groups items of the `Sidebar` together.
*
* On bigger screens, this won't have any effect at the moment.
* On small screens, it will add an action to the bottom navigation - either triggering an overlay menu or acting as a link
*
* @public
*/
export const SidebarGroup = (
props: React.PropsWithChildren<SidebarGroupProps>,
) => {
@@ -122,7 +122,6 @@ export const SidebarSubmenuItem = (props: SidebarSubmenuItemProps) => {
const { title, to, icon: Icon, dropdownItems } = props;
const classes = useStyles();
const { setIsHoveredOn } = useContext(SidebarItemWithSubmenuContext);
const closeSubmenu = () => {
setIsHoveredOn(false);
};
@@ -58,11 +58,17 @@ export const submenuConfig = {
export const SIDEBAR_INTRO_LOCAL_STORAGE =
'@backstage/core/sidebar-intro-dismissed';
/**
* Types for the `SidebarContext`
*/
export type SidebarContextType = {
isOpen: boolean;
setOpen?: (open: boolean) => void;
};
/**
* Context wether the `Sidebar` is open
*/
export const SidebarContext = createContext<SidebarContextType>({
isOpen: false,
});
@@ -15,7 +15,8 @@
*/
export { Sidebar } from './Bar';
export { MobileSidebar } from './MobileSidebar';
export { MobileSidebar, MobileSidebarContext } from './MobileSidebar';
export type { MobileSidebarContextType } from './MobileSidebar';
export { SidebarGroup } from './SidebarGroup';
export type { SidebarGroupProps } from './SidebarGroup';
export { SidebarSubmenuItem } from './SidebarSubmenuItem';
@@ -87,6 +87,8 @@ import {
SidebarSpacerClassKey,
SidebarDividerClassKey,
SidebarIntroClassKey,
SidebarItemClassKey,
SidebarPageClassKey,
CustomProviderClassKey,
SignInPageClassKey,
TabbedCardClassKey,
@@ -162,6 +164,8 @@ type BackstageComponentsNameToClassKey = {
BackstageSidebarSpacer: SidebarSpacerClassKey;
BackstageSidebarDivider: SidebarDividerClassKey;
BackstageSidebarIntro: SidebarIntroClassKey;
BackstageSidebarItem: SidebarItemClassKey;
BackstageSidebarPage: SidebarPageClassKey;
BackstageCustomProvider: CustomProviderClassKey;
BackstageSignInPage: SignInPageClassKey;
BackstageTabbedCard: TabbedCardClassKey;
@@ -20,7 +20,7 @@ import { UserSettingsPinToggle } from './UserSettingsPinToggle';
import { UserSettingsThemeToggle } from './UserSettingsThemeToggle';
export const UserSettingsAppearanceCard = () => {
const isMobile = useContext(SidebarStateContext);
const { isMobile } = useContext(SidebarStateContext);
return (
<InfoCard title="Appearance" variant="gridItem">