Implement a grouping for mobile sidebar

Signed-off-by: Philipp Hugenroth <philipph@spotify.com>
This commit is contained in:
Philipp Hugenroth
2021-08-16 11:23:56 +02:00
parent b7c1a5a040
commit 772995ac5e
7 changed files with 224 additions and 129 deletions
+82 -58
View File
@@ -14,35 +14,44 @@
* limitations under the License.
*/
import React, { useContext, PropsWithChildren } from 'react';
import { Link, makeStyles } from '@material-ui/core';
import HomeIcon from '@material-ui/icons/Home';
import ExtensionIcon from '@material-ui/icons/Extension';
import RuleIcon from '@material-ui/icons/AssignmentTurnedIn';
import MapIcon from '@material-ui/icons/MyLocation';
import LayersIcon from '@material-ui/icons/Layers';
import LibraryBooks from '@material-ui/icons/LibraryBooks';
import CreateComponentIcon from '@material-ui/icons/AddCircleOutline';
import MoneyIcon from '@material-ui/icons/MonetizationOn';
import LogoFull from './LogoFull';
import LogoIcon from './LogoIcon';
import { NavLink } from 'react-router-dom';
import {
MobileSidebar,
sidebarConfig,
SidebarContext,
SidebarDivider,
SidebarGroup,
SidebarItem,
SidebarPage,
SidebarScrollWrapper,
SidebarSpace,
} from '@backstage/core-components';
import { GraphiQLIcon } from '@backstage/plugin-graphiql';
import { Settings as SidebarSettings } from '@backstage/plugin-user-settings';
import { SidebarSearch } from '@backstage/plugin-search';
import { Shortcuts } from '@backstage/plugin-shortcuts';
import {
Sidebar,
SidebarPage,
sidebarConfig,
SidebarContext,
SidebarItem,
SidebarDivider,
SidebarSpace,
SidebarScrollWrapper,
Settings as SidebarSettings,
UserSettingsSignInAvatar,
} from '@backstage/plugin-user-settings';
import {
BottomNavigation,
SidebarGroup,
} from '@backstage/core-components';
BottomNavigationAction,
Link,
makeStyles,
} from '@material-ui/core';
import CreateComponentIcon from '@material-ui/icons/AddCircleOutline';
import RuleIcon from '@material-ui/icons/AssignmentTurnedIn';
import ExtensionIcon from '@material-ui/icons/Extension';
import HomeIcon from '@material-ui/icons/Home';
import LayersIcon from '@material-ui/icons/Layers';
import LibraryBooks from '@material-ui/icons/LibraryBooks';
import MenuIcon from '@material-ui/icons/Menu';
import MoneyIcon from '@material-ui/icons/MonetizationOn';
import MapIcon from '@material-ui/icons/MyLocation';
import SearchIcon from '@material-ui/icons/Search';
import React, { PropsWithChildren, useContext, useState } from 'react';
import { NavLink } from 'react-router-dom';
import LogoFull from './LogoFull';
import LogoIcon from './LogoIcon';
const useSidebarLogoStyles = makeStyles({
root: {
@@ -77,37 +86,52 @@ const SidebarLogo = () => {
);
};
export const Root = ({ children }: PropsWithChildren<{}>) => (
<SidebarPage>
<Sidebar>
<SidebarLogo />
<SidebarSearch />
<SidebarDivider />
{/* Global nav, not org-specific */}
<SidebarItem icon={HomeIcon} to="catalog" text="Home" />
<SidebarItem icon={ExtensionIcon} to="api-docs" text="APIs" />
<SidebarItem icon={LibraryBooks} to="docs" text="Docs" />
<SidebarItem icon={LayersIcon} to="explore" text="Explore" />
<SidebarItem icon={CreateComponentIcon} to="create" text="Create..." />
{/* End global nav */}
<SidebarDivider />
<SidebarScrollWrapper>
<SidebarItem icon={MapIcon} to="tech-radar" text="Tech Radar" />
<SidebarItem icon={RuleIcon} to="lighthouse" text="Lighthouse" />
<SidebarItem icon={MoneyIcon} to="cost-insights" text="Cost Insights" />
<SidebarItem icon={GraphiQLIcon} to="graphiql" text="GraphiQL" />
</SidebarScrollWrapper>
<SidebarDivider />
<Shortcuts />
<SidebarSpace />
<SidebarDivider />
<SidebarSettings />
</Sidebar>
<BottomNavigation>
<SidebarGroup />
<SidebarGroup />
<SidebarGroup />
</BottomNavigation>
{children}
</SidebarPage>
);
export const Root = ({ children }: PropsWithChildren<{}>) => {
return (
<SidebarPage>
<MobileSidebar>
<SidebarLogo />
<SidebarGroup label="Search" icon={<SearchIcon />} to="/search">
<SidebarSearch />
</SidebarGroup>
<SidebarDivider />
{/* Global nav, not org-specific */}
<SidebarGroup label="Menu" icon={<MenuIcon />}>
<SidebarItem icon={HomeIcon} to="catalog" text="Home" />
<SidebarItem icon={ExtensionIcon} to="api-docs" text="APIs" />
<SidebarItem icon={LibraryBooks} to="docs" text="Docs" />
<SidebarItem icon={LayersIcon} to="explore" text="Explore" />
<SidebarItem
icon={CreateComponentIcon}
to="create"
text="Create..."
/>
{/* End global nav */}
<SidebarDivider />
<SidebarScrollWrapper>
<SidebarItem icon={MapIcon} to="tech-radar" text="Tech Radar" />
<SidebarItem icon={RuleIcon} to="lighthouse" text="Lighthouse" />
<SidebarItem
icon={MoneyIcon}
to="cost-insights"
text="Cost Insights"
/>
<SidebarItem icon={GraphiQLIcon} to="graphiql" text="GraphiQL" />
</SidebarScrollWrapper>
<SidebarDivider />
<Shortcuts />
</SidebarGroup>
<SidebarSpace />
<SidebarDivider />
<SidebarGroup
label="Settings"
icon={<UserSettingsSignInAvatar />}
to="/settings"
>
<SidebarSettings />
</SidebarGroup>
</MobileSidebar>
{children}
</SidebarPage>
);
};
@@ -1,52 +0,0 @@
/*
* Copyright 2020 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 {
BottomNavigation as MUIBottomNavigation,
makeStyles,
} from '@material-ui/core';
import React from 'react';
const useStyles = makeStyles({
root: {
position: 'fixed',
bottom: 0,
left: 0,
right: 0,
},
});
/**
* Filters for sidebar groups and reorders them to create a custom BottomNavigation
*/
export const BottomNavigation = ({ children }: React.PropsWithChildren<{}>) => {
const classes = useStyles();
const [value, setValue] = React.useState('recents');
const handleChange = (event: React.ChangeEvent<{}>, newValue: string) => {
setValue(newValue);
};
return (
<MUIBottomNavigation
value={value}
onChange={handleChange}
className={classes.root}
>
{children}
</MUIBottomNavigation>
);
};
@@ -14,15 +14,50 @@
* limitations under the License.
*/
import { BottomNavigationAction } from '@material-ui/core';
import MenuIcon from '@material-ui/icons/Menu';
import {
BottomNavigationAction,
BottomNavigationActionProps,
makeStyles,
} from '@material-ui/core';
import React from 'react';
import { NavLink, useLocation } from 'react-router-dom';
interface SidebarGroupProps extends BottomNavigationActionProps {
to?: string;
injectedSelected?: boolean;
}
const useStyles = makeStyles({
label: {
display: 'none',
},
});
/**
* If the page is mobile it should be BottomNavigationAction - otherwise just a fragment
* Links to page, which will be displayed
* - If 'to' Prop is not defined it will render a Menu page out of the children (if children given)
*/
export const SidebarGroup = ({ children }: React.PropsWithChildren<{}>) => {
return <BottomNavigationAction label="Recents" icon={<MenuIcon />} />;
export const SidebarGroup = ({
to,
label,
icon,
onClick,
injectedSelected,
}: React.PropsWithChildren<SidebarGroupProps>) => {
const classes = useStyles();
const location = useLocation();
return (
<BottomNavigationAction
label={label}
icon={icon}
component={NavLink}
to={to ? to : location.pathname}
onClick={onClick}
value={to}
selected={injectedSelected}
classes={classes}
/>
);
};
@@ -0,0 +1,96 @@
/*
* Copyright 2020 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 { BottomNavigation, Box, makeStyles } from '@material-ui/core';
import React, { useState, useEffect } from 'react';
import { useLocation } from 'react-router';
import { SidebarGroup } from './Group';
const useStyles = makeStyles({
root: {
position: 'fixed',
bottom: 0,
left: 0,
right: 0,
zIndex: 1000,
},
});
const OverlayMenu = ({ children }: React.PropsWithChildren<{}>) => (
<Box
style={{
background: 'black',
width: '100%',
height: '100%',
position: 'fixed',
zIndex: 500,
}}
>
{children}
</Box>
);
let currentLocation: string;
/**
* Filters for sidebar groups and reorders them to create a custom BottomNavigation
*/
export const MobileSidebar = ({ children }: React.PropsWithChildren<{}>) => {
const classes = useStyles();
const [value, setValue] = useState<number | undefined>();
const [menu, setMenu] = useState<React.ReactNode | undefined>();
const location = useLocation();
useEffect(() => {
if (menu && currentLocation !== location.pathname) {
setMenu(undefined);
setValue(undefined);
}
currentLocation = location.pathname;
}, [location, menu]);
return (
<React.Fragment>
{value && menu && <OverlayMenu children={menu} />}
<BottomNavigation className={classes.root}>
{React.Children.map(children, (child, index) => {
if (
React.isValidElement(child) &&
(child as React.ReactElement).type === SidebarGroup
) {
if (index === value && !menu && !child.props.to) {
setMenu(child.props.children);
}
return React.cloneElement(child, {
injectedSelected: value
? index === value
: location.pathname === child.props.to,
onClick: () => {
if (index === value && menu) {
setValue(undefined);
setMenu(undefined);
} else {
setValue(index);
}
},
});
}
return null;
})}
</BottomNavigation>
</React.Fragment>
);
};
@@ -25,19 +25,11 @@ import { sidebarConfig } from './config';
import { BackstageTheme } from '@backstage/theme';
import { LocalStorage } from './localStorage';
export type SidebarPageClassKey = 'root';
const useStyles = makeStyles<BackstageTheme, { isPinned: boolean }>(
{
root: {
width: '100%',
minHeight: '100%',
transition: 'padding-left 0.1s ease-out',
paddingLeft: ({ isPinned }) =>
isPinned
? sidebarConfig.drawerWidthOpen
: sidebarConfig.drawerWidthClosed,
},
const useStyles = makeStyles<BackstageTheme, { isPinned: boolean }>({
root: {
width: '100%',
minHeight: '100%',
transition: 'padding-left 0.1s ease-out',
},
{ name: 'BackstageSidebarPage' },
);
@@ -45,5 +45,5 @@ export type SidebarContextType = {
};
export const SidebarContext = createContext<SidebarContextType>({
isOpen: false,
isOpen: true,
});
@@ -15,7 +15,7 @@
*/
export { Sidebar } from './Bar';
export { BottomNavigation } from './BottomNavigation';
export { MobileSidebar } from './MobileSidebar';
export { SidebarGroup } from './Group';
export { SidebarPage, SidebarPinStateContext } from './Page';
export type { SidebarPinStateContextType, SidebarPageClassKey } from './Page';